SpringSecurity实现短信登录功能
⒈封装短信验证码类
1 package cn.coreqi.security.validate; 2 3 import java.time.LocalDateTime; 4 5 public class ValidateCode { 6 private String code; 7 private LocalDateTime expireTime; //过期时间 8 9 public ValidateCode(String code, Integer expireIn) { 10 this.code = code; 11 this.expireTime = LocalDateTime.now().plusSeconds(expireIn); 12 } 13 14 public ValidateCode(String code, LocalDateTime expireTime) { 15 this.code = code; 16 this.expireTime = expireTime; 17 } 18 19 public boolean isExpried(){ 20 return LocalDateTime.now().isAfter(expireTime); 21 } 22 public String getCode() { 23 return code; 24 } 25 26 public void setCode(String code) { 27 this.code = code; 28 } 29 30 public LocalDateTime getExpireTime() { 31 return expireTime; 32 } 33 34 public void setExpireTime(LocalDateTime expireTime) { 35 this.expireTime = expireTime; 36 } 37 38 }
⒉封装短信验证码接口及实现类
1 package cn.coreqi.security.validate; 2 3 public interface SmsCodeSender { 4 void send(String mobile,String code); 5 }
1 package cn.coreqi.security.validate; 2 3 public class DefaultSmsCodeSender implements SmsCodeSender { 4 @Override 5 public void send(String mobile, String code) { 6 System.out.println("向手机"+mobile+"发送短信验证码"+code+""); 7 } 8 }
⒊封装验证码控制器
1 package cn.coreqi.security.controller; 2 3 import cn.coreqi.security.validate.DefaultSmsCodeSender; 4 import cn.coreqi.security.validate.SmsCodeSender; 5 import cn.coreqi.security.validate.ValidateCode; 6 import org.apache.commons.lang3.RandomStringUtils; 7 import org.springframework.social.connect.web.HttpSessionSessionStrategy; 8 import org.springframework.social.connect.web.SessionStrategy; 9 import org.springframework.web.bind.ServletRequestBindingException; 10 import org.springframework.web.bind.ServletRequestUtils; 11 import org.springframework.web.bind.annotation.GetMapping; 12 import org.springframework.web.bind.annotation.RestController; 13 import org.springframework.web.context.request.ServletWebRequest; 14 15 import javax.servlet.http.HttpServletRequest; 16 import javax.servlet.http.HttpServletResponse; 17 18 @RestController 19 public class ValidateSmsController { 20 21 public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; 22 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); 23 24 @GetMapping("/code/sms") 25 public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException { 26 ValidateCode smsCode = new ValidateCode(RandomStringUtils.randomNumeric(4),60); 27 sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode); 28 String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile"); 29 SmsCodeSender smsCodeSender = new DefaultSmsCodeSender(); 30 smsCodeSender.send(mobile,smsCode.getCode()); 31 } 32 }
⒋放行验证码的Rest地址
⒌修改登录表单
1 <h3>短信登陆</h3> 2 <form action="/authentication/mobile" method="post"> 3 <table> 4 <tr> 5 <td>手机号码:</td> 6 <td><input type="text" name="mobile" value="13800138000"></td> 7 </tr> 8 <tr> 9 <td>短信验证码:</td> 10 <td> 11 <input type="text" name="smsCode"> 12 <a href="/code/sms?mobile=13800138000"/> 13 </td> 14 </tr> 15 <tr> 16 <td colspan="2"><button type="submit">登录</button></td> 17 </tr> 18 </table> 19 </form>
⒍封装安全验证流程相关
1.验证码验证Filter
1 package cn.coreqi.security.Filter; 2 3 import cn.coreqi.security.controller.ValidateSmsController; 4 import cn.coreqi.security.validate.ValidateCode; 5 import cn.coreqi.security.validate.ValidateCodeException; 6 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 7 import org.springframework.social.connect.web.HttpSessionSessionStrategy; 8 import org.springframework.social.connect.web.SessionStrategy; 9 import org.springframework.util.StringUtils; 10 import org.springframework.web.bind.ServletRequestBindingException; 11 import org.springframework.web.bind.ServletRequestUtils; 12 import org.springframework.web.context.request.ServletWebRequest; 13 import org.springframework.web.filter.OncePerRequestFilter; 14 15 import javax.servlet.FilterChain; 16 import javax.servlet.ServletException; 17 import javax.servlet.http.HttpServletRequest; 18 import javax.servlet.http.HttpServletResponse; 19 import java.io.IOException; 20 21 /** 22 * 短信验证码过滤器 23 */ 24 public class SmsCodeFilter extends OncePerRequestFilter { 25 private AuthenticationFailureHandler authenticationFailureHandler; 26 27 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); 28 29 public AuthenticationFailureHandler getAuthenticationFailureHandler() { 30 return authenticationFailureHandler; 31 } 32 33 public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { 34 this.authenticationFailureHandler = authenticationFailureHandler; 35 } 36 37 public SessionStrategy getSessionStrategy() { 38 return sessionStrategy; 39 } 40 41 public void setSessionStrategy(SessionStrategy sessionStrategy) { 42 this.sessionStrategy = sessionStrategy; 43 } 44 45 @Override 46 protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 47 if (httpServletRequest.equals("/authentication/mobile") && httpServletRequest.getMethod().equals("post")) { 48 try { 49 validate(new ServletWebRequest(httpServletRequest)); 50 51 }catch (ValidateCodeException e){ 52 authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e); 53 return; 54 } 55 } 56 filterChain.doFilter(httpServletRequest,httpServletResponse); //如果不是登录请求,直接调用后面的过滤器链 57 } 58 59 private void validate(ServletWebRequest request) throws ServletRequestBindingException { 60 ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateSmsController.SESSION_KEY); 61 String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"smsCode"); 62 if(!StringUtils.hasText(codeInRequest)){ 63 throw new ValidateCodeException("验证码的值不能为空!"); 64 } 65 if(codeInSession == null){ 66 throw new ValidateCodeException("验证码不存在!"); 67 } 68 if(codeInSession.isExpried()){ 69 sessionStrategy.removeAttribute(request,ValidateSmsController.SESSION_KEY); 70 throw new ValidateCodeException("验证码已过期!"); 71 } 72 if(!codeInSession.getCode().equals(codeInRequest)){ 73 throw new ValidateCodeException("验证码不正确!"); 74 } 75 sessionStrategy.removeAttribute(request,ValidateSmsController.SESSION_KEY); 76 } 77 }
2.封装短信登陆Token类
1 package cn.coreqi.security.Token; 2 3 import org.springframework.security.authentication.AbstractAuthenticationToken; 4 import org.springframework.security.core.GrantedAuthority; 5 import org.springframework.security.core.SpringSecurityCoreVersion; 6 7 import java.util.Collection; 8 9 public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { 10 private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; 11 private final Object principal; //存放认证信息,认证之前存放手机号,认证之后存放登录的用户 12 13 public SmsCodeAuthenticationToken(String mobile) { 14 super((Collection)null); 15 this.principal = mobile; 16 this.setAuthenticated(false); 17 } 18 19 public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { 20 super(authorities); 21 this.principal = principal; 22 super.setAuthenticated(true); 23 } 24 25 public Object getCredentials() { 26 return null; 27 } 28 29 public Object getPrincipal() { 30 return this.principal; 31 } 32 33 public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { 34 if (isAuthenticated) { 35 throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); 36 } else { 37 super.setAuthenticated(false); 38 } 39 } 40 41 public void eraseCredentials() { 42 super.eraseCredentials(); 43 } 44 }
3.短信登陆请求Filter
1 package cn.coreqi.security.Filter; 2 3 import cn.coreqi.security.Token.SmsCodeAuthenticationToken; 4 import org.springframework.security.authentication.AuthenticationServiceException; 5 import org.springframework.security.core.Authentication; 6 import org.springframework.security.core.AuthenticationException; 7 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 8 import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 9 import org.springframework.util.Assert; 10 11 import javax.servlet.http.HttpServletRequest; 12 import javax.servlet.http.HttpServletResponse; 13 14 public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 15 public static final String COREQI_FORM_MOBILE_KEY = "mobile"; 16 private String mobileParameter = COREQI_FORM_MOBILE_KEY; //请求中携带手机号的参数名称 17 private boolean postOnly = true; //指定当前过滤器是否只处理POST请求 18 19 public SmsCodeAuthenticationFilter() { 20 super(new AntPathRequestMatcher("/authentication/mobile", "POST")); //指定当前过滤器处理的请求 21 } 22 23 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 24 if (this.postOnly && !request.getMethod().equals("POST")) { 25 throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); 26 } else { 27 String mobile = this.obtainMobile(request); 28 if (mobile == null) { 29 mobile = ""; 30 } 31 mobile = mobile.trim(); 32 SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); 33 this.setDetails(request, authRequest); 34 return this.getAuthenticationManager().authenticate(authRequest); 35 } 36 } 37 38 /** 39 * 获取手机号码 40 * @param request 41 * @return 42 */ 43 protected String obtainMobile(HttpServletRequest request) { 44 return request.getParameter(this.mobileParameter); 45 } 46 47 /** 48 * 把请求的详情,例如请求ip、SessionId等设置到验证请求中去 49 * @param request 50 * @param authRequest 51 */ 52 protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { 53 authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); 54 } 55 56 public void setMobileParameter(String mobileParameter) { 57 Assert.hasText(mobileParameter, "Username parameter must not be empty or null"); 58 this.mobileParameter = mobileParameter; 59 } 60 61 62 public void setPostOnly(boolean postOnly) { 63 this.postOnly = postOnly; 64 } 65 66 public final String getMobileParameter() { 67 return this.mobileParameter; 68 } 69 70 }
4.短信身份认证类
1 package cn.coreqi.security.Provider; 2 3 import cn.coreqi.security.Token.SmsCodeAuthenticationToken; 4 import org.springframework.security.authentication.AuthenticationProvider; 5 import org.springframework.security.authentication.InternalAuthenticationServiceException; 6 import org.springframework.security.core.Authentication; 7 import org.springframework.security.core.AuthenticationException; 8 import org.springframework.security.core.userdetails.UserDetails; 9 import org.springframework.security.core.userdetails.UserDetailsService; 10 11 public class SmsCodeAuthenticationProvider implements AuthenticationProvider { 12 13 private UserDetailsService userDetailsService; 14 15 public UserDetailsService getUserDetailsService() { 16 return userDetailsService; 17 } 18 19 public void setUserDetailsService(UserDetailsService userDetailsService) { 20 this.userDetailsService = userDetailsService; 21 } 22 23 /** 24 * 进行身份认证的逻辑 25 * @param authentication 就是我们传入的Token 26 * @return 27 * @throws AuthenticationException 28 */ 29 @Override 30 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 31 32 //利用UserDetailsService获取用户信息,拿到用户信息后重新组装一个已认证的Authentication 33 34 SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication; 35 UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); //根据手机号码拿到用户信息 36 if(user == null){ 37 throw new InternalAuthenticationServiceException("无法获取用户信息"); 38 } 39 SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities()); 40 authenticationResult.setDetails(authenticationToken.getDetails()); 41 return authenticationResult; 42 } 43 44 /** 45 * AuthenticationManager挑选一个AuthenticationProvider来处理传入进来的Token就是根据supports方法来判断的 46 * @param aClass 47 * @return 48 */ 49 @Override 50 public boolean supports(Class<?> aClass) { 51 return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass); //判断出入进来的是不是SmsCodeAuthenticationToken类型 52 } 53 }
⒎配置
1 package cn.coreqi.security.config; 2 3 import cn.coreqi.security.Filter.SmsCodeAuthenticationFilter; 4 import cn.coreqi.security.Provider.SmsCodeAuthenticationProvider; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.security.authentication.AuthenticationManager; 7 import org.springframework.security.config.annotation.SecurityConfigurerAdapter; 8 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 import org.springframework.security.core.userdetails.UserDetailsService; 10 import org.springframework.security.web.DefaultSecurityFilterChain; 11 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 12 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 13 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 14 import org.springframework.stereotype.Component; 15 16 @Component 17 public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { 18 19 @Autowired 20 private AuthenticationSuccessHandler coreqiAuthenticationSuccessHandler; 21 22 @Autowired 23 private AuthenticationFailureHandler coreqiAuthenticationFailureHandler; 24 25 @Autowired 26 private UserDetailsService userDetailsService; 27 28 @Override 29 public void configure(HttpSecurity http) throws Exception { 30 SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); 31 smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); 32 smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(coreqiAuthenticationSuccessHandler); 33 smsCodeAuthenticationFilter.setAuthenticationFailureHandler(coreqiAuthenticationFailureHandler); 34 35 SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); 36 smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); 37 38 http.authenticationProvider(smsCodeAuthenticationProvider) 39 .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 40 } 41 }
1 package cn.coreqi.security.config; 2 3 import cn.coreqi.security.Filter.SmsCodeFilter; 4 import cn.coreqi.security.Filter.ValidateCodeFilter; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Configuration; 8 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 import org.springframework.security.crypto.password.NoOpPasswordEncoder; 11 import org.springframework.security.crypto.password.PasswordEncoder; 12 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 13 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 14 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 15 16 @Configuration 17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 18 19 @Autowired 20 private AuthenticationSuccessHandler coreqiAuthenticationSuccessHandler; 21 22 @Autowired 23 private AuthenticationFailureHandler coreqiAuthenticationFailureHandler; 24 25 @Autowired 26 private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; 27 28 @Bean 29 public PasswordEncoder passwordEncoder(){ 30 return NoOpPasswordEncoder.getInstance(); 31 } 32 33 34 @Override 35 protected void configure(HttpSecurity http) throws Exception { 36 ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); 37 validateCodeFilter.setAuthenticationFailureHandler(coreqiAuthenticationFailureHandler); 38 39 SmsCodeFilter smsCodeFilter = new SmsCodeFilter(); 40 41 42 //http.httpBasic() //httpBasic登录 BasicAuthenticationFilter 43 http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) //加载用户名密码过滤器的前面 44 .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) //加载用户名密码过滤器的前面 45 .formLogin() //表单登录 UsernamePasswordAuthenticationFilter 46 .loginPage("/coreqi-signIn.html") //指定登录页面 47 //.loginPage("/authentication/require") 48 .loginProcessingUrl("/authentication/form") //指定表单提交的地址用于替换UsernamePasswordAuthenticationFilter默认的提交地址 49 .successHandler(coreqiAuthenticationSuccessHandler) //登录成功以后要用我们自定义的登录成功处理器,不用Spring默认的。 50 .failureHandler(coreqiAuthenticationFailureHandler) //自己体会把 51 .and() 52 .authorizeRequests() //对授权请求进行配置 53 .antMatchers("/coreqi-signIn.html","/code/image").permitAll() //指定登录页面不需要身份认证 54 .anyRequest().authenticated() //任何请求都需要身份认证 55 .and().csrf().disable() //禁用CSRF 56 .apply(smsCodeAuthenticationSecurityConfig); 57 //FilterSecurityInterceptor 整个SpringSecurity过滤器链的最后一环 58 } 59 }