项目场景:

APP登录不仅仅是用户名和密码登录,同时还有手机验证码登录、第三方登录等。这回就说说手机验证码以及APP用户名和密码登录吧。


技术详解:

自定义认证服务其实很简单,简化步骤如下

1.自定义grant_type

2.自定义Authentication

3.实现AbstractTokenGranter

4.自定义AuthenticationProvider

后续我们就按照这个步骤一步一步实现我们的想要的功能,满足我们的需求。

用户名密码登录:

之前的用户名和密码登录,/oauth/token的grant_type默认是password,不过这里会把请求的用户名和密码暴露出去,因此我的想法是中间加密,然后解密之后再进行认证.因此我打算把之前的认证方式进行增强。

按照上面的逻辑,来一步一步来实践。

第一步:自定义grant_type,定义新的用户名和密码的grant_type是password_login

第二步:自定义Authentication(复用老的UsernamePasswordAuthenticationToken)

第三步:实现AbstractTokenGranter,这里直接附上代码

/**
 * 自定义用户名密码登录,在原有的密码账号登录上进行增强
 */
public class UserPasswordTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "password_login";

    private final AuthenticationManager authenticationManager;

    public UserPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected UserPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        String sign = parameters.get("sign");

        String username = AESUtil.AESDecrypt(sign)[0];
        String encodePassword = AESUtil.AESDecrypt(sign)[1];


        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, encodePassword);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException("当前用户已经被锁定,请联系客服.");
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException("用户信息查询异常,请确认是否已注册.");
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
    }

}

这里的逻辑其实可以参考源码ResourceOwnerPasswordTokenGranter,只不过是获取参数的地方进行了修改。由于用户名和密码的验证逻辑大部分可以复用,因此不需要第三步骤和第四步骤,不过按照流程来还是写一下吧

第四步:实现AuthenticationProvider(复用老的DaoAuthenticationProvider)

手机验证码:

有了前面用户名和密码的增强,现在我们来实现手机验证码的自定义认证.

第一步:发送手机验证码

这个逻辑可以自己实现,这边就不贴代码了。看看验证码存储方式吧,如果是单台机器的话就直接放到session里面,如果是多台的话就放到redis里面

第二步:自定义grant_type,定义新的用户名和密码的grant_type是sms_login

第三步:自定义Authentication,继承UsernamePasswordAuthenticationToken,因为后续有手机号加密码登录的方式

/**
 * 手机验证码token
 */
public class PhoneAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private String phone;

    private String code;

    /**
     * @param principal 用户名
     */
    public PhoneAuthenticationToken(Object principal, Object credentials,String phone,String code) {
        super(principal, credentials);
        setAuthenticated(false);
        this.phone = phone;
        this.code = code;
    }
    
    public String getPhone() {
        return phone;
    }

    public String getCode() {
        return code;
    }
}

第四步.实现AbstractTokenGranter,这里直接附上代码(代码仅供参考)

/**
 * 自定义认证,手机验证码登录
 */
@Slf4j
public class PhoneTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "sms_code";

    protected final AuthenticationManager authenticationManager;

    protected final AppUserDao appUserDao;

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

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest){

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String phone = parameters.get("phone");
        String code = parameters.get("code");

        if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code))
            throw new InvalidGrantException("参数错误.");

        AppParam appParam = new AppParam();
        appParam.setTelphone(phone);
        AppUser appUser = appUserDao.selectUserByCondition(appParam);

        log.info("phone = {}, code = {}, appUser = {}",phone,code, JSON.toJSONString(appUser));
        // 根据手机号码查询用户信息
        if (appUser == null)
            throw new InvalidGrantException("手机号码填写错误.");

        Authentication userAuth = new PhoneAuthenticationToken(appUser.getUserName(), appUser.getPassword(), phone,code);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException("当前用户已经被锁定,请联系客服.");
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException("用户信息查询异常,请确认是否注册.");
        } catch (InternalAuthenticationServiceException var10){
            throw new InvalidGrantException("验证码校验失败.");
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + phone);
        }

    }

}

第五步:自定义AuthenticationProvider

/**
 * 手机验证码认证新增额外认证
 * 认证验证码是否正确
 */
@Slf4j
public class PhoneAuthenticationProvider extends DaoAuthenticationProvider {

    protected final RedisUtil redisUtil;

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    public PhoneAuthenticationProvider(RedisUtil redisUtil) {
        super();
        this.redisUtil = redisUtil;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

        PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication;

        try {

            PhoneVerifyCodeVO codeVO = redisUtil.get(PhoneVerifyCodeVO.getPhoneVerifyCodeKey(PhoneVerifyCodeVO.LOGIN_TYPE, authenticationToken.getPhone()), PhoneVerifyCodeVO.class);

            if (codeVO == null || codeVO.isExpireTime())
                throw new InternalAuthenticationServiceException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "验证码已过期."));
            String saveCode = codeVO.getVerifyCode();
            String code = authenticationToken.getCode();
            if (StringUtils.isEmpty(code) || !StringUtils.pathEquals(saveCode, code))
                throw new InternalAuthenticationServiceException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "验证码填写错误."));
        } finally {
            // 删除验证码
            redisUtil.delete(PhoneVerifyCodeVO.getPhoneVerifyCodeKey(PhoneVerifyCodeVO.LOGIN_TYPE, authenticationToken.getPhone()));
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

参考DaoAuthenticationProvider,重写了additionalAuthenticationChecks方法里面的内容。这里需要注意的是抛出的异常。不能够照抄,要不然会出现验证码错误但是也可以认证通过的问题。

配置信息:

好了,我们现在写好了这些东西,不过这还没完。我们还需要把这些provider配置才有效果.

/**
 * 登录验证
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    protected UserDetailsService myUserDetailsService;
    
    @Autowired
    protected RedisUtil redisUtil;


    // 省略部分---
    
    /**
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().sameOrigin();
        PhoneAuthenticationProvider provider = new PhoneAuthenticationProvider(redisUtil);
        provider.setUserDetailsService(myUserDetailsService);
        
    }    
    

}

 

@EnableAuthorizationServer
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

     @Autowired
    protected PasswordEncoder passwordEncoder;

    @Autowired
    protected UserDetailsService myUserDetailsService;

    @Autowired
    protected AuthenticationManager authenticationManager;

    @Autowired
    protected TokenStore redisTokenStore;

    @Autowired
    protected DataSource dataSource;

    @Autowired
    protected AppUserDao appUserDao;

    @Autowired
    protected ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    protected CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator;

    @Override
    @SneakyThrows
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {

        /**
         * redis token 方式
         */
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService)
                .tokenStore(redisTokenStore);

        // 初始化所有的TokenGranter,并且类型为CompositeTokenGranter
        List<TokenGranter> tokenGranters = getDefaultTokenGranters(endpoints);

        endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters))
                // 配置tokenStore,使用redis 存储token
                .tokenStore(redisTokenStore)
                .authenticationManager(authenticationManager)
                // 用户管理服务
                .userDetailsService(myUserDetailsService)
                // 配置令牌转化器
                // 允许 GET、POST 请求获取 token,即访问端点:oauth/token
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .reuseRefreshTokens(false);
        //      将增强的token设置到增强链中
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer()));
        endpoints.tokenStore(redisTokenStore)
                .userDetailsService(myUserDetailsService)
                .tokenEnhancer(enhancerChain);
        endpoints.exceptionTranslator(customWebResponseExceptionTranslator);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }

    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

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

    @Bean
    public TokenEnhancer customTokenEnhancer() {
        return new CustomTokenEnhancer();
    }

    /**
     * 初始化所有的TokenGranter
     */
    private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {

        ClientDetailsService clientDetails = endpoints.getClientDetailsService();
        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();

        List<TokenGranter> tokenGranters = new ArrayList<>();
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
                requestFactory));
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
        tokenGranters.add(implicit);
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
        if (authenticationManager != null) {
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
                    clientDetails, requestFactory));
            tokenGranters.add(new UserPasswordTokenGranter(authenticationManager, tokenServices,
                    clientDetails, requestFactory));
            
            tokenGranters.add(new PhoneTokenGranter(authenticationManager, endpoints.getTokenServices(),
                    endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(),appUserDao));
        }
        return tokenGranters;
    }

    @Bean
    public ResourceServerTokenServices resourceServerTokenServices(){
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenEnhancer(customTokenEnhancer());
        defaultTokenServices.setTokenStore(redisTokenStore);
        defaultTokenServices.setClientDetailsService(clientDetails());
        return defaultTokenServices;
    }
    
}