在之前的博客中我們實現了基於驗證碼的登陸方式。但是我們僅僅實現了通過圖片驗證碼的登錄,現在我們基於security 實現一下基於 短信驗證碼的登錄。
基於之前對於 security 的了解,我們知道了要實現一個驗證的基本流程,其中最重要的是 AbstractAuthenticationToken(令牌類)、AuthenticationProvider(認證類)、AbstractAuthenticationProcessingFilter (過濾器類)這三個類。
我們可以基於用戶密碼的登陸方式來實現,對於用戶名密碼的登錄涉及的三個類為:UsernamePasswordAuthenticationToken、UsernamePasswordAuthenticationFilter、而 AuthenticationProvider 在之前我們也是自定義的 ,修改一下就可以用。
1. 編寫 SmsCodeAuthenticationToken
/** * Create with IntelliJ IDEA * User: Wuzhenzhao * Date: 2019/3/13 * Time: 18:13 * Description: 參考 UsernamePasswordAuthenticationToken 寫 * 封裝登錄信息 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // ~ Instance fields // ================================================================================================
private final Object principal; //這個在 UsernamePasswordAuthenticationToken 是登錄密碼,不需要 干掉。
private Object credentials; // ~ Constructors // ===================================================================================================
/** * This constructor can be safely used by any code that wishes to create a * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()} * will return <code>false</code>. * */
public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * This constructor should only be used by <code>AuthenticationManager</code> or * <code>AuthenticationProvider</code> implementations that are satisfied with * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>) * authentication token. * * @param principal * @param authorities */
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); // must use super, as we override
} // ~ Methods // ========================================================================================================
public Object getCredentials() { return null; } public Object getPrincipal() { return this.principal; } public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
2. SmsCodeAuthenticationProvider ,用於短信登陸的驗證
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /* * (non-Javadoc) * * @see org.springframework.security.authentication.AuthenticationProvider# * authenticate(org.springframework.security.core.Authentication) */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("無法獲取用戶信息"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } /* * (non-Javadoc) * * @see org.springframework.security.authentication.AuthenticationProvider# * supports(java.lang.Class) */ @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
3. SmsCodeAuthenticationFilter 攔截短信登陸請求
/** * Create with IntelliJ IDEA * User: Wuzhenzhao * Date: 2019/3/13 * Time: 18:13 * Description: 參考 UsernamePasswordAuthenticationFilter */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // =====================================================================================
/** * 發送短信驗證碼 或 驗證短信驗證碼時,傳遞手機號的參數的名稱 */
private String mobileParameter = "mobile"; private boolean postOnly = true; // ~ Constructors // ===================================================================================================
public SmsCodeAuthenticationFilter() { /** * 默認的手機驗證碼登錄請求處理url * http://localhost:8889/code/sms?mobile=13888888888 */ super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } // ~ Methods // ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * 獲取手機號 */
protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } /** * Provided so that subclasses may configure what is put into the * authentication request's details property. * * @param request * that an authentication request is being created for * @param authRequest * the authentication request object that should have its details * set */
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Sets the parameter name which will be used to obtain the username from * the login request. * * @param usernameParameter * the parameter name. Defaults to "username". */
public void setMobileParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.mobileParameter = usernameParameter; } /** * Defines whether only HTTP POST requests will be allowed by this filter. * If set to true, and an authentication request is received which is not a * POST request, an exception will be raised immediately and authentication * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method * will be called as if handling a failed authentication. * <p> * Defaults to <tt>true</tt> but may be overridden by subclasses. */
public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
4.在 security 配置類中注入:
@Configuration @EnableWebSecurity// 開啟Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟Spring方法級安全
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationProvider myAuthenticationProvider; @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler; @Autowired private MyUserDetailService myUserDetailService; // 自定義認證配置
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(myAuthenticationProvider); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(myUserDetailService); auth.authenticationProvider(smsCodeAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { // .......省略部分代碼
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(this.authenticationManager()); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenctiationFailureHandler); http.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
對於提交登錄得時候驗證碼的校驗邏輯在 博客 :https://www.cnblogs.com/wuzhenzhao/p/13169023.html 中提到的 AbstractValidateCodeProcessor 進行驗證。這樣子先進行一個短信的發送,然后模擬一下登錄就可用實現了.