一、工作流程
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 {@Autowiredprivate SmsCodeGenerator smsCodeGenerator;@Resourceprivate SmsCodeSender smsCodeSender;@Resourceprivate RedisTemplate redisTemplate;@GetMapping("/code/sms")public String createSmsCode(@RequestParam String mobile) throws IOException {//获取验证码String smsCode = smsCodeGenerator.generate();//把验证码设置到redisredisTemplate.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);}/*** 正常这个是返回密码,但手机登录没有密码,不用管*/@Overridepublic Object getCredentials() {return null;}/*** 获取手机号或用户信息*/@Overridepublic Object getPrincipal() {return this.principal;}
}
2.3、拦截请求、获取手机号码
/*** 手机号码拦截器, 获取手机号码*/
public class PhoneNumAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public PhoneNumAuthenticationFilter() {super(new AntPathRequestMatcher("/phoneLogin", "POST"));}@Overridepublic 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 手机号码验证TokenPhoneNumAuthenticationToken authRequest = new PhoneNumAuthenticationToken(phoneNum);// 身份验证详细信息authRequest.setDetails(super.authenticationDetailsSource.buildDetails(request));return this.getAuthenticationManager().authenticate(authRequest);}
}
2.4、短信验证码验证过滤器
/*** 短信验证码验证过滤器*/
@Component
public class SmsCodeFilter extends OncePerRequestFilter {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;@Overrideprotected 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 {/*** 数据源*/@Resourceprivate DataSource dataSource;/*** 用户信息服务*/@Resourceprivate UserAuthentication userAuthentication;/*** 成功处理*/@Resourceprivate CustomizeAuthencationSuccessHandler customizeAuthencationSuccessHandler;/*** 失败处理*/@Resourceprivate CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;/*** 用户登出处理*/@Resourceprivate UserLogoutSuccessHandler userLogoutSuccessHandler;/*** 多用户登录处理*/@Resourceprivate MutilpleSessionHandler mutilpleSessionHandler;/*** 密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 手机号码登录验证处理*/@Resourceprivate DaoPhoneNumAuthenticationProvider daoPhoneNumAuthenticationProvider;/*** 信息验证码过滤器*/@Resourceprivate SmsCodeFilter smsCodeFilter;/*** 把AuthenticationManager公开*/@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 配置自定义验证查询/加密工具*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userAuthentication).passwordEncoder(passwordEncoder());}/*** 手机号码登录拦截器*/@Beanpublic PhoneNumAuthenticationFilter phoneNumAuthenticationFilter() throws Exception {// 手机号码拦截器, 获取手机号码PhoneNumAuthenticationFilter phoneNumAuthenticationFilter = new PhoneNumAuthenticationFilter();phoneNumAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());//使用手机号登录失败了如何处理phoneNumAuthenticationFilter.setAuthenticationFailureHandler(customizeAuthencationFailureHandler);// 使用手机号登录成功了如何处理phoneNumAuthenticationFilter.setAuthenticationSuccessHandler(customizeAuthencationSuccessHandler);return phoneNumAuthenticationFilter;}@Overrideprotected 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();}
}