⒈封裝短信驗證碼類
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 }