您好,登錄后才能下訂單哦!
如何進行個性化認證以及RememberMe實現,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權限、認證相關的內容、原理及設計學習并整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。
項目環境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x
?? 在 授權過程 和 認證過程 中我們都是使用的 Security 默認的一個登錄頁面(/login),那么如果我們想自定義一個登錄頁面該如何實現呢?其實很簡單,我們新建 FormAuthenticationConfig 配置類,然后在configure(HttpSecurity http) 方法中實現以下設置:
http.formLogin() //可以設置自定義的登錄頁面 或者 (登錄)接口 // 注意1: 一般來說設置成(登錄)接口后,該接口會配置成無權限即可訪問,所以會走匿名filter, 也就意味著不會走認證過程了,所以我們一般不直接設置成接口地址 // 注意2: 這里配置的 地址一定要配置成無權限訪問,否則將出現 一直重定向問題(因為無權限后又會重定向到這里配置的登錄頁url) .loginPage(securityProperties.getLogin().getLoginPage()) //.loginPage("/loginRequire") // 指定驗證憑據的URL(默認為 /login) , // 注意1:這里修改后的 url 會意味著 UsernamePasswordAuthenticationFilter 將 驗證此處的 url // 注意2: 與 loginPage設置的接口地址是有 區別, 一但 loginPage 設置了的是訪問接口url,那么此處配置將無任何意義 // 注意3: 這里設置的 Url 是有默認無權限訪問的 .loginProcessingUrl(securityProperties.getLogin().getLoginUrl()) //分別設置成功和失敗的處理器 .successHandler(customAuthenticationSuccessHandler) .failureHandler(customAuthenticationFailureHandler);
??最后在 SpringSecurityConfig 的 configure(HttpSecurity http) 方法中 調用 formAuthenticationConfig.configure(http) 即可;
?? 正如看到的一樣,我們通過 loginPage()設置 登錄頁面 或 接口, 通過 loginProcessingUrl() 設置 UsernamePasswordAuthenticationFilter 要匹配的 接口地址(一定是Post)(看過授權過程的同學應該都知道其默認的是/login)。 這里有以下幾點值得注意:
> - loginPage() 這里配置的 地址(不管是接口url還是登錄頁面)一定要配置成無權限訪問,否則將出現 一直重定向問題(因為無權限后又會重定向到這里配置的登錄頁url > - loginPage() 一般來說不直接設置成(登錄)接口,因為設置了接口會配置成無權限即可訪問(當然設置成登錄頁面也需要配置無權限訪問),所以會走匿名filter, 也就意味著不會走認證過程了,所以我們一般不直接設置成接口地址 > - loginProcessingUrl() 這里修改后的 url 會意味著 UsernamePasswordAuthenticationFilter 將 驗證此處的 url > - loginProcessingUrl() 這里設置的 Url 是有默認無權限訪問的,與 loginPage設置的接口地址是有 區別, 一但 loginPage 設置了的是接口url,那么此處配置將無任何意義 > - successHandler() 和 failureHandler 分別 設置認證成功處理器 和 認證失敗處理器 (如果對這2個處理器沒印象的話,建議回顧下授權過程)
?? 在授權過程中,我們增簡單提及到過這2個處理器,在Security中默認的處理器分別是SavedRequestAwareAuthenticationSuccessHandler 和 SimpleUrlAuthenticationFailureHandler ,這次我們自定義這2個處理器,分別為 CustomAuthenticationSuccessHandler ( extends SavedRequestAwareAuthenticationSuccessHandler ) 重寫 onAuthenticationSuccess() 方法 :
@Component("customAuthenticationSuccessHandler") @Slf4j public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Autowired private SecurityProperties securityProperties; private RequestCache requestCache = new HttpSessionRequestCache(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登錄成功"); // 如果設置了loginSuccessUrl,總是跳到設置的地址上 // 如果沒設置,則嘗試跳轉到登錄之前訪問的地址上,如果登錄前訪問地址為空,則跳到網站根路徑上 if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) { requestCache.removeRequest(request, response); setAlwaysUseDefaultTargetUrl(true); setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl()); } super.onAuthenticationSuccess(request, response, authentication); } }
和 CustomAuthenticationFailureHandler( extends SimpleUrlAuthenticationFailureHandler) 重寫 onAuthenticationFailure() 方法 :
@Component("customAuthenticationFailureHandler") @Slf4j public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Autowired private SecurityProperties securityProperties; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { logger.info("登錄失敗"); if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage())); } else { // 跳轉設置的登陸失敗頁面 redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl()); } } }
這里就不再描述,直接貼代碼:
<meta charset="UTF-8"> <title>登錄</title> <h3>登錄頁面</h3> <form action="/loginUp" method="post"> <table> <tbody><tr> <td>用戶名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><input name="remember-me" type="checkbox" value="true">記住我</td> </tr> <tr> <td colspan="2"> <button type="submit">登錄</button> </td> </tr> </tbody></table> </form>
??注意這里請求的地址是 loginProcessingUrl() 配置的地址
??這里就不在貼結果圖了,只要我們明白結果流程就行是這樣的就可以: localhost:8080 ——> 點擊 測試驗證Security 權限控制 ————> 跳轉到 我們自定義的 /loginUp.html 登錄頁,登錄后 ————> 有配置loginSuccessUrl,則跳轉到 loginSuccess.html;反之則直接跳轉到 /get_user/test 接口返回結果。 整個流程就全面涉及到了我們自定義的登錄頁面、自定義的登錄成功/失敗處理器。
首先我們一股腦的將rememberMe配置加上,然后看下現象:
1、 創建 persistent_logins 表,用于存儲token和用戶的關聯信息:
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
2 、 添加rememberMe配置 信息
@Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 如果token表不存在,使用下面語句可以初始化 persistent_logins(ddl在db目錄下) 表;若存在,請注釋掉這條語句,否則會報錯。 //tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception { formAuthenticationConfig.configure(http); http. .... .and() // 開啟 記住我功能,意味著 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息 .rememberMe() // 設置 tokenRepository ,這里默認使用 jdbcTokenRepositoryImpl,意味著我們將從數據庫中讀取token所代表的用戶信息 .tokenRepository(persistentTokenRepository()) // 設置 userDetailsService , 和 認證過程的一樣,RememberMe 有專門的 RememberMeAuthenticationProvider ,也就意味著需要 使用UserDetailsService 加載 UserDetails 信息 .userDetailsService(userDetailsService) // 設置 rememberMe 的有效時間,這里通過 配置來設置 .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds()) .and() .csrf().disable(); // 關閉csrf 跨站(域)攻擊防控 }
這里解釋下配置:
rememberMe() 開啟 記住我功能,意味著 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息
tokenRepository() 配置 token的獲取策略,這里配置成從數據庫中讀取
userDetailsService() 配置 UserDetaisService (如果不熟悉該對象,建議回顧認證過程)
tokenValiditySeconds() 設置 rememberMe 的有效時間,這里通過 配置來設置
另一個重要的配置在登錄頁面,這里的 必須是 name="remember-me" ,rememberMe就是通過驗證這個配置來開啟remermberMe功能的。
<input name="remember-me" type="checkbox" value="true">記住我
??實操結果應該為:進入登陸頁面 ——> 勾選記住我后登錄 ——> 成功后,查看persistent_logins 表發現有一條數據——> 重啟項目 ——> 重新訪問需要登錄才能訪問的頁面,發現無需登錄即可訪問——> 刪除 persistent_logins 表數據,等待token設置的有效時間過期,然后重新刷新頁面發現跳轉到登陸頁面。
?? 首先我們查看UsernamePasswordAuthenticationFiler(AbstractAuthenticationProcessingFilter) 的 successfulAuthentication() 方法內部源碼:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // 1 設置 認證成功的Authentication對象到SecurityContext中 SecurityContextHolder.getContext().setAuthentication(authResult); // 2 調用 RememberMe 相關service處理 rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //3 調用成功處理器 successHandler.onAuthenticationSuccess(request, response, authResult); }
其中我們發現我們本次重點關注的一行代碼: rememberMeServices.loginSuccess(request, response, authResult) , 查看這個方法內部源碼:
@Override public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { // 這里就在判斷用戶是否勾選了記住我 if (!rememberMeRequested(request, parameter)) { logger.debug("Remember-me login not requested."); return; } onLoginSuccess(request, response, successfulAuthentication); }
通過 rememberMeRequested() 判斷是否勾選了記住我。 onLoginSuccess() 方法 最終會調用到 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法,貼出其方法源碼如下:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { // 1 獲取賬戶名 String username = successfulAuthentication.getName(); // 2 創建 PersistentRememberMeToken 對象 PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { // 3 通過 tokenRepository 存儲 persistentRememberMeToken 信息 tokenRepository.createNewToken(persistentToken); // 4 將 persistentRememberMeToken 信息添加到Cookie中 addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }
分析下源碼步驟:
獲取 賬戶信息 username
傳入 username 創建 PersistentRememberMeToken 對象
通過 tokenRepository 存儲 persistentRememberMeToken信息
將 persistentRememberMeToken 信息添加到Cookie中
??這里的 tokenRepository 就是我們配置 rememberMe功能所設置的。經過上面的解析我們看到了rememberServices 將 創建一個 token 信息,并存儲到數據庫(因為我們配置的是數據庫存儲方式 JdbcTokenRepositoryImpl )中,并將token信息添加到Cookie中了。到這里,我們看到了RememberMe實現前的一些業務處理,那么后面如何實現RememberMe,我想大家心里大概都有個底了。這里直接拋出之前授權過程中我們沒有提及到的 filter 類 RememberMeAuthenticationFilter,它是介于 UsernamePasswordAuthenticationFilter 和 AnonymousAuthenticationFilter 之間的一個filter,它主要負責的就是前面的filter都沒有認證成功后從Cookie中獲取token信息然后再通過tokenRepository 獲取 登錄用戶名,然后UserDetailsServcie 加載 UserDetails 信息 ,最后創建 Authticaton(RememberMeAuthenticationToken) 信息再調用 AuthenticationManager.authenticate() 進行認證過程。
RememberMeAuthenticationFilter
??我們來看下 RememberMeAuthenticationFilter 的dofiler方法源碼:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (SecurityContextHolder.getContext().getAuthentication() == null) { // 1 調用 rememberMeServices.autoLogin() 獲取Authtication 信息 Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { // Attempt authenticaton via AuthenticationManager try { // 2 調用 authenticationManager.authenticate() 認證 rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); ...... } } catch (AuthenticationException authenticationException) { ..... } chain.doFilter(request, response); }
我們主要關注 rememberMeServices.autoLogin(request,response) 方法實現,查看器源碼:
@Override public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { // 1 從Cookie 中獲取 token 信息 String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; } if (rememberMeCookie.length() == 0) { cancelCookie(request, response); return null; } UserDetails user = null; try { // 2 解析 token信息 String[] cookieTokens = decodeCookie(rememberMeCookie); // 3 通過 token 信息 生成 Uerdetails 信息 user = processAutoLoginCookie(cookieTokens, request, response); userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted"); // 4 通過 UserDetails 信息創建 Authentication return createSuccessfulAuthentication(request, user); } ..... }
內部實現步驟:
從Cookie中獲取 token 信息并解析
通過 解析的token 生成 UserDetails (processAutoLoginCookie() 方法實現 )
通過 UserDetails 生成 Authentication ( createSuccessfulAuthentication() 創建 RememberMeAuthenticationToken )
其中最關鍵的一部是 processAutoLoginCookie() 方法是如何生成UserDetails 對象的,我們查看這個方法源碼實現:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { final String presentedSeries = cookieTokens[0]; final String presentedToken = cookieTokens[1]; // 1 通過 tokenRepository 加載數據庫token信息 PersistentRememberMeToken token = tokenRepository .getTokenForSeries(presentedSeries); PersistentRememberMeToken newToken = new PersistentRememberMeToken( token.getUsername(), token.getSeries(), generateTokenData(), new Date()); // 2 判斷 用戶傳入token和數據中的token是否一致,不一致可能存在安全問題 if (!presentedToken.equals(token.getTokenValue())) { tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException( messages.getMessage( "PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); } try { // 3 更新 token 并添加到Cookie中 tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); addCookie(newToken, request, response); } catch (Exception e) { throw new RememberMeAuthenticationException( "Autologin failed due to data access problem"); } // 4 通過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回 return getUserDetailsService().loadUserByUsername(token.getUsername()); }
我們看下其內部步驟:
通過 tokenRepository 加載數據庫token信息
判斷 用戶傳入token和數據中的token是否一致,不一致可能存在安全問題
更新 token 并添加到Cookie中
通過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回
?? 看到這里相信大家以下就明白了,當初為啥在啟用rememberMe功能時要配置 tokenRepository 和 UserDetailsService了。
這里我就不再演示整個實現的流程了,老規矩,上流程圖:
看完上述內容,你們掌握如何進行個性化認證以及RememberMe實現的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。