Spring Security Oauth2关于自定义登录的几种解决方案(二)

Spring Security Oauth2关于自定义登录的几种解决方案(二)

  • 本篇采用新增TokenGranter完成自定义登录
    • 第一步:创建一个新的Token,继承AbstractAuthenticationToken
    • 第二步:创建AccountAuthenticationProvider,实现AuthenticationProvider
    • 第三步:将AccountAuthenticationProvider放入到authenticationProviders中,参考第一篇
    • 第四步:创建AccountGranter,继承AbstractTokenGranter
    • 第五步:将AccountGranter放入到GranterList中
    • 第六步:创建controller登录接口,/login/user

上一篇文章,通过简单的方式进行自定义用户登录授权的动作,投机取巧使用了他的内部循环处理机制,完成了我们所需要的功能,本篇将介绍一下自定义模式的登录方式(例如:原始oauth2自带的密码模式,授权码模式,简单模式,客户端模式)。最终其实我们还是采用了密码模式的思路,只是修改了两个参数而已。

本篇采用新增TokenGranter完成自定义登录

第一步:创建一个新的Token,继承AbstractAuthenticationToken

其中,Token的关键在于Authenticated是否授权,需要两个构造,一个为未鉴权Token,以及一个已鉴权Token,后续也需要判断是否为该Token,才进行相关判断

package com.example.customoauth.authentication.account;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;

/**
我个人将验证码以及密码模式封装在一起,所以传入三个参数,通过判断判断短信验证码是否为空来决定采用哪种验证方式,是密码验证还是短信验证码验证,
**/
public class AccountLoginToken extends AbstractAuthenticationToken {
     
    //用户名
    private final Object principal;
    //密码
    private Object credentials;
    //短信验证码
    private String vcode;
    /**
     * 构建一个没有鉴权的 AccountLoginToken,手机号,密码,验证码
     */
    public AccountLoginToken(Object principal, Object credentials,String vcode) {
     
        super(null);
        this.principal = principal;
        this.credentials=credentials;
        this.vcode = vcode;
        setAuthenticated(false);
    }
    /**
     * 构建拥有鉴权的 AccountLoginToken
     */
    public AccountLoginToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
     
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }
    @Override
    public Object getCredentials() {
     
        return this.credentials;
    }
    @Override
    public Object getPrincipal() {
     
        return this.principal;
    }
    public String getVcode() {
     
        return this.vcode;
    }
}

第二步:创建AccountAuthenticationProvider,实现AuthenticationProvider

此功能在第一篇已经介绍,不过多赘述,直接贴入代码

package com.example.customoauth.authentication.account;

import com.alibaba.fastjson.JSON;
import com.yfhcloud.common.yfhutils.Constant;
import com.yfhcloud.common.yfhutils.WebResponse;
import com.yfhcloud.common.yfhutils.enums.ResultEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * 后端短信认证校验
 * 原理:AuthenticationProvider通常按照认证请求链顺序去执行,
 * 一个返回非null响应表示程序验证通过,
 * 不再尝试验证其它的provider;如果后续提供的身份验证程序 成功地对请求进行身份认证,
 * 则忽略先前的身份验证异常及null响应,并将使用成功的身份验证。
 * 如果没有provider提供一个非null响应,或者有一个新的抛出AuthenticationException,
 * 那么最后的AuthenticationException将会抛出。
 *
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/22 15:51
 */
@Component
@Slf4j
public class AccountAuthenticationProvider implements AuthenticationProvider {
     

	//这个代码我就不贴了,上一篇有,根据自身业务进行修改即可
    @Autowired
    @Qualifier("securityUserDetailsService")
    private UserDetailsService securityUserDetailsService;

	//由于使用@Autowired进行加载,似乎有冲突,暂时我默认直接在用的地方加载,第一篇那地方也需要修改以下,暂时不在config中用@bean进行初始化。
    private PasswordEncoder passwordEncoder= PasswordEncoderFactories.createDelegatingPasswordEncoder();

    //注入REDIS,存放短信验证码使用
    @Autowired
    private RedisTemplate redisTemplate;


    //认证方法
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
     
        AccountLoginToken accountLoginToken = (AccountLoginToken) authentication;
        System.out.println("===进入USER登录验证环节=====" + JSON.toJSONString(accountLoginToken));
        UserDetails userDetails = securityUserDetailsService.loadUserByUsername(accountLoginToken.getName());
        if (StringUtils.isNotBlank(accountLoginToken.getVcode())) {
     
            //如果vcode不为空,则为短信验证码校验
            if (redisTemplate.opsForValue().get(Constant.REDIS_SMS_LOGIN_CODE + accountLoginToken.getName()) != null) {
     
                String redisCode = redisTemplate.opsForValue().get(Constant.REDIS_SMS_LOGIN_CODE + accountLoginToken.getName()).toString();
                if (!redisCode.equalsIgnoreCase(accountLoginToken.getVcode())) {
     
                    throw new BadCredentialsException("验证码不正确");
                }
            } else {
     
                throw new BadCredentialsException("验证码已失效!");
            }
            //校验成功,返回一个授权过的Token
            return new AccountLoginToken(userDetails, userDetails.getAuthorities());
        } else {
     
            //其他模式直接采用用户密码判断
            if (passwordEncoder.matches(authentication.getCredentials().toString(), userDetails.getPassword())) {
     
                //返回一个授权过的Token
                return new AccountLoginToken(userDetails, userDetails.getAuthorities());
            }
        }
        throw new BadCredentialsException("用户名密码不正确");
    }

    //判断是否支持自定义的Token,从而决定是否需要进行认证校验,当使用accountLoginToken进行校验
    @Override
    public boolean supports(Class<?> authentication) {
     
        return AccountLoginToken.class.isAssignableFrom(authentication);
    }
}

第三步:将AccountAuthenticationProvider放入到authenticationProviders中,参考第一篇

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.userDetailsService(securityUserDetailsService);
        auth.authenticationProvider(adminSmsAuthenticationProvider);
        auth.authenticationProvider(adminPwdAuthenticationProvider);
        auth.authenticationProvider(accountAuthenticationProvider);
    }

第四步:创建AccountGranter,继承AbstractTokenGranter

该类主要进行的就是封装的token传入到security的authenticationManager进行校验动作,在上一篇也描述了,密码模式采用的UserPassword**Token进行处理,这里我们需要修改成自定义的AccountLoginToken传入到authenticationManager中。
AbstractTokenGranter中的granter方法,主要先校验clientId,后校验用户信息。所以我们主要重写的getOAuth2Authentication方法,进行后续处理

package com.example.customoauth.authentication.account;

import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 自定义模式
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/28 15:47
 */
public class AccountGranter extends AbstractTokenGranter {
     

    //模式名称
    public static final String GRANT_TYPE = "account_mobile";
    private final AuthenticationManager authenticationManager;

    public  AccountGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
     
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
     
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        //此处map为controller封装的参数map,名字也可以自己定义
        //从参数中获取手机号
        String mobile = parameters.get("username");
        //参数中获取密码
        String password = parameters.get("password");
        //参数中获取密码
        String vcode = parameters.get("vcode");

        //删除参数中的两项(这里我暂时还不清楚为啥要删除,不清楚这个传输会引起什么问题,源码中删除那么我先暂时删除)
        parameters.remove("password");
        parameters.remove("vcode");

        //暂时将三个全部传入
        Authentication userAuth = new AccountLoginToken(mobile, password,vcode);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
     
            userAuth = authenticationManager.authenticate(userAuth);
        }catch (AccountStatusException | BadCredentialsException ase) {
     
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        }
        // If the username/password are wrong the spec says we should send 400/invalid grant
        if (userAuth == null || !userAuth.isAuthenticated()) {
     
            throw new InvalidGrantException("校验失败: " + mobile);
        }

        //验证通过,则返回一个Oauth2token。
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

第五步:将AccountGranter放入到GranterList中

package com.example.customoauth.config;

import com.example.customoauth.authentication.account.AccountGranter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/8 16:19
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
    /**
     * 用户认证 Manager
     */
    @Autowired
    private AuthenticationManager authenticationManager;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     
        endpoints.authenticationManager(authenticationManager);
        //放入自定义授权模式,先把默认的几个模式放进去。最后加入我们自定义模式。
        List<TokenGranter> grantersList = new ArrayList<TokenGranter>(Arrays.asList(endpoints.getTokenGranter()));
        grantersList.add(new AccountGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), authenticationManager));
        endpoints.tokenGranter(new CompositeTokenGranter(grantersList));
        //此处list并不是5,而是2,因为第一个是一个config,具体你们可以在启动的时候调试看内部信息。
        System.out.println("=====长度:"+grantersList.size());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
     
        oauthServer.allowFormAuthenticationForClients();
        oauthServer.checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     
    //此处要注意给这个client进行授权新模式,否则校验不通过
        clients.inMemory() // <4.1>
                .withClient("clientapp").secret("{noop}112233") // <4.2> Client 账号、密码。
                .authorizedGrantTypes("password",AccountGranter.GRANT_TYPE) // <4.2> 密码模式
                .scopes("read_userinfo", "read_contacts") // <4.2> 可授权的 Scope
//                .and().withClient() // <4.3> 可以继续配置新的 Client
        ;
    }


}

第六步:创建controller登录接口,/login/user

package com.example.customoauth.web;

import com.example.customoauth.authentication.account.AccountGranter;
import com.example.customoauth.authentication.account.AccountLoginToken;
import com.example.customoauth.request.UserRequest;
import com.yfhcloud.common.yfhutils.WebResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author : Windy
 * @version :1.0
 * @since : 2020/12/9 16:44
 */
@RestController
@RequestMapping("/")
@Slf4j
public class LoginController {
     

    @Autowired
    private OAuth2ClientProperties oauth2ClientProperties;
    @Autowired
    private TokenEndpoint tokenEndpoint;


    @PostMapping("/login/admin")
    public WebResponse adminLogin(UserRequest request) {
     

        Map<String, String> parameters = createClientParameters(request.getPhone(),request.getVcode(),"password");
        try {
     
            OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(createClientToken(), parameters).getBody();
            return WebResponse.success(oAuth2AccessToken);
        } catch (Exception e) {
     
            log.error("登录失败!!" + e.getMessage());
        }
        return WebResponse.error("登录失败,请重试!");

    }


    @PostMapping("/login/user")
    public WebResponse userLogin(UserRequest request) {
     
        //封装所需要的参数
        Map<String, String> parameters = createClientParameters(request.getPhone(),request.getPassword(), AccountGranter.GRANT_TYPE);
        //放入可能需要的vcode,后续TokenGranter需要
        parameters.put("vcode",request.getVcode());
        User u = new User(oauth2ClientProperties.getClientId(), oauth2ClientProperties.getClientSecret(), new ArrayList<>());
        //生成验证过的clientUsertoken
        AccountLoginToken token = new AccountLoginToken(u,new ArrayList<>());

        try {
     
            OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(token, parameters).getBody();
            return WebResponse.success(oAuth2AccessToken);
        } catch (Exception e) {
     
            log.error("登录失败!!" + e.getMessage());
        }
        return WebResponse.error("登录失败,请重试!");
    }


    //构造客户端请求
    private Map<String, String> createClientParameters(String username, String password,String grantType) {
     
        Map<String, String> parameters = new HashMap<String, String>();
        parameters.put("username", username);
        parameters.put("password", password);
        parameters.put("grant_type", grantType);
        return parameters;
    }

    //构造客户端Token
    private UsernamePasswordAuthenticationToken createClientToken() {
     
        //客户端信息
        User u = new User(oauth2ClientProperties.getClientId(), oauth2ClientProperties.getClientSecret(), new ArrayList<>());
        //生成已经认证的client
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(u, null, new ArrayList<>());
        return token;
    }


}

至此,整个登录就结束了,这种好处是没有那么多provider去循环。毕竟在模式中就已经选定了,目前这两种应该是按照security oauth2的逻辑进行重写,实现这套登录逻辑,网上很多在filter中去写校验我个人认为还是有点不合理。违背了源码中的设计方式。

目前仅仅将登录这部分代码整理清楚,源码我暂时还不对外发布到git上,毕竟现在还是个半成品,连client都还在内存中,这肯定不合理。等后续全部写好,并上线,我再将整个模块放上去,在此,有任何疑问或者问题,都可以在评论区留言,我这边有问题会及时回复,共同探讨,,如果对我的理解有不同的理解和看法,欢迎指正,共同分析源码,找出依据,完善整个思路。

可惜spring security oauth2在3.4以上版本似乎全部被淘汰。只能等全新的 【spring-authorization-server】项目成熟上线。后续我将持续关注。有新的权限框架,我会继续更新。

大家在使用【spring-authorization-server】这个项目里的demo进行试验的时候一定一定要注意,不要修改里面的任何地址,包括里面有个设置,需要你在hosts中设置虚拟域名指向,也一定要去设置,如果你贸然改成localhost将会出现问题,导致不能使用,具体原因我也不太清楚。所以一定要设置hosts虚拟域名。不会设置请百度。

接下来,我会写一些spring security oauth2鉴权方面的文章,如果不复杂,我将找几篇好的文章分享给大家,我就不过多赘述,坚决不抄袭,只负责整理,不会把别人的文章全部抄过来或者转载,最多发链接和自己的见解,拒绝搬运,只做原创。

你可能感兴趣的:(Spring,security,oauth2,oauth,java,spring)