Spring Security基于散列加密方案實(shí)現(xiàn)自動(dòng)登錄功能
前言
在前面的2個(gè)章節(jié)中,一一哥 帶大家實(shí)現(xiàn)了在Spring Security中添加圖形驗(yàn)證碼校驗(yàn)功能,其實(shí)Spring Security的功能不僅僅是這些,還可以實(shí)現(xiàn)很多別的效果,比如實(shí)現(xiàn)自動(dòng)登錄,注銷登錄等。
有的小伙伴會(huì)問,我們?yōu)槭裁匆獙?shí)現(xiàn)自動(dòng)登錄啊?這個(gè)需求其實(shí)還是很常見的,因?yàn)閷?duì)于用戶來說,他可能經(jīng)常需要進(jìn)行登錄以及退出登錄,你想想,如果用戶每次登錄時(shí)都要輸入自己的用戶名和密碼,是不是很煩,用戶體驗(yàn)是不是很不好?
所以為了提高項(xiàng)目的用戶體驗(yàn),我們可以在項(xiàng)目中添加自動(dòng)登錄功能,當(dāng)然也要給用戶提供退出登錄的功能。接下來就跟著 一一哥 來學(xué)習(xí)如何實(shí)現(xiàn)這些功能吧!
一. 自動(dòng)登錄簡介
1. 為什么要自動(dòng)登錄
我們?cè)谠L問網(wǎng)站或app時(shí),一般都會(huì)要求我們注冊(cè)一個(gè)賬號(hào),包含用戶名和密碼信息,其中密碼還會(huì)有長度及取值范圍的限制。很多時(shí)候,我們?cè)诓煌木W(wǎng)站上注冊(cè)的賬號(hào),可能密碼也不同,這就導(dǎo)致我們必須記住這些不同網(wǎng)站上的用戶信息。那么在下次登錄時(shí),因?yàn)槲覀兊拿艽a太多了,很有可能會(huì)記不起這些賬號(hào)密碼。所以在幾次嘗試登錄失敗之后,很多人都會(huì)選擇找回密碼,從而再次陷入如何設(shè)置密碼的循環(huán)里。
為了盡可能減少用戶重新登錄的頻率,提高用戶的使用體驗(yàn),我們可以提供自動(dòng)登錄這樣一個(gè)會(huì)給用戶帶來便利,同時(shí)也會(huì)給用戶帶來風(fēng)險(xiǎn)的體驗(yàn)性功能。
2. 自動(dòng)登錄的實(shí)現(xiàn)方案
了解了自動(dòng)登錄出現(xiàn)的背景及作用后,那么我們?cè)撛趺磳?shí)現(xiàn)自動(dòng)登錄呢?
首先我們知道,自動(dòng)登錄是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當(dāng)用戶下次訪問時(shí),自動(dòng)實(shí)現(xiàn)校驗(yàn)并建立登錄狀態(tài)的一種機(jī)制。
所以基于上面的原理,Spring Security 就為我們提供了兩種比較好的實(shí)現(xiàn)自動(dòng)登錄的方案:
- 基于散列加密算法機(jī)制:加密用戶必要的登錄信息,并生成令牌來實(shí)現(xiàn)自動(dòng)登錄,利用TokenBasedRememberMeServices類來實(shí)現(xiàn)。
- 基于數(shù)據(jù)庫等持久化數(shù)據(jù)存儲(chǔ)機(jī)制:生成持久化令牌來實(shí)現(xiàn)自動(dòng)登錄,利用PersistentTokenBasedRememberMeServices來實(shí)現(xiàn)。
我上面提到的2個(gè)實(shí)現(xiàn)類,其實(shí)都是AbstractRememberMeServices的子類,如下圖所示:


了解了這些核心API之后,我們就可以利用這兩個(gè)API來實(shí)現(xiàn)自動(dòng)登錄了。
二. 基于散列加密方案實(shí)現(xiàn)自動(dòng)登錄
我先帶各位利用第1種實(shí)現(xiàn)方案,即基于散列加密方案來實(shí)現(xiàn)自動(dòng)登錄。
首先我們還是在之前的案例基礎(chǔ)之上進(jìn)行開發(fā),具體的項(xiàng)目創(chuàng)建過程略過,請(qǐng)參考之前的章節(jié)內(nèi)容。
1. 配置加密令牌的key
首先我們創(chuàng)建一個(gè)application.yml文件,在其中添加數(shù)據(jù)庫配置,以及一個(gè)用來加密令牌的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類
跟之前的案例一樣,我還是要?jiǎng)?chuàng)建一個(gè)SecurityConfig類,在其中的configure(HttpSecurity http)方法中,通過JdbcTokenRepositoryImpl關(guān)聯(lián)我們的數(shù)據(jù)庫,并且通過rememberMe()方法開啟“記住我”功能,另外還要把我們前面在配置文件中的rememberKey配置進(jìn)來,作為散列加密的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關(guān)聯(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() {
//不對(duì)登錄密碼進(jìn)行加密
return NoOpPasswordEncoder.getInstance();
}
}
3. 添加測(cè)試接口
為了方便后續(xù)的測(cè)試,我隨便編寫一個(gè)測(cè)試用的web接口。
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("hello")
public String hello() {
return "hello, user";
}
}
4. 啟動(dòng)項(xiàng)目測(cè)試
然后我們把項(xiàng)目啟動(dòng)起來進(jìn)行測(cè)試,當(dāng)然你別忘了編寫項(xiàng)目入口類,這里我就不粘貼相關(guān)代碼了。
我們?cè)L問一下/user/hello接口,會(huì)先重定向到/login接口,這時(shí)候會(huì)發(fā)現(xiàn)在默認(rèn)的登錄頁面上多了一個(gè)“記住我”功能。

此時(shí)如果我們打開 開發(fā)者調(diào)試工具,并且勾選“記住我”,然后發(fā)起請(qǐng)求,這時(shí)候我們會(huì)在控制臺(tái)看到remember-me的cookie信息,說明Spring Security已經(jīng)自動(dòng)生成了remember-me這個(gè)cookie,且表單中的remember-me參數(shù)也處于了“on”狀態(tài)。


也就是說,我們利用簡單的幾行代碼,就實(shí)現(xiàn)了基于散列加密方案的自動(dòng)登錄。
三. 散列加密方案實(shí)現(xiàn)原理
你可能會(huì)很好奇,散列加密方案到底是怎么實(shí)現(xiàn)自動(dòng)登錄的呢?別急,接下來 壹哥就為你分析一下散列加密的實(shí)現(xiàn)原理。
1. cookie的加密原理分析
我在前面給各位說過,自動(dòng)登錄其實(shí)就是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當(dāng)用戶下次訪問時(shí),自動(dòng)實(shí)現(xiàn)校驗(yàn)并建立登錄狀態(tài)的一種機(jī)制。所以在自動(dòng)登錄后,肯定會(huì)生成代表用戶的cookie信息,但是為了安全,這個(gè)cookie肯定不會(huì)明文存儲(chǔ),需要把這個(gè)cookie進(jìn)行加密處理,當(dāng)然也會(huì)解碼處理。所以接下來我就給各位分析一下這個(gè)cookie的加密和解碼過程。
首先 壹哥 給各位解釋一下所謂的散列加密算法,其實(shí)質(zhì)就是把 username、expirationTime、password等字段,再加上自定義的key字段合并起來,在每個(gè)字段之間用 ":" 分隔,最后利用md5算法進(jìn)行哈希運(yùn)算,這樣就可以得到一個(gè)加密后的字符串。Spring Security把這個(gè)加密的字符串存儲(chǔ)到cookie中,作為用戶已登錄的標(biāo)識(shí)信息。
然后 壹哥 帶你看看TokenBasedRememberMeServices源碼類中的makeTokenSignature()方法,你會(huì)看到散列加密算法的具體加密實(shí)現(xiàn)過程,源碼如下圖所示:

2. cookie的解碼原理分析
上面利用MD5進(jìn)行了加密,用戶在下次登錄后,肯定需要進(jìn)行信息的比對(duì),以判斷用戶信息是否一致。Spring Security是先對(duì)cookie中的信息進(jìn)行解碼,然后與之前記錄的登錄信息進(jìn)行比對(duì),以此判斷用戶是否已登錄。
Spring Security是在AbstractRememberMeServices類的decodeCookie()方法中,利用Base64對(duì)cookie進(jìn)行解碼,如下圖所示:

對(duì)于以上2個(gè)源碼方法,我們可以簡化抽取出如下兩行代碼:
//對(duì)各字段進(jìn)行散列加密 hashInfo=md5Hex(username +":"+expirationTime +":"password+":"+key) //利用base64進(jìn)行解碼 rememberCookie=base64(username+":"+expirationrime+":"+hashInfo)
其中,expirationTime是指本次自動(dòng)登錄的有效期,key是自己指定的一個(gè)散列鹽值,用于防止令牌被修改。利用以上兩個(gè)
分析完源碼之后,壹哥給各位簡單總結(jié)一下cookie的生成驗(yàn)證原理:
- 首先利用上面的源碼生成cookie,并保存在瀏覽器中;
- 在瀏覽器關(guān)閉并重新打開之后,用戶再去訪問 /user/hello 接口時(shí),此時(shí)就會(huì)攜帶remember-me這個(gè)cookie到服務(wù)端;
- 服務(wù)器端拿到cookie之后,利用Base64進(jìn)行解碼,計(jì)算出用戶名和過期時(shí)間,再根據(jù)用戶名查詢到用戶密碼;
- 最后還要通過 MD5 散列函數(shù)計(jì)算出散列值,并將計(jì)算出的散列值和瀏覽器傳遞來的散列值進(jìn)行對(duì)比,以此確認(rèn)這個(gè)令牌是否有效。
3. 自動(dòng)登錄的源碼分析
上面分析完cookie信息的加密和解碼之后,接下來我再結(jié)合源碼,從兩個(gè)方面來介紹自動(dòng)登錄的實(shí)現(xiàn)過程,一個(gè)是 remember-me 令牌的生成的過程,另一個(gè)則是該令牌的解析過程。
3.1 令牌生成的源碼分析
我們要想知道源碼中是如何生成remember-me自動(dòng)登錄令牌的,首先得知道Spring Security是如何進(jìn)入到該令牌所在代碼的,這個(gè)代碼的執(zhí)行與我們前一章節(jié)所講的Spring Security的認(rèn)證授權(quán)有關(guān),請(qǐng)進(jìn)入到前面查看。
AbstractAuthenticationProcessingFilter#doFilter ->
AbstractAuthenticationProcessingFilter#successfulAuthentication ->
AbstractRememberMeServices#loginSuccess ->
TokenBasedRememberMeServices#onLoginSuccess
這個(gè)令牌生成的核心處理方法定義在:TokenBasedRememberMeServices#onLoginSuccess。
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
//從認(rèn)證對(duì)象中獲取用戶名
String username = retrieveUserName(successfulAuthentication);
//從認(rèn)證對(duì)象中獲取密碼
String password = retrievePassword(successfulAuthentication);
......
if (!StringUtils.hasLength(password)) {
//根據(jù)用戶名查詢出對(duì)應(yīng)的用戶
UserDetails user = getUserDetailsService().loadUserByUsername(username);
//獲取到用戶身上的密碼
password = user.getPassword();
}
//獲取登錄過期時(shí)間,默認(rèn)是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())));
}
以上源碼的實(shí)現(xiàn)邏輯很好理解:
- 首先從登錄成功的 Authentication 對(duì)象中提取出用戶名/密碼;
- 由于登錄成功之后,密碼可能被擦除了,所以如果一開始沒有拿到密碼,就再從 UserDetailsService 中重新加載用戶并重新獲取密碼;
- 接下來獲取令牌的有效期,令牌有效期默認(rèn)是兩周;
- 再接下來調(diào)用 makeTokenSignature()方法 去計(jì)算散列值,實(shí)際上就是根據(jù) username、令牌有效期以及 password、key 一起計(jì)算一個(gè)散列值。如果我們沒有自己去設(shè)置這個(gè) key,默認(rèn)是在 RememberMeConfigurer#getKey 方法中進(jìn)行設(shè)置的,它的值是一個(gè) UUID 字符串。但是如果服務(wù)端重啟,這個(gè)默認(rèn)的 key 是會(huì)變的,這樣就導(dǎo)致之前派發(fā)出去的所有 remember-me 自動(dòng)登錄令牌失效,所以我們可以指定這個(gè) key。
- 最后,將用戶名、令牌有效期以及計(jì)算得到的散列值放入 Cookie 中并隨response返回。
3.2 令牌解析的源碼分析
對(duì)于RememberMe 這個(gè)功能,Spring Security提供了 RememberMeAuthenticationFilter 這個(gè)過濾器類來處理相關(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) {
//處理自動(dòng)登錄的業(yè)務(wù)邏輯
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);
}
}
這個(gè)方法最關(guān)鍵的地方在于,如果從 SecurityContextHolder 中無法獲取到當(dāng)前登錄用戶實(shí)例,那么就調(diào)用 rememberMeServices.autoLogin()邏輯進(jìn)行登錄,我們來看下這個(gè)方法:
@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 信息,并對(duì) cookie 信息進(jìn)行解碼。解碼之后,再調(diào)用 processAutoLoginCookie()方法去做校驗(yàn)。processAutoLoginCookie() 方法的代碼我就不貼了,核心流程就是首先獲取用戶名和過期時(shí)間,再根據(jù)用戶名查詢到用戶密碼,然后通過 MD5 散列函數(shù)計(jì)算出散列值。最后再將拿到的散列值和瀏覽器傳遞來的散列值進(jìn)行對(duì)比,就能確認(rèn)這個(gè)令牌是否有效,進(jìn)而確認(rèn)登錄是否有效。
至此,壹哥 就結(jié)合著源碼和底層原理,給大家講解了基于散列加密方案實(shí)現(xiàn)了自動(dòng)登錄,并且在本案例中給大家介紹了散列加密算法,你掌握的怎么樣呢?請(qǐng)?jiān)谠u(píng)論區(qū)給 一一哥 留言,說說你的感受吧!下一篇文章中,壹哥 會(huì)給各位講解 基于持久化令牌方案實(shí)現(xiàn)自動(dòng)登錄,敬請(qǐng)期待哦!
到此這篇關(guān)于Spring Security基于散列加密方案實(shí)現(xiàn)自動(dòng)登錄功能的文章就介紹到這了,更多相關(guān)Spring Security散列加密方案實(shí)現(xiàn)自動(dòng)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring security密碼加密實(shí)現(xiàn)代碼實(shí)例
- Spring security實(shí)現(xiàn)對(duì)賬戶進(jìn)行加密
- Spring Security使用數(shù)據(jù)庫認(rèn)證及用戶密碼加密和解密功能
- Spring?Security如何實(shí)現(xiàn)升級(jí)密碼加密方式詳解
- Python 內(nèi)置函數(shù)globals()和locals()對(duì)比詳解
- Python基礎(chǔ)教程之內(nèi)置函數(shù)locals()和globals()用法分析
- python內(nèi)置函數(shù)globals()的實(shí)現(xiàn)代碼
相關(guān)文章
微服務(wù)鏈路追蹤Spring Cloud Sleuth整合Zipkin解析
這篇文章主要為大家介紹了微服務(wù)鏈路追蹤Spring Cloud Sleuth整合Zipkin解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
SpringMVC對(duì)自定義controller入?yún)㈩A(yù)處理方式
這篇文章主要介紹了SpringMVC對(duì)自定義controller入?yún)㈩A(yù)處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
springboot使用Mybatis-plus分頁插件的案例詳解
這篇文章主要介紹了springboot使用Mybatis-plus分頁插件的相關(guān)知識(shí),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05
elasticsearch索引index之Translog數(shù)據(jù)功能分析
這篇文章主要為大家介紹了elasticsearch索引index之Translog數(shù)據(jù)功能分析,主要分析translog的結(jié)構(gòu)及寫入方式,有需要的朋友可以借鑒參考下2022-04-04
阿里云部署SpringBoot項(xiàng)目啟動(dòng)后被殺進(jìn)程的問題解析
這篇文章主要介紹了阿里云部署SpringBoot項(xiàng)目啟動(dòng)后被殺進(jìn)程的問題,本文給大家分享問題原因所在及解決步驟,需要的朋友可以參考下2023-09-09
Java8新特性之泛型的目標(biāo)類型推斷_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
泛型是Java SE 1.5的新特性,泛型的本質(zhì)是參數(shù)化類型,也就是說所操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)。下面通過本文給分享Java8新特性之泛型的目標(biāo)類型推斷,感興趣的朋友參考下吧2017-06-06
spring boot+spring cache實(shí)現(xiàn)兩級(jí)緩存(redis+caffeine)
這篇文章主要介紹了spring boot+spring cache實(shí)現(xiàn)兩級(jí)緩存(redis+caffeine),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02

