自動登錄是我們在軟件開發時一個非常常見的功能,例如我們登錄 QQ 郵箱:
很多網站我們在登錄的時候都會看到類似的選項,畢竟總讓用戶輸入用戶名密碼是一件很麻煩的事。
自動登錄功能就是,用戶在登錄成功后,在某一段時間內,如果用戶關閉了瀏覽器并重新打開,或者服務器重啟了,都不需要用戶重新登錄了,用戶依然可以直接訪問接口數據
作為一個常見的功能,我們的 Spring Security 肯定也提供了相應的支持,本文我們就來看下 Spring Security 中如何實現這個功能。
一、加入 remember-me
為了配置方便,加入兩個依賴即可:
配置類中添加如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser( "yolo" ) .password( "123" ).roles( "admin" ); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .rememberMe() .and() .csrf().disable(); } } |
大家看到,這里只需要添加一個 .rememberMe()
即可,自動登錄功能就成功添加進來了。
接下來我們隨意添加一個測試接口:
1
2
3
4
5
6
7
|
@RestController public class HelloController { @GetMapping ( "/hello" ) public String hello(){ return "Hello Yolo !!!" ; } } |
這個時候大家發現,默認的登錄頁面多了一個選項,就是記住我。我們輸入用戶名密碼,并且勾選上記住我這個框,然后點擊登錄按鈕執行登錄操作。
可以看到,登錄數據中,除了 username 和 password 之外,還有一個 remember-me,之所以給大家看這個,是想告訴大家,如果你你需要自定義登錄頁面,RememberMe 這個選項的 key 該怎么寫。
登錄成功之后,就會自動跳轉到 hello 接口了。我們注意,系統訪問 hello 接口的時候,攜帶的 cookie:
大家注意到,這里多了一個 remember-me
,這就是這里實現的核心,關于這個 remember-me
我一會解釋,我們先來測試效果。
接下來,我們關閉瀏覽器,再重新打開瀏覽器。正常情況下,瀏覽器關閉再重新打開,如果需要再次訪問 hello 接口,就需要我們重新登錄了。但是此時,我們再去訪問 hello 接口,發現不用重新登錄了,直接就能訪問到,這就說明我們的 RememberMe 配置生效了(即下次自動登錄功能生效了)。
二、原理分析
按理說,瀏覽器關閉再重新打開,就要重新登錄,現在竟然不用等了,那么這個功能到底是怎么實現的呢?
首先我們來分析一下 cookie 中多出來的這個 remember-me
,這個值一看就是一個 Base64 轉碼后的字符串,我們可以使用網上的一些在線工具來解碼,可以自己簡單寫兩行代碼來解碼:
1
2
3
4
5
6
|
@Test void contextLoads() { String s = new String( Base64.getDecoder().decode( "eW9sbzoxNjAxNDczNTY2NTA1OjlmMGY5YjBjOTAzYmNjYmU3ZjMwYWM0NjVlZjEzNmQ5" )); System.out.println( "s = " + s); } |
執行這段代碼,輸出結果如下:
s = yolo:1601473566505:9f0f9b0c903bccbe7f30ac465ef136d9
可以看到,這段 Base64 字符串實際上用 :
隔開,分成了三部分:
(1)第一段是用戶名,這個無需質疑。
(2)第二段看起來是一個時間戳,我們通過在線工具或者 Java 代碼解析后發現,這是一個兩周后的數據。
(3)第三段我就不賣關子了,這是使用 MD5 散列函數算出來的值,他的明文格式是
username + ":" + tokenExpiryTime + ":" + password + ":" + key
,最后的 key 是一個散列鹽值,可以用來防治令牌被修改。
了解到 cookie 中 remember-me
的含義之后,那么我們對于記住我的登錄流程也就很容易猜到了了。
在瀏覽器關閉后,并重新打開之后,用戶再去訪問 hello 接口,此時會攜帶著 cookie 中的 remember-me
到服務端,服務到拿到值之后,可以方便的計算出用戶名和過期時間,再根據用戶名查詢到用戶密碼,然后通過 MD5 散列函數計算出散列值,再將計算出的散列值和瀏覽器傳遞來的散列值進行對比,就能確認這個令牌是否有效。
流程就是這么個流程,接下來我們通過分析源碼來驗證一下這個流程對不對。
三、源碼分析
接下來,我們通過源碼來驗證一下我們上面說的對不對。
這里主要從兩個方面來介紹,一個是 remember-me 這個令牌生成的過程,另一個則是它解析的過程。
1. 生成
生成的核心處理方法在:TokenBasedRememberMeServices#onLoginSuccess:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Override public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = retrieveUserName(successfulAuthentication); String password = retrievePassword(successfulAuthentication); if (!StringUtils.hasLength(password)) { UserDetails user = getUserDetailsService().loadUserByUsername(username); password = user.getPassword(); } int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication); long expiryTime = System.currentTimeMillis(); expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime); String signatureValue = makeTokenSignature(expiryTime, username, password); 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()))); } |
(1)首先從登錄成功的 Authentication 中提取出用戶名/密碼。
(2)由于登錄成功之后,密碼可能被擦除了,所以,如果一開始沒有拿到密碼,就再從 UserDetailsService 中重新加載用戶并重新獲取密碼。
(3)再接下來去獲取令牌的有效期,令牌有效期默認就是兩周。
(4)再接下來調用makeTokenSignature
方法去計算散列值,實際上就是根據 username、令牌有效期以及 password、key 一起計算一個散列值。如果我們沒有自己去設置這個 key,默認是在RememberMeConfigurer#getKey
方法中進行設置的,它的值是一個 UUID 字符串。
(5)最后,將用戶名、令牌有效期以及計算得到的散列值放入 Cookie 中。
關于第四點,我這里再說一下。
由于我們自己沒有設置 key,key 默認值是一個 UUID 字符串,這樣會帶來一個問題,就是如果服務端重啟,這個 key 會變,這樣就導致之前派發出去的所有 remember-me
自動登錄令牌失效,所以,我們可以指定這個 key。指定方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .rememberMe() .key( "yolo" ) .and() .csrf().disable(); } |
如果自己配置了 key,即使服務端重啟,即使瀏覽器打開再關閉,也依然能夠訪問到 hello 接口
這是 remember-me 令牌生成的過程。至于是如何走到 onLoginSuccess 方法的,這里可以給大家稍微提醒一下思路:
AbstractAuthenticationProcessingFilter#doFilter -> AbstractAuthenticationProcessingFilter#successfulAuthentication -> AbstractRememberMeServices#loginSuccess -> TokenBasedRememberMeServices#onLoginSuccess。
2. 解析
那么當用戶關掉并打開瀏覽器之后,重新訪問 /hello
接口,此時的認證流程又是怎么樣的呢?
我們之前說過,Spring Security 中的一系列功能都是通過一個過濾器鏈實現的,RememberMe
這個功能當然也不例外。
Spring Security 中提供了 RememberMeAuthenticationFilter
類專門用來做相關的事情,我們來看下 RememberMeAuthenticationFilter
的 doFilter
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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 ) { Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null ) { rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); onSuccessfulAuthentication(request, response, rememberMeAuth); if ( this .eventPublisher != null ) { eventPublisher .publishEvent( new InteractiveAuthenticationSuccessEvent( SecurityContextHolder.getContext() .getAuthentication(), this .getClass())); } if (successHandler != null ) { successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return ; } } chain.doFilter(request, response); } else { chain.doFilter(request, response); } } |
這個方法最關鍵的地方在于,如果從 SecurityContextHolder
中無法獲取到當前登錄用戶實例,那么就調用 rememberMeServices.autoLogin
邏輯進行登錄,我們來看下這個方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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); } catch (CookieTheftException cte) { throw cte; } cancelCookie(request, response); return null ; } |
可以看到,這里就是提取出 cookie
信息,并對 cookie
信息進行解碼,解碼之后,再調用 processAutoLoginCookie
方法去做校驗,processAutoLoginCookie
方法的代碼我就不貼了,核心流程就是首先獲取用戶名和過期時間,再根據用戶名查詢到用戶密碼,然后通過 MD5 散列函數計算出散列值,再將拿到的散列值和瀏覽器傳遞來的散列值進行對比,就能確認這個令牌是否有效,進而確認登錄是否有效。
四、總結
看了上面的文章,大家可能已經發現,如果我們開啟了 RememberMe
功能,最最核心的東西就是放在 cookie
中的令牌了,這個令牌突破了 session
的限制,即使服務器重啟、即使瀏覽器關閉又重新打開,只要這個令牌沒有過期,就能訪問到數據。
一旦令牌丟失,別人就可以拿著這個令牌隨意登錄我們的系統了,這是一個非常危險的操作。
但是實際上這是一段悖論,為了提高用戶體驗(少登錄),我們的系統不可避免的引出了一些安全問題,不過我們可以通過技術將安全風險降低到最小
到此這篇關于SpringBoot 配合 SpringSecurity 實現自動登錄功能的代碼的文章就介紹到這了,更多相關SpringSecurity自動登錄內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/nanhuaibeian/article/details/108630616