概要
Spring Security 是一个很常用的安全框架,当然老外写的框架很多时候还是不会适应咱们的国情,比如现在的登录,手机号加验证码才是主流,毕竟密码太多,谁都会忘。而其默认的认证方式还是username 加 password的方式:UsernamePasswordAuthenticationToken
。本文讲述了如何使用解决手机号和验证码的方式完成认证。
参考资料
这里我参考几篇文章,不过大部分的文章还在用WebSecurityConfigurerAdapter
,这个已经不适用于最新的SpringSecurity了,还有就是一般会让你写token,provider,和filter三个文件。
SpringSecurity自定义实现手机短信登录
Spring Security学习(六)——配置多个Provider(存在两种认证规则)
整体架构流程
本文会相对来说比较简单,同时本文建立在第四节 springsecurity结合jwt实现前后端分离开发的基础上进行开发的。
- 创建一个
SmsAuthenticationProvider
- `ProviderManager``中配置两个provider
SmsAuthenticationProvider
首先我们参考DaoAuthenticationProvider写一个针对手机号验证码的认证提供者。我们需要继承AbstractUserDetailsAuthenticationProvider
,然后实现retrieveUser和additionalAuthenticationChecks,第一个方法其实基本不用动,核心就是在第二个方法里面对验证码进行判断
package com.sbvadmin.config;
import com.jayway.jsonpath.Criteria;
import com.sbvadmin.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class SmsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserServiceImpl userService;
@Autowired
RedisTemplate redisTemplate;
private UserDetailsService userDetailsService;
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedCode = authentication.getCredentials().toString(); // 待匹配的验证码
Object codeInCache = redisTemplate.opsForValue().get(userDetails.getUsername()); // 短信生成的验证码
if (codeInCache == null){
this.logger.info("验证码已经失效");
throw new BadCredentialsException("验证码已经失效");
}
if (!codeInCache.toString().equals(presentedCode)){
this.logger.info("验证码错误");
throw new BadCredentialsException("验证码错误");
}
}
}
ProviderManager中配置两个provider
@Bean
DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
daoAuthenticationProvider.setUserDetailsService(userServiceDetails);
return daoAuthenticationProvider;
}
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
smsAuthenticationProvider.setUserDetailsService(userServiceDetails);
// 加入两个provider
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(daoAuthenticationProvider(),smsAuthenticationProvider));
authenticationManager.setEraseCredentialsAfterAuthentication(false);
return authenticationManager;
}
小结
其实这里的核心就是看懂getAuthenticationManager().authenticate执行认证时候的代码,里面对provider进行了循环认证,默认的只有daoAuthenticationProvider一个,我们再加一个smsAuthenticationProvider的provider就能很好的解决这个问题,然后登录接口地址也不变,十分方便。唯一不足的就是提示方面弱一些,统一会提示用户名或密码或验证码错误。
代码地址:https://github.com/billyshen26/sbvadmin