Spring?Security自定義認證邏輯實例詳解
前言
這篇文章的內容基于對Spring Security 認證流程的理解,如果你不了解,可以讀一下這篇文章:Spring Security 認證流程 。
分析問題
以下是 Spring Security 內置的用戶名/密碼認證的流程圖,我們可以從這里入手:
根據(jù)上圖,我們可以照貓畫虎,自定義一個認證流程,比如手機短信碼認證。在圖中,我已經把流程中涉及到的主要環(huán)節(jié)標記了不同的顏色,其中藍色塊的部分,是用戶名/密碼認證對應的部分,綠色塊標記的部分,則是與具體認證方式無關的邏輯。
因此,我們可以按照藍色部分的類,開發(fā)我們自定義的邏輯,主要包括以下內容:
- 一個自定義的
Authentication
實現(xiàn)類,與UsernamePasswordAuthenticationToken
類似,用來保存認證信息。 - 一個自定義的過濾器,與
UsernamePasswordAuthenticationFilter
類似,針對特定的請求,封裝認證信息,調用認證邏輯。 - 一個
AuthenticationProvider
的實現(xiàn)類,提供認證邏輯,與DaoAuthenticationProvider
類似。
接下來,以手機驗證碼認證為例,一一完成。
自定義 Authentication
先給代碼,后面進行說明:
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; public SmsCodeAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
和 UsernamePasswordAuthenticationToken
一樣,繼承 AbstractAuthenticationToken
抽象類,需要實現(xiàn) getPrincipal
和 getCredentials
兩個方法。在用戶名/密碼認證中,principal 表示用戶名,credentials 表示密碼,在此,我們可以讓它們指代手機號和驗證碼,因此,我們增加這兩個屬性,然后實現(xiàn)方法。
除此之外,我們需要寫兩個構造方法,分別用來創(chuàng)建未認證的和已經成功認證的認證信息。
自定義 Filter
這一部分,可以參考 UsernamePasswordAuthenticationFilter
來寫。還是線上代碼:
public class SmsCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public static final String FORM_MOBILE_KEY = "mobile"; public static final String FORM_SMS_CODE_KEY = "smsCode"; private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST"); private boolean postOnly = true; protected SmsCodeAuthenticationProcessingFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); mobile = (mobile != null) ? mobile : ""; mobile = mobile.trim(); String smsCode = obtainSmsCode(request); smsCode = (smsCode != null) ? smsCode : ""; SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } private String obtainMobile(HttpServletRequest request) { return request.getParameter(FORM_MOBILE_KEY); } private String obtainSmsCode(HttpServletRequest request) { return request.getParameter(FORM_SMS_CODE_KEY); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } }
這部分比較簡單,關鍵點如下:
- 首先,默認的構造方法中制定了過濾器匹配那些請求,這里匹配的是
/sms/login
的 POST 請求。 - 在
attemptAuthentication
方法中,首先從request
中獲取表單輸入的手機號和驗證碼,創(chuàng)建未經認證的 Token 信息。 - 將 Token 信息交給
this.getAuthenticationManager().authenticate(authRequest)
方法。
自定義 Provider
這里是完成認證的主要邏輯,這里的代碼只有最基本的校驗邏輯,沒有寫比較嚴謹?shù)男r?,比如校驗用戶是否禁用等,因為這部分比較繁瑣但是簡單。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { public static final String SESSION_MOBILE_KEY = "mobile"; public static final String SESSION_SMS_CODE_KEY = "smsCode"; public static final String FORM_MOBILE_KEY = "mobile"; public static final String FORM_SMS_CODE_KEY = "smsCode"; private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { authenticationChecks(authentication); String mobile = authentication.getName(); UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); SmsCodeAuthenticationToken authResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); return authResult; } /** * 認證信息校驗 * @param authentication */ private void authenticationChecks(Authentication authentication) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 表單提交的手機號和驗證碼 String formMobile = request.getParameter(FORM_MOBILE_KEY); String formSmsCode = request.getParameter(FORM_SMS_CODE_KEY); // 會話中保存的手機號和驗證碼 String sessionMobile = (String) request.getSession().getAttribute(SESSION_MOBILE_KEY); String sessionSmsCode = (String) request.getSession().getAttribute(SESSION_SMS_CODE_KEY); if (StringUtils.isEmpty(sessionMobile) || StringUtils.isEmpty(sessionSmsCode)) { throw new BadCredentialsException("為發(fā)送手機驗證碼"); } if (!formMobile.equals(sessionMobile)) { throw new BadCredentialsException("手機號碼不一致"); } if (!formSmsCode.equals(sessionSmsCode)) { throw new BadCredentialsException("驗證碼不一致"); } } @Override public boolean supports(Class<?> authentication) { return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication)); } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
這段代碼的重點有以下幾個:
supports
方法用來判斷這個 Provider 支持的 AuthenticationToken 的類型,這里對應我們之前創(chuàng)建的SmsCodeAuthenticationToken
。- 在
authenticate
方法中,我們將 Token 中的手機號和驗證碼與 Session 中保存的手機號和驗證碼進行對比。(向 Session 中保存手機號和驗證碼的部分在下文中實現(xiàn))對比無誤后,從 UserDetailsService 中獲取對應的用戶,并依此創(chuàng)建通過認證的 Token,并返回,最終到達 Filter 中。
自定義認證成功/失敗后的 Handler
之前,我們通過分析源碼知道,F(xiàn)ilter 中的 doFilter
方法,其實是在它的父類
AbstractAuthenticationProcessingFilter
中的,attemptAuthentication
方法也是在 doFilter 中被調用的。
當我們進行完之前的自定義邏輯,無論是否認證成功,attemptAuthentication
方法會返回認證成功的結果或者拋出認證失敗的異常。doFilter
方法中會根據(jù)認證的結果(成功/失敗),調用不同的處理邏輯,這兩個處理邏輯,我們也可以進行自定義。
我直接在下面貼代碼:
public class SmsCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write(authentication.getName()); } }
public class SmsCodeAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write("認證失敗"); } }
以上是成功和失敗后的處理邏輯,需要分別實現(xiàn)對應的接口,并實現(xiàn)方法。注意,這里只是為了測試,寫了最簡單的邏輯,以便測試的時候能夠區(qū)分兩種情況。真實的項目中,要根據(jù)具體的業(yè)務執(zhí)行相應的邏輯,比如保存當前登錄用戶的信息等。
配置自定義認證的邏輯
為了使我們的自定義認證生效,需要將 Filter 和 Provider 添加到 Spring Security 的配置當中,我們可以把這一部分配置先單獨放到一個配置類中:
@Component @RequiredArgsConstructor public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) { SmsCodeAuthenticationProcessingFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationProcessingFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new SmsCodeAuthenticationSuccessHandler()); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsCodeAuthenticationFailureHandler()); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
其中,有以下需要注意的地方:
- 一定記得把 AuthenticationManager 提供給 Filter,回顧之前講到的認證邏輯,如果沒有這一步,在 Filter 中完成認證信息的封裝后,就沒辦法去找對應的 Provider。
- 要把成功/失敗后的處理邏輯的兩個類提供給 Filter,否則不會進入這兩個邏輯,而是會進入默認的處理邏輯。
- Provider 中用到了 UserDetailsService,也要記得提供。
- 最后,將兩者添加到 HttpSecurity 對象中。
接下來,需要在 Spring Security 的主配置中添加如下內容。
- 首先,注入
SmsCodeAuthenticationSecurityConfig
配置。 - 然后,在
configure(HttpSecurity http)
方法中,引入配置:http.apply`` ( ``smsCodeAuthenticationSecurityConfig`` ) ``;
。 - 最后,由于在認證前,需要請求和校驗驗證碼,因此,對
/sms/**
路徑進行放行。
測試
大功告成,我們測試一下,首先需要提供一個發(fā)送驗證碼的接口,由于是測試,我們直接將驗證碼返回。接口代碼如下:
@GetMapping("/getCode") public String getCode(@RequestParam("mobile") String mobile, HttpSession session) { String code = "123456"; session.setAttribute("mobile", mobile); session.setAttribute("smsCode", code); return code; }
為了能獲取到相應的用戶,如果你還沒有實現(xiàn)自己的 UserDetailsService,先寫一個簡單的邏輯,完成測試,其中的 loadUserByUsername
方法如下即可:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO: 臨時邏輯,之后對接用戶管理相關的服務 return new User(username, "123456", AuthorityUtils.createAuthorityList("admin")); }
OK,下面是測試結果:
總結
到此這篇關于Spring Security自定義認證邏輯的文章就介紹到這了,更多相關Spring Security自定義認證邏輯內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解eclipse下創(chuàng)建第一個spring boot項目
本文詳細介紹了創(chuàng)建第一個基于eclipse(eclipse-jee-neon-3-win32-x86_64.zip)+spring boot創(chuàng)建的項目。2017-04-04