在程序的认证过程中,除了常规的用户名和密码方式(可以参考深入理解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测试成功,结果如下:
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13631.html