前言
在前面的2個章節(jié)中,一一哥 帶大家實現(xiàn)了在Spring Security中添加圖形驗證碼校驗功能,其實Spring Security的功能不僅僅是這些,還可以實現(xiàn)很多別的效果,比如實現(xiàn)自動登錄,注銷登錄等。
有的小伙伴會問,我們?yōu)槭裁匆獙崿F(xiàn)自動登錄啊?這個需求其實還是很常見的,因為對于用戶來說,他可能經(jīng)常需要進行登錄以及退出登錄,你想想,如果用戶每次登錄時都要輸入自己的用戶名和密碼,是不是很煩,用戶體驗是不是很不好?
所以為了提高項目的用戶體驗,我們可以在項目中添加自動登錄功能,當然也要給用戶提供退出登錄的功能。接下來就跟著 一一哥 來學習如何實現(xiàn)這些功能吧!
一. 自動登錄簡介
1. 為什么要自動登錄
我們在訪問網(wǎng)站或app時,一般都會要求我們注冊一個賬號,包含用戶名和密碼信息,其中密碼還會有長度及取值范圍的限制。很多時候,我們在不同的網(wǎng)站上注冊的賬號,可能密碼也不同,這就導致我們必須記住這些不同網(wǎng)站上的用戶信息。那么在下次登錄時,因為我們的密碼太多了,很有可能會記不起這些賬號密碼。所以在幾次嘗試登錄失敗之后,很多人都會選擇找回密碼,從而再次陷入如何設置密碼的循環(huán)里。
為了盡可能減少用戶重新登錄的頻率,提高用戶的使用體驗,我們可以提供自動登錄這樣一個會給用戶帶來便利,同時也會給用戶帶來風險的體驗性功能。
2. 自動登錄的實現(xiàn)方案
了解了自動登錄出現(xiàn)的背景及作用后,那么我們該怎么實現(xiàn)自動登錄呢?
首先我們知道,自動登錄是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當用戶下次訪問時,自動實現(xiàn)校驗并建立登錄狀態(tài)的一種機制。
所以基于上面的原理,Spring Security 就為我們提供了兩種比較好的實現(xiàn)自動登錄的方案:
- 基于散列加密算法機制:加密用戶必要的登錄信息,并生成令牌來實現(xiàn)自動登錄,利用TokenBasedRememberMeServices類來實現(xiàn)。
- 基于數(shù)據(jù)庫等持久化數(shù)據(jù)存儲機制:生成持久化令牌來實現(xiàn)自動登錄,利用PersistentTokenBasedRememberMeServices來實現(xiàn)。
我上面提到的2個實現(xiàn)類,其實都是AbstractRememberMeServices的子類,如下圖所示:
了解了這些核心API之后,我們就可以利用這兩個API來實現(xiàn)自動登錄了。
二. 基于散列加密方案實現(xiàn)自動登錄
我先帶各位利用第1種實現(xiàn)方案,即基于散列加密方案來實現(xiàn)自動登錄。
首先我們還是在之前的案例基礎之上進行開發(fā),具體的項目創(chuàng)建過程略過,請參考之前的章節(jié)內容。
1. 配置加密令牌的key
首先我們創(chuàng)建一個application.yml文件,在其中添加數(shù)據(jù)庫配置,以及一個用來加密令牌的key字符串,字符串的值隨便自定義就行。
spring: datasource: url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT username: root password: syc security: remember-me: key: yyg
2. 配置SecurityConfig類
跟之前的案例一樣,我還是要創(chuàng)建一個SecurityConfig類,在其中的configure(HttpSecurity http)方法中,通過JdbcTokenRepositoryImpl關聯(lián)我們的數(shù)據(jù)庫,并且通過rememberMe()方法開啟“記住我”功能,另外還要把我們前面在配置文件中的rememberKey配置進來,作為散列加密的key。
@EnableWebSecurity(debug = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.remember-me.key}") private String rememberKey; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { //利用JdbcTokenRepositoryImpl關聯(lián)數(shù)據(jù)源 JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); http.authorizeRequests() .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .hasRole("USER") .antMatchers("/app/**") .permitAll() .anyRequest() .authenticated() .and() .formLogin() .permitAll() .and() //開啟“記住我”功能 .rememberMe() .userDetailsService(userDetailsService) //配置散列加密用的key .key(rememberKey) .and() .csrf() .disable(); } @Bean public PasswordEncoder passwordEncoder() { //不對登錄密碼進行加密 return NoOpPasswordEncoder.getInstance(); } }
3. 添加測試接口
為了方便后續(xù)的測試,我隨便編寫一個測試用的web接口。
@RestController @RequestMapping("/user") public class UserController { @GetMapping("hello") public String hello() { return "hello, user"; } }
4. 啟動項目測試
然后我們把項目啟動起來進行測試,當然你別忘了編寫項目入口類,這里我就不粘貼相關代碼了。
我們訪問一下/user/hello接口,會先重定向到/login接口,這時候會發(fā)現(xiàn)在默認的登錄頁面上多了一個“記住我”功能。
此時如果我們打開 開發(fā)者調試工具,并且勾選“記住我”,然后發(fā)起請求,這時候我們會在控制臺看到remember-me的cookie信息,說明Spring Security已經(jīng)自動生成了remember-me這個cookie,且表單中的remember-me參數(shù)也處于了“on”狀態(tài)。
也就是說,我們利用簡單的幾行代碼,就實現(xiàn)了基于散列加密方案的自動登錄。
三. 散列加密方案實現(xiàn)原理
你可能會很好奇,散列加密方案到底是怎么實現(xiàn)自動登錄的呢?別急,接下來 壹哥就為你分析一下散列加密的實現(xiàn)原理。
1. cookie的加密原理分析
我在前面給各位說過,自動登錄其實就是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當用戶下次訪問時,自動實現(xiàn)校驗并建立登錄狀態(tài)的一種機制。所以在自動登錄后,肯定會生成代表用戶的cookie信息,但是為了安全,這個cookie肯定不會明文存儲,需要把這個cookie進行加密處理,當然也會解碼處理。所以接下來我就給各位分析一下這個cookie的加密和解碼過程。
首先 壹哥 給各位解釋一下所謂的散列加密算法,其實質就是把 username、expirationTime、password等字段,再加上自定義的key字段合并起來,在每個字段之間用 ":" 分隔,最后利用md5算法進行哈希運算,這樣就可以得到一個加密后的字符串。Spring Security把這個加密的字符串存儲到cookie中,作為用戶已登錄的標識信息。
然后 壹哥 帶你看看TokenBasedRememberMeServices源碼類中的makeTokenSignature()方法,你會看到散列加密算法的具體加密實現(xiàn)過程,源碼如下圖所示:
2. cookie的解碼原理分析
上面利用MD5進行了加密,用戶在下次登錄后,肯定需要進行信息的比對,以判斷用戶信息是否一致。Spring Security是先對cookie中的信息進行解碼,然后與之前記錄的登錄信息進行比對,以此判斷用戶是否已登錄。
Spring Security是在AbstractRememberMeServices類的decodeCookie()方法中,利用Base64對cookie進行解碼,如下圖所示:
對于以上2個源碼方法,我們可以簡化抽取出如下兩行代碼:
//對各字段進行散列加密 hashInfo=md5Hex(username +":"+expirationTime +":"password+":"+key) //利用base64進行解碼 rememberCookie=base64(username+":"+expirationrime+":"+hashInfo)
其中,expirationTime是指本次自動登錄的有效期,key是自己指定的一個散列鹽值,用于防止令牌被修改。利用以上兩個
分析完源碼之后,壹哥給各位簡單總結一下cookie的生成驗證原理:
- 首先利用上面的源碼生成cookie,并保存在瀏覽器中;
- 在瀏覽器關閉并重新打開之后,用戶再去訪問 /user/hello 接口時,此時就會攜帶remember-me這個cookie到服務端;
- 服務器端拿到cookie之后,利用Base64進行解碼,計算出用戶名和過期時間,再根據(jù)用戶名查詢到用戶密碼;
- 最后還要通過 MD5 散列函數(shù)計算出散列值,并將計算出的散列值和瀏覽器傳遞來的散列值進行對比,以此確認這個令牌是否有效。
3. 自動登錄的源碼分析
上面分析完cookie信息的加密和解碼之后,接下來我再結合源碼,從兩個方面來介紹自動登錄的實現(xiàn)過程,一個是 remember-me 令牌的生成的過程,另一個則是該令牌的解析過程。
3.1 令牌生成的源碼分析
我們要想知道源碼中是如何生成remember-me自動登錄令牌的,首先得知道Spring Security是如何進入到該令牌所在代碼的,這個代碼的執(zhí)行與我們前一章節(jié)所講的Spring Security的認證授權有關,請進入到前面查看。
AbstractAuthenticationProcessingFilter#doFilter -> AbstractAuthenticationProcessingFilter#successfulAuthentication -> AbstractRememberMeServices#loginSuccess -> TokenBasedRememberMeServices#onLoginSuccess
這個令牌生成的核心處理方法定義在:TokenBasedRememberMeServices#onLoginSuccess。
@Override public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { //從認證對象中獲取用戶名 String username = retrieveUserName(successfulAuthentication); //從認證對象中獲取密碼 String password = retrievePassword(successfulAuthentication); ...... if (!StringUtils.hasLength(password)) { //根據(jù)用戶名查詢出對應的用戶 UserDetails user = getUserDetailsService().loadUserByUsername(username); //獲取到用戶身上的密碼 password = user.getPassword(); } //獲取登錄過期時間,默認是2周 int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication); long expiryTime = System.currentTimeMillis(); expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime); //生成remember-me簽名信息 String signatureValue = makeTokenSignature(expiryTime, username, password); //保存cookie setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request, response); } protected String makeTokenSignature(long tokenExpiryTime, String username, String password) { String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey(); MessageDigest digest; digest = MessageDigest.getInstance("MD5"); return new String(Hex.encode(digest.digest(data.getBytes()))); }
以上源碼的實現(xiàn)邏輯很好理解:
- 首先從登錄成功的 Authentication 對象中提取出用戶名/密碼;
- 由于登錄成功之后,密碼可能被擦除了,所以如果一開始沒有拿到密碼,就再從 UserDetailsService 中重新加載用戶并重新獲取密碼;
- 接下來獲取令牌的有效期,令牌有效期默認是兩周;
- 再接下來調用 makeTokenSignature()方法 去計算散列值,實際上就是根據(jù) username、令牌有效期以及 password、key 一起計算一個散列值。如果我們沒有自己去設置這個 key,默認是在 RememberMeConfigurer#getKey 方法中進行設置的,它的值是一個 UUID 字符串。但是如果服務端重啟,這個默認的 key 是會變的,這樣就導致之前派發(fā)出去的所有 remember-me 自動登錄令牌失效,所以我們可以指定這個 key。
- 最后,將用戶名、令牌有效期以及計算得到的散列值放入 Cookie 中并隨response返回。
3.2 令牌解析的源碼分析
對于RememberMe 這個功能,Spring Security提供了 RememberMeAuthenticationFilter 這個過濾器類來處理相關功能,我們來看下 RememberMeAuthenticationFilter 的 doFilter() 方法:
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) { //處理自動登錄的業(yè)務邏輯 Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { // Attempt authenticaton via AuthenticationManager try { rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); // Store to SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); onSuccessfulAuthentication(request, response, rememberMeAuth); if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } // Fire event if (this.eventPublisher != null) { eventPublisher .publishEvent(new InteractiveAuthenticationSuccessEvent( SecurityContextHolder.getContext() .getAuthentication(), this.getClass())); } if (successHandler != null) { successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException authenticationException) { if (logger.isDebugEnabled()) { logger.debug( "SecurityContextHolder not populated with remember-me token, as " + "AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", authenticationException); } rememberMeServices.loginFail(request, response); onUnsuccessfulAuthentication(request, response, authenticationException); } } chain.doFilter(request, response); } else { if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } chain.doFilter(request, response); } }
這個方法最關鍵的地方在于,如果從 SecurityContextHolder 中無法獲取到當前登錄用戶實例,那么就調用 rememberMeServices.autoLogin()邏輯進行登錄,我們來看下這個方法:
@Override public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; } logger.debug("Remember-me cookie detected"); if (rememberMeCookie.length() == 0) { logger.debug("Cookie was empty"); cancelCookie(request, response); return null; } UserDetails user = null; try { String[] cookieTokens = decodeCookie(rememberMeCookie); user = processAutoLoginCookie(cookieTokens, request, response); userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted"); return createSuccessfulAuthentication(request, user); } ...... cancelCookie(request, response); return null; }
Spring Security就是在這里提取出 cookie 信息,并對 cookie 信息進行解碼。解碼之后,再調用 processAutoLoginCookie()方法去做校驗。processAutoLoginCookie() 方法的代碼我就不貼了,核心流程就是首先獲取用戶名和過期時間,再根據(jù)用戶名查詢到用戶密碼,然后通過 MD5 散列函數(shù)計算出散列值。最后再將拿到的散列值和瀏覽器傳遞來的散列值進行對比,就能確認這個令牌是否有效,進而確認登錄是否有效。
至此,壹哥 就結合著源碼和底層原理,給大家講解了基于散列加密方案實現(xiàn)了自動登錄,并且在本案例中給大家介紹了散列加密算法,你掌握的怎么樣呢?
到此這篇關于SpringSecurity基于散列加密方案實現(xiàn)自動登錄的文章就介紹到這了,更多相關SpringSecurity 自動登錄內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/syc000666/article/details/120481624