项目场景:
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;
}
}