一、工作流程
1.向手机发送验证码,第三方短信发送平台,如阿里云短信。
2.手机获取验证码后,在表单中输入验证码。
3.使用自定义过滤器SmsCodeValidateFilter。
4.短信校验通过后,使用自定义手机认证过滤器SmsCodeAuthenticationFilter校验手机号码是否存在。
5.自定义SmsCodeAuthenticationToken提供给SmsCodeAuthenticationFilter。
6.自定义SmsCodeAuthenticationProvider提供给AuthenticationManager。
7.创建针对手机号查询用户信息的SmsCodeUserDetailsService,提交给。SmsCodeAuthenticationProvider。
8.自定义SmsCodeSecurityConfig配置类将上面组件连接起来。
9.将SmsCodeSecurityConfig添加到LearnSrpingSecurity安全配置的过滤器链上。
二、实现
2.1、验证码生成、发送
/**
* 创建验证码生成器
*/
@Component
public class SmsCodeGenerator {
public String generate() {
return RandomStringUtils.randomNumeric(4);
}
}
/**
* 验证码发送器
*/
@Component
public class SmsCodeSender {
public void send(String mobile, String code) {
System.out.println("向手机" + mobile + "发送短信验证码" + code);
}
}
/**
* 发送短信接口
*/
@RestController
public class ValidateCodeController {
@Autowired
private SmsCodeGenerator smsCodeGenerator;
@Resource
private SmsCodeSender smsCodeSender;
@Resource
private RedisTemplate redisTemplate;
@GetMapping("/code/sms")
public String createSmsCode(@RequestParam String mobile) throws IOException {
//获取验证码
String smsCode = smsCodeGenerator.generate();
//把验证码设置到redis
redisTemplate.boundValueOps(SecurityConstants.getValidCodeKey(mobile)).set(smsCode, 300, TimeUnit.SECONDS);
smsCodeSender.send("18360903475", "登录验证码为:" + smsCode + ",五分钟过期");
return "验证码是 : " + smsCode;
}
}
2.2、手机号码认证 Token
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 手机号码认证 Token
*/
public class PhoneNumAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* principal的作用有两个, 在未登录之前是用户名,那么在登录之后是用户的信息。
*/
private final Object principal;
/**
* 构造
* @param principal 手机号码
*/
public PhoneNumAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(false);
}
/**
* 构造
* @param principal 用户信息
* @param authorities 用户权限列表
*/
public PhoneNumAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(true);
}
/**
* 正常这个是返回密码,但手机登录没有密码,不用管
*/
@Override
public Object getCredentials() {
return null;
}
/**
* 获取手机号或用户信息
*/
@Override
public Object getPrincipal() {
return this.principal;
}
}
2.3、拦截请求、获取手机号码
/**
* 手机号码拦截器, 获取手机号码
*/
public class PhoneNumAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public PhoneNumAuthenticationFilter() {
super(new AntPathRequestMatcher("/phoneLogin", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!Objects.equals(request.getMethod(),"POST")) {
throw new AuthenticationServiceException("身份验证方法需为:'POST'请求");
}
// 获取手机号
String phoneNum = Optional.ofNullable(request.getParameter(Constants.PHONE_NUM_PARAMETER)).map(String::trim).orElse("");
// new 手机号码验证Token
PhoneNumAuthenticationToken authRequest = new PhoneNumAuthenticationToken(phoneNum);
// 身份验证详细信息
authRequest.setDetails(super.authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2.4、短信验证码验证过滤器
/**
* 短信验证码验证过滤器
*/
@Component
public class SmsCodeFilter extends OncePerRequestFilter {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {
/**
* uri = /phoneLogin 即手机号码登录才拦截
*/
if (Objects.equals(Constants.SMS_LOGIN_URI,request.getRequestURI())) {
try{
// 验证手机验证码
validateProcess(request);
}catch (AuthenticationException ex) {
customizeAuthencationFailureHandler.onAuthenticationFailure(request, response, ex);
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 验证手机验证码
*/
private void validateProcess(HttpServletRequest request){
// 获取手机号
String msgCode = stringRedisTemplate.opsForValue().get(Constants.SMS_CODE_SESSION_KEY);
String code = request.getParameter(Constants.MSG_CODE);
if(Strings.isBlank(code)) {
throw new InternalAuthenticationServiceException("短信验证码不能为空.");
}
if(null == msgCode) {
throw new InternalAuthenticationServiceException("短信验证码已失效.");
}
if(!code.equals(msgCode)) {
throw new InternalAuthenticationServiceException("短信验证码错误.");
}
}
}
2.5、继承 WebSecurityConfigurerAdapter 配置 HttpSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 数据源
*/
@Resource
private DataSource dataSource;
/**
* 用户信息服务
*/
@Resource
private UserAuthentication userAuthentication;
/**
* 成功处理
*/
@Resource
private CustomizeAuthencationSuccessHandler customizeAuthencationSuccessHandler;
/**
* 失败处理
*/
@Resource
private CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;
/**
* 用户登出处理
*/
@Resource
private UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 多用户登录处理
*/
@Resource
private MutilpleSessionHandler mutilpleSessionHandler;
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 手机号码登录验证处理
*/
@Resource
private DaoPhoneNumAuthenticationProvider daoPhoneNumAuthenticationProvider;
/**
* 信息验证码过滤器
*/
@Resource
private SmsCodeFilter smsCodeFilter;
/**
* 把AuthenticationManager公开
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置自定义验证查询/加密工具
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userAuthentication).passwordEncoder(passwordEncoder());
}
/**
* 手机号码登录拦截器
*/
@Bean
public PhoneNumAuthenticationFilter phoneNumAuthenticationFilter() throws Exception {
// 手机号码拦截器, 获取手机号码
PhoneNumAuthenticationFilter phoneNumAuthenticationFilter = new PhoneNumAuthenticationFilter();
phoneNumAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
//使用手机号登录失败了如何处理
phoneNumAuthenticationFilter.setAuthenticationFailureHandler(customizeAuthencationFailureHandler);
// 使用手机号登录成功了如何处理
phoneNumAuthenticationFilter.setAuthenticationSuccessHandler(customizeAuthencationSuccessHandler);
return phoneNumAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 加入短信验证码过滤器
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 加入手机号码登录过滤器
.addFilterAfter(phoneNumAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 加入手机号码登录验证提供者
.authenticationProvider(daoPhoneNumAuthenticationProvider)
// 表单登录
.formLogin()
// 未登录跳转登录页面
.loginPage("/login.html")
// 指定登录路径
.loginProcessingUrl("/login")
// 用户登录成功的处理
.successHandler(customizeAuthencationSuccessHandler)
// 用户登录失败的处理
.failureHandler(customizeAuthencationFailureHandler)
// 因为用户传入过来的token, 需要再次进行校验
.userDetailsService(userAuthentication)
.tokenValiditySeconds(3600)
// .alwaysRemember(true)
// 认证配置
.and()
.authorizeRequests()
//不拦截的Url
.antMatchers("/login.html", "/image/code", "/smsCode", "/css/**", "/js/**", "/phoneLogin").permitAll()
.anyRequest() //所有的请求
.authenticated() //认证之后就可以访问
// 多端登录限制,限制一个账号同时只能一个人登录
.and()
.sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(mutilpleSessionHandler)
.and()
// 登出配置
.and()
.logout()
.logoutUrl("/logout")
// 登出成功处理
.logoutSuccessHandler(userLogoutSuccessHandler)
.and()
.csrf().disable();
}
}