玩转Spring Cloud Security OAuth2身份认证扩展——电话号码+验证码认证

在程序的认证过程中,除了常规的用户名和密码方式(可以参考深入理解Spring Cloud Security OAuth2身份认证),也经常会出现电话号码+密码的方式;电话号码+验证码的方式;或者第三方软件的方式。以下,以电话号码+验证码的方式讲述OAuth2认证方式的扩展。

认证对象(PhoneAndVerificationCodeAuthenticationToken )

在OAuth2认证开始认证时,会提前Authentication认证信息,然后交由AuthenticationManager认证。定义电话号码+验证码的Authentication认证信息,源码如下:

public class PhoneAndVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {

    /**
     * 手机号
     */
    private final Object phoneNumber;

    /**
     * 验证码
     */
    private final Object verificationCode;

    public PhoneAndVerificationCodeAuthenticationToken(Object phoneNumber, Object verificationCode) {
        super(null);
        this.phoneNumber = phoneNumber;
        this.verificationCode = verificationCode;
    }

    public PhoneAndVerificationCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object phoneNumber, Object verificationCode) {
        super(authorities);
        this.phoneNumber = phoneNumber;
        this.verificationCode = verificationCode;
        // 认证已经通过
        setAuthenticated(true);
    }

    /**
     * 用户身份凭证(一般是密码或者验证码)
     */
    @Override
    public Object getCredentials() {
        return verificationCode;
    }

    /**
     * 身份标识(一般是姓名,手机号)
     */
    @Override
    public Object getPrincipal() {
        return phoneNumber;
    }
}

认证逻辑(PhoneAndVerificationCodeAuthenticationProvider )

在AuthenticationManager认证过程中,是通过AuthenticationProvider接口的扩展来实现自定义认证方式的。定义手机和验证码认证提供者PhoneAndVerificationCodeAuthenticationProvider,其源码如下:

public class PhoneAndVerificationCodeAuthenticationProvider implements AuthenticationProvider {

    /**
     * UserDetailsService
     */
    private final MallUserDetailsService mallUserDetailsService;

    /**
     * redis服务
     */
    public final RedisTemplate<String, Object> redisTemplate;

    public PhoneAndVerificationCodeAuthenticationProvider(MallUserDetailsService mallUserDetailsService, RedisTemplate<String, Object> redisTemplate) {
        this.mallUserDetailsService = mallUserDetailsService;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        PhoneAndVerificationCodeAuthenticationToken phoneAndVerificationCodeAuthenticationToken = (PhoneAndVerificationCodeAuthenticationToken) authentication;
        Object verificationCodeObj;
        String verificationCode = Objects.nonNull(verificationCodeObj = phoneAndVerificationCodeAuthenticationToken.getCredentials()) ?
                verificationCodeObj.toString() : StringUtils.EMPTY;
        // TODO 验证授权码
        // 验证用户
        Object phoneNumberObj;
        String phoneNumber = Objects.nonNull(phoneNumberObj = phoneAndVerificationCodeAuthenticationToken.getPrincipal())
                ? phoneNumberObj.toString() : StringUtils.EMPTY;
        if (StringUtils.isBlank(phoneNumber)) {
            throw new InternalAuthenticationServiceException("phone number is empty!");
        }
        // 根据电话号码获取用户
        UserDetails userDetails = mallUserDetailsService.loadUserByPhoneNumber(phoneNumber);
        if (Objects.isNull(userDetails)) {
            throw new InternalAuthenticationServiceException(
                    "MallUserDetailsService returned null, which is an interface contract violation");
        }
        // 封装需要认证的PhoneAndVerificationCodeAuthenticationToken对象
        return new PhoneAndVerificationCodeAuthenticationToken(userDetails.getAuthorities(), phoneNumber, verificationCode);
    }

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

认证TokenGranter(PhoneAndVerificationCodeTokenGranter )

spring security OAuth2中,默认的只有授权码类型,简化类型,密码类型,客户端类型。这里需要新增一种电话号码+验证码的认证和生成访问授权码的TokenGranter。接口TokenGranter定义了token获取方法,源码如下:

public interface TokenGranter {

	OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);

}

spring security使用了组合模式定义了上述4中TokenGranter,如下:

  • ImplicitTokenGranter:简化类型
  • ResourceOwnerPasswordTokenGranter:密码类型
  • AuthorizationCodeTokenGranter:授权码类型
  • ClientCredentialsTokenGranter:客户端类型
  • RefreshTokenGranter:刷新token
  • CompositeTokenGranter:组合类型

CompositeTokenGranter,组合了所有的TokenGranter实现类,其源码如下:

public class CompositeTokenGranter implements TokenGranter {

    // TokenGranter 列表
	private final List<TokenGranter> tokenGranters;

	public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
		this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
	}
	
	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}
	
	public void addTokenGranter(TokenGranter tokenGranter) {
		if (tokenGranter == null) {
			throw new IllegalArgumentException("Token granter is null");
		}
		tokenGranters.add(tokenGranter);
	}

}

新增电话验证码类型,PhoneAndVerificationCodeTokenGranter,参考密码类型ResourceOwnerPasswordTokenGranter的认证流程,首先进行电话号码与验证码的认证,然后生成访问授权码,其源码如下:

public class PhoneAndVerificationCodeTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "cms_code";

    private final AuthenticationManager authenticationManager;

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

    @Autowired
    protected PhoneAndVerificationCodeTokenGranter(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 phoneNumber = parameters.get("phone_number");
        String verificationCode = parameters.get("verification_code");

        Authentication userAuth = new PhoneAndVerificationCodeAuthenticationToken(phoneNumber, verificationCode);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            // authenticationManager进行验证
            userAuth = authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        } catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate phone number: " + phoneNumber);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

自定义认证配置

在资源服务中的配置,主要说下在配置tokenGranter,需要把默认端的全部一起设置,不然就是新增的才有效。tokenGranter配置的的源码如下:

    /**
     * 授权服务端点配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        // 初始化所有的TokenGranter,并且类型为CompositeTokenGranter
        List<TokenGranter> tokenGranters = getDefaultTokenGranters(endpoints);
        tokenGranters.add(new PhoneAndVerificationCodeTokenGranter(authenticationManager, endpoints.getTokenServices(),
                endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));

        endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters))
                // 配置tokenStore,使用redis 存储token
                .tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                // 用户管理服务
                .userDetailsService(userDetailsService)
                // 配置令牌转化器
                .accessTokenConverter(jwtAccessTokenConverter)
                // 允许 GET、POST 请求获取 token,即访问端点:oauth/token
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .reuseRefreshTokens(true);
    }

    /**
     * 初始化所有的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));
        }
        return tokenGranters;
    }

手机号码和验证码认证提供者的配置,源码如下:

    /**
     * 配置HTTP安全
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        // 配置电话号码和验证码认证
        httpSecurity.authenticationProvider(new PhoneAndVerificationCodeAuthenticationProvider(userDetailsService, redisTemplate))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated();
    }

认证测试

在测试时,在db中,配置授权类型为自定义的cms_code,client_id为meituan,client_secret为123456的客户端;配置phone_number为15198273234用户信息,模拟手机验证1234。执行postman测试成功,结果如下:

发布了37 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/new_com/article/details/104707362