本文共 9774 字,大约阅读时间需要 32 分钟。
短信验证码登录的思路,需要通过验证码过滤器,过滤验证码是否正确。次过程和图形验证码校验逻辑完全一样。 之后,需要通过Spring Security 认真的一套逻辑,来去数据库查询用户信息,进行 认证信息 Authentication
的封装。
此处案例的Provider
认证校验类,只是从数据库查询信息,然后进行封装。实际开发中可能需求不同,按需求进行更改。
@Datapublic class ValidateCode { /** * 验证码 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; public ValidateCode(String code, int expireTime) { this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireTime); } public ValidateCode(String code, LocalDateTime expireTime) { this.code = code; this.expireTime = expireTime; } /** * 判断验证码是否过期 * @return */ public boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); }}
/** * @Author L.jg * @Title 抽象接口,让客户端可配置接口 * @Date 2021/5/24 11:42 */public interface ValidateCodeGenerate { ValidateCode generate(HttpServletRequest request);}
public class SmsCodeGenerate implements ValidateCodeGenerate { private SmsProperties smsProperties; public SmsCodeGenerate(SmsProperties smsProperties) { this.smsProperties = smsProperties; } @Override public ValidateCode generate(HttpServletRequest request) { String smsCode = RandomUtil.randomNumbers(smsProperties.getLength()); return new ValidateCode(smsCode, smsProperties.getExpireTime()); }}
@Configurationpublic class VlidateCodeConfig { @Autowired private SecurityProperties securityProperties; @Bean @ConditionalOnMissingBean(name = "smsCodeGenerate") public ValidateCodeGenerate smsCodeGenerate() { SmsCodeGenerate smsCodeGenerate = new SmsCodeGenerate(securityProperties.getValidateCode().getSms()); return smsCodeGenerate; }}
public interface SmsCodeSender { /** * 发送验证码 * * @param mobile 手机号 * @param code 验证码 */ void send(String mobile, String code);}
@Componentpublic class DefaultSmsCodeSender implements SmsCodeSender { @Override public void send(String mobile, String code) { System.out.println("手机号:" + mobile + "短信验证码:" + code); }}
@GetMapping("/sms/code") public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException { // 生成imageCode// ImageCode imageCode = createImageCode(request); ValidateCode smsCode = smsCodeGenerate.generate(request); // 将imageCode 保存在session中 sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, smsCode); String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile"); smsCodeSender.send(mobile, smsCode.getCode()); }
以Spring Security的form表单为例,是先通过UsernamePasswordAuthenticationFilter
来获取用户的登录信息。
UsernamePasswordAuthenticationToken
。 将封装的 Authentication信息交给 AuthenticationManager
管理。 根据Authentication
的类型,调用对应的Provider来处理认证逻辑。 这里参考 UsernamepasswordAuthenticationFilter
、 UsernamePasswordAuthenticationToken
、DaoAuthenticationProvider
实现自己的各个类。
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // 手机号 private final Object principal; public SmsCodeAuthenticationToken(Object principal) { super(null); this.principal = principal; setAuthenticated(false); } public SmsCodeAuthenticationToken(Collection authorities, Object principal) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean authenticated) { if (authenticated) { throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); }}
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String mobileParameter = "mobile"; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String mobile = this.obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(this.mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; }}
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getPrincipal().toString(); UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()); if (userDetails == null) { throw new InternalAuthenticationServiceException("无法通过手机号获取用户信息"); } SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken( userDetails.getAuthorities(),userDetails); smsCodeAuthenticationToken.setDetails(authentication.getDetails()); return smsCodeAuthenticationToken; } @Override public boolean supports(Class aClass) { return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; }}
将自定义的实现逻辑,配置到 Security 里
/** * @Author L.jg * @Title app和浏览器都需要使用,短信验证配置 * @Date 2021/5/24 17:35 */@Componentpublic class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter{ @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity builder) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); builder.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}
/** * 短信自定义登录config */ @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; /** * 为减少代码重复开发,多个应用使用同一个认证中心,每个应用需要自己指定登录页面。 * 这里需要将 loginpage 指向一个controlelr地址。 * 如果是html页面,就跳转到指定的登录页。 * 如果不是html页面,就提示401 没有认证信息。 * 如果有应用有指定的就使用自己的。如果没指定就使用本认证模块默认的登录页。 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler); validateCodeFilter.setSecurityProperties(securityProperties); validateCodeFilter.afterPropertiesSet(); // 配置过滤器的位置 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); http.apply(smsCodeAuthenticationSecurityConfig); http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler)// .successForwardUrl("/index") .defaultSuccessUrl("/index") .and() .authorizeRequests() .antMatchers("/sms/code","/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll() .anyRequest().authenticated() .and().csrf().disable(); }
这里缺少了,登录验证码校验功能,可以参考 图像验证码校验功能,只要在 添加一个过滤器,自定义校验即可。
转载地址:http://ydfoi.baihongyu.com/