SpringBoot 配合 SpringSecurity 實(shí)現(xiàn)自動登錄功能的代碼
自動登錄是我們在軟件開發(fā)時(shí)一個(gè)非常常見的功能,例如我們登錄 QQ 郵箱:

很多網(wǎng)站我們在登錄的時(shí)候都會看到類似的選項(xiàng),畢竟總讓用戶輸入用戶名密碼是一件很麻煩的事。
自動登錄功能就是,用戶在登錄成功后,在某一段時(shí)間內(nèi),如果用戶關(guān)閉了瀏覽器并重新打開,或者服務(wù)器重啟了,都不需要用戶重新登錄了,用戶依然可以直接訪問接口數(shù)據(jù)
作為一個(gè)常見的功能,我們的 Spring Security 肯定也提供了相應(yīng)的支持,本文我們就來看下 Spring Security 中如何實(shí)現(xiàn)這個(gè)功能。
一、加入 remember-me
為了配置方便,加入兩個(gè)依賴即可:

配置類中添加如下代碼:
@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();
}
}
大家看到,這里只需要添加一個(gè) .rememberMe() 即可,自動登錄功能就成功添加進(jìn)來了。
接下來我們隨意添加一個(gè)測試接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello Yolo !!!";
}
}

這個(gè)時(shí)候大家發(fā)現(xiàn),默認(rèn)的登錄頁面多了一個(gè)選項(xiàng),就是記住我。我們輸入用戶名密碼,并且勾選上記住我這個(gè)框,然后點(diǎn)擊登錄按鈕執(zhí)行登錄操作。
可以看到,登錄數(shù)據(jù)中,除了 username 和 password 之外,還有一個(gè) remember-me,之所以給大家看這個(gè),是想告訴大家,如果你你需要自定義登錄頁面,RememberMe 這個(gè)選項(xiàng)的 key 該怎么寫。
登錄成功之后,就會自動跳轉(zhuǎn)到 hello 接口了。我們注意,系統(tǒng)訪問 hello 接口的時(shí)候,攜帶的 cookie:

大家注意到,這里多了一個(gè) remember-me,這就是這里實(shí)現(xiàn)的核心,關(guān)于這個(gè) remember-me 我一會解釋,我們先來測試效果。
接下來,我們關(guān)閉瀏覽器,再重新打開瀏覽器。正常情況下,瀏覽器關(guān)閉再重新打開,如果需要再次訪問 hello 接口,就需要我們重新登錄了。但是此時(shí),我們再去訪問 hello 接口,發(fā)現(xiàn)不用重新登錄了,直接就能訪問到,這就說明我們的 RememberMe 配置生效了(即下次自動登錄功能生效了)。
二、原理分析
按理說,瀏覽器關(guān)閉再重新打開,就要重新登錄,現(xiàn)在竟然不用等了,那么這個(gè)功能到底是怎么實(shí)現(xiàn)的呢?
首先我們來分析一下 cookie 中多出來的這個(gè) remember-me,這個(gè)值一看就是一個(gè) Base64 轉(zhuǎn)碼后的字符串,我們可以使用網(wǎng)上的一些在線工具來解碼,可以自己簡單寫兩行代碼來解碼:
@Test
void contextLoads() {
String s = new String(
Base64.getDecoder().decode("eW9sbzoxNjAxNDczNTY2NTA1OjlmMGY5YjBjOTAzYmNjYmU3ZjMwYWM0NjVlZjEzNmQ5"));
System.out.println("s = " + s);
}
執(zhí)行這段代碼,輸出結(jié)果如下:
s = yolo:1601473566505:9f0f9b0c903bccbe7f30ac465ef136d9
可以看到,這段 Base64 字符串實(shí)際上用 : 隔開,分成了三部分:
(1)第一段是用戶名,這個(gè)無需質(zhì)疑。
(2)第二段看起來是一個(gè)時(shí)間戳,我們通過在線工具或者 Java 代碼解析后發(fā)現(xiàn),這是一個(gè)兩周后的數(shù)據(jù)。
(3)第三段我就不賣關(guān)子了,這是使用 MD5 散列函數(shù)算出來的值,他的明文格式是
username + ":" + tokenExpiryTime + ":" + password + ":" + key,最后的 key 是一個(gè)散列鹽值,可以用來防治令牌被修改。
了解到 cookie 中 remember-me 的含義之后,那么我們對于記住我的登錄流程也就很容易猜到了了。
在瀏覽器關(guān)閉后,并重新打開之后,用戶再去訪問 hello 接口,此時(shí)會攜帶著 cookie 中的 remember-me 到服務(wù)端,服務(wù)到拿到值之后,可以方便的計(jì)算出用戶名和過期時(shí)間,再根據(jù)用戶名查詢到用戶密碼,然后通過 MD5 散列函數(shù)計(jì)算出散列值,再將計(jì)算出的散列值和瀏覽器傳遞來的散列值進(jìn)行對比,就能確認(rèn)這個(gè)令牌是否有效。
流程就是這么個(gè)流程,接下來我們通過分析源碼來驗(yàn)證一下這個(gè)流程對不對。
三、源碼分析
接下來,我們通過源碼來驗(yàn)證一下我們上面說的對不對。
這里主要從兩個(gè)方面來介紹,一個(gè)是 remember-me 這個(gè)令牌生成的過程,另一個(gè)則是它解析的過程。
1. 生成
生成的核心處理方法在:TokenBasedRememberMeServices#onLoginSuccess:
@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)再接下來去獲取令牌的有效期,令牌有效期默認(rèn)就是兩周。
(4)再接下來調(diào)用makeTokenSignature方法去計(jì)算散列值,實(shí)際上就是根據(jù) username、令牌有效期以及 password、key 一起計(jì)算一個(gè)散列值。如果我們沒有自己去設(shè)置這個(gè) key,默認(rèn)是在RememberMeConfigurer#getKey方法中進(jìn)行設(shè)置的,它的值是一個(gè) UUID 字符串。
(5)最后,將用戶名、令牌有效期以及計(jì)算得到的散列值放入 Cookie 中。
關(guān)于第四點(diǎn),我這里再說一下。
由于我們自己沒有設(shè)置 key,key 默認(rèn)值是一個(gè) UUID 字符串,這樣會帶來一個(gè)問題,就是如果服務(wù)端重啟,這個(gè) key 會變,這樣就導(dǎo)致之前派發(fā)出去的所有 remember-me 自動登錄令牌失效,所以,我們可以指定這個(gè) key。指定方式如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("yolo")
.and()
.csrf().disable();
}
如果自己配置了 key,即使服務(wù)端重啟,即使瀏覽器打開再關(guān)閉,也依然能夠訪問到 hello 接口
這是 remember-me 令牌生成的過程。至于是如何走到 onLoginSuccess 方法的,這里可以給大家稍微提醒一下思路:
AbstractAuthenticationProcessingFilter#doFilter -> AbstractAuthenticationProcessingFilter#successfulAuthentication -> AbstractRememberMeServices#loginSuccess -> TokenBasedRememberMeServices#onLoginSuccess。
2. 解析
那么當(dāng)用戶關(guān)掉并打開瀏覽器之后,重新訪問 /hello 接口,此時(shí)的認(rèn)證流程又是怎么樣的呢?
我們之前說過,Spring Security 中的一系列功能都是通過一個(gè)過濾器鏈實(shí)現(xiàn)的,RememberMe 這個(gè)功能當(dāng)然也不例外。
Spring Security 中提供了 RememberMeAuthenticationFilter 類專門用來做相關(guān)的事情,我們來看下 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) {
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);
}
}
這個(gè)方法最關(guān)鍵的地方在于,如果從 SecurityContextHolder 中無法獲取到當(dāng)前登錄用戶實(shí)例,那么就調(diào)用 rememberMeServices.autoLogin 邏輯進(jìn)行登錄,我們來看下這個(gè)方法:
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 信息進(jìn)行解碼,解碼之后,再調(diào)用 processAutoLoginCookie 方法去做校驗(yàn),processAutoLoginCookie 方法的代碼我就不貼了,核心流程就是首先獲取用戶名和過期時(shí)間,再根據(jù)用戶名查詢到用戶密碼,然后通過 MD5 散列函數(shù)計(jì)算出散列值,再將拿到的散列值和瀏覽器傳遞來的散列值進(jìn)行對比,就能確認(rèn)這個(gè)令牌是否有效,進(jìn)而確認(rèn)登錄是否有效。
三、總結(jié)
看了上面的文章,大家可能已經(jīng)發(fā)現(xiàn),如果我們開啟了 RememberMe 功能,最最核心的東西就是放在 cookie 中的令牌了,這個(gè)令牌突破了 session 的限制,即使服務(wù)器重啟、即使瀏覽器關(guān)閉又重新打開,只要這個(gè)令牌沒有過期,就能訪問到數(shù)據(jù)。
一旦令牌丟失,別人就可以拿著這個(gè)令牌隨意登錄我們的系統(tǒng)了,這是一個(gè)非常危險(xiǎn)的操作。
但是實(shí)際上這是一段悖論,為了提高用戶體驗(yàn)(少登錄),我們的系統(tǒng)不可避免的引出了一些安全問題,不過我們可以通過技術(shù)將安全風(fēng)險(xiǎn)降低到最小
到此這篇關(guān)于SpringBoot 配合 SpringSecurity 實(shí)現(xiàn)自動登錄功能的代碼的文章就介紹到這了,更多相關(guān)SpringSecurity自動登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot如何整合Springsecurity實(shí)現(xiàn)數(shù)據(jù)庫登錄及權(quán)限控制
- SpringBoot基于SpringSecurity表單登錄和權(quán)限驗(yàn)證的示例
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- Springboot+SpringSecurity實(shí)現(xiàn)圖片驗(yàn)證碼登錄的示例
- SpringSecurity整合springBoot、redis實(shí)現(xiàn)登錄互踢功能
- springboot+jwt+springSecurity微信小程序授權(quán)登錄問題
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- SpringBoot+SpringSecurity處理Ajax登錄請求問題(推薦)
- SpringBoot整合Springsecurity實(shí)現(xiàn)數(shù)據(jù)庫登錄及權(quán)限控制功能
相關(guān)文章
在Java的JDBC使用中設(shè)置事務(wù)回滾的保存點(diǎn)的方法
這篇文章主要介紹了在Java的JDBC使用中設(shè)置事務(wù)回滾的保存點(diǎn)的方法,JDBC是Java用于連接各種數(shù)據(jù)庫的API,需要的朋友可以參考下2015-12-12
SpringBoot集成Quartz實(shí)現(xiàn)持久化定時(shí)接口調(diào)用任務(wù)
Quartz是功能強(qiáng)大的開源作業(yè)調(diào)度庫,幾乎可以集成到任何?Java?應(yīng)用程序中,從最小的獨(dú)立應(yīng)用程序到最大的電子商務(wù)系統(tǒng),本文將通過代碼示例給大家介紹SpringBoot集成Quartz實(shí)現(xiàn)持久化定時(shí)接口調(diào)用任務(wù),需要的朋友可以參考下2023-07-07
SpringBoot如何通過自定義注解實(shí)現(xiàn)權(quán)限檢查詳解
這篇文章主要給大家介紹了關(guān)于SpringBoot如何通過自定義注解實(shí)現(xiàn)權(quán)限檢查的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
Java實(shí)現(xiàn)List轉(zhuǎn)換為Map的方法小結(jié)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)List轉(zhuǎn)換為Map的一些常見的方法,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以參考一下2024-03-03
AsyncHttpClient KeepAliveStrategy源碼流程解讀
這篇文章主要為大家介紹了AsyncHttpClient KeepAliveStrategy源碼流程解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java動態(tài)代理之?dāng)r截器的應(yīng)用
今天小編就為大家分享一篇關(guān)于Java動態(tài)代理之?dāng)r截器的應(yīng)用,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01
Java中實(shí)體與Map之間的相互轉(zhuǎn)換代碼示例
生活中經(jīng)常用到map數(shù)據(jù)與實(shí)體類的轉(zhuǎn)換,下面這篇文章主要給大家介紹了關(guān)于Java中實(shí)體與Map之間相互轉(zhuǎn)換的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12

