溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)

發(fā)布時(shí)間:2021-07-06 18:07:36 來(lái)源:億速云 閱讀:148 作者:chen 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要講解了“SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)”吧!

降低安全風(fēng)險(xiǎn),我主要從兩個(gè)方面來(lái)給大家介紹:

  1. 持久化令牌方案

  2. 二次校驗(yàn)

當(dāng)然,還是老規(guī)矩,閱讀本文一定先閱讀本系列前面的文章,這有助于更好的理解本文:

好了,我們就不廢話(huà)了,來(lái)看今天的文章。

1.持久化令牌

1.1 原理

要理解持久化令牌,一定要先搞明白自動(dòng)登錄的基本玩法,參考(Spring Boot + Spring Security 實(shí)現(xiàn)自動(dòng)登錄功能)。

持久化令牌就是在基本的自動(dòng)登錄功能基礎(chǔ)上,又增加了新的校驗(yàn)參數(shù),來(lái)提高系統(tǒng)的安全性,這一些都是由開(kāi)發(fā)者在后臺(tái)完成的,對(duì)于用戶(hù)來(lái)說(shuō),登錄體驗(yàn)和普通的自動(dòng)登錄體驗(yàn)是一樣的。

在持久化令牌中,新增了兩個(gè)經(jīng)過(guò) MD5 散列函數(shù)計(jì)算的校驗(yàn)參數(shù),一個(gè)是 series,另一個(gè)是 token。其中,series  只有當(dāng)用戶(hù)在使用用戶(hù)名/密碼登錄時(shí),才會(huì)生成或者更新,而 token 只要有新的會(huì)話(huà),就會(huì)重新生成,這樣就可以避免一個(gè)用戶(hù)同時(shí)在多端登錄,就像手機(jī) QQ  ,一個(gè)手機(jī)上登錄了,就會(huì)踢掉另外一個(gè)手機(jī)的登錄,這樣用戶(hù)就會(huì)很容易發(fā)現(xiàn)賬戶(hù)是否泄漏(之前看到松哥交流群里有小伙伴在討論如何禁止多端登錄,其實(shí)就可以借鑒這里的思路)。

持久化令牌的具體處理類(lèi)在 PersistentTokenBasedRememberMeServices 中,上篇文章我們講到的自動(dòng)化登錄具體的處理類(lèi)是在  TokenBasedRememberMeServices 中,它們有一個(gè)共同的父類(lèi):

SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)

而用來(lái)保存令牌的處理類(lèi)則是 PersistentRememberMeToken,該類(lèi)的定義也很簡(jiǎn)潔命令:

public class PersistentRememberMeToken {  private final String username;  private final String series;  private final String tokenValue;  private final Date date;     //省略 getter }

這里的 Date 表示上一次使用自動(dòng)登錄的時(shí)間。

1.2 代碼演示

接下來(lái),我通過(guò)代碼來(lái)給大家演示一下持久化令牌的具體用法。

首先我們需要一張表來(lái)記錄令牌信息,這張表我們可以完全自定義,也可以使用系統(tǒng)默認(rèn)提供的 JDBC 來(lái)操作,如果使用默認(rèn)的 JDBC,即  JdbcTokenRepositoryImpl,我們可以來(lái)分析一下該類(lèi)的定義:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements   PersistentTokenRepository {  public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "    + "token varchar(64) not null, last_used timestamp not null)";  public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";  public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";  public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";  public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?"; }

根據(jù)這段 SQL 定義,我們就可以分析出來(lái)表的結(jié)構(gòu),松哥這里給出一段 SQL 腳本:

CREATE TABLE `persistent_logins` (   `username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,   `series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,   `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,   `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,   PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

首先我們?cè)跀?shù)據(jù)庫(kù)中準(zhǔn)備好這張表。

既然要連接數(shù)據(jù)庫(kù),我們還需要準(zhǔn)備 jdbc 和 mysql 依賴(lài),如下:

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency>     <groupId>mysql</groupId>     <artifactId>mysql-connector-java</artifactId> </dependency>

然后修改 application.properties ,配置數(shù)據(jù)庫(kù)連接信息:

spring.datasource.url=jdbc:mysql:///oauth3?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123

接下來(lái),我們修改 SecurityConfig,如下:

@Autowired DataSource dataSource; @Bean JdbcTokenRepositoryImpl jdbcTokenRepository() {     JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();     jdbcTokenRepository.setDataSource(dataSource);     return jdbcTokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception {     http.authorizeRequests()             .anyRequest().authenticated()             .and()             .formLogin()             .and()             .rememberMe()             .key("javaboy")             .tokenRepository(jdbcTokenRepository())             .and()             .csrf().disable(); }

提供一個(gè) JdbcTokenRepositoryImpl 實(shí)例,并給其配置 DataSource 數(shù)據(jù)源,最后通過(guò) tokenRepository 將  JdbcTokenRepositoryImpl 實(shí)例納入配置中。

OK,做完這一切,我們就可以測(cè)試了。

1.3 測(cè)試

我們還是先去訪問(wèn) /hello  接口,此時(shí)會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,然后我們執(zhí)行登錄操作,記得勾選上“記住我”這個(gè)選項(xiàng),登錄成功后,我們可以重啟服務(wù)器、然后關(guān)閉瀏覽器再打開(kāi),再去訪問(wèn) /hello  接口,發(fā)現(xiàn)依然能夠訪問(wèn)到,說(shuō)明我們的持久化令牌配置已經(jīng)生效。

查看 remember-me 的令牌,如下:

SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)

這個(gè)令牌經(jīng)過(guò)解析之后,格式如下:

emhqATk3ZDBdR8862WP4Ig%3D%3D:ZAEv6EIWqA7CkGbYewCh8g%3D%3D

這其中,%3D 表示 =,所以上面的字符實(shí)際上可以翻譯成下面這樣:

emhqATk3ZDBdR8862WP4Ig==:ZAEv6EIWqA7CkGbYewCh8g==

此時(shí),查看數(shù)據(jù)庫(kù),我們發(fā)現(xiàn)之前的表中生成了一條記錄:

SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)

數(shù)據(jù)庫(kù)中的記錄和我們看到的 remember-me 令牌解析后是一致的。

1.4 源碼分析

這里的源碼分析和上篇文章的流程基本一致,只不過(guò)實(shí)現(xiàn)類(lèi)變了,也就是生成令牌/解析令牌的實(shí)現(xiàn)變了,所以這里我主要和大家展示不一樣的地方,流程問(wèn)題,大家可以參考上篇文章。

這次的實(shí)現(xiàn)類(lèi)主要是:PersistentTokenBasedRememberMeServices,我們先來(lái)看里邊幾個(gè)和令牌生成相關(guān)的方法:

protected void onLoginSuccess(HttpServletRequest request,   HttpServletResponse response, Authentication successfulAuthentication) {  String username = successfulAuthentication.getName();  PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(    username, generateSeriesData(), generateTokenData(), new Date());  tokenRepository.createNewToken(persistentToken);  addCookie(persistentToken, request, response); } protected String generateSeriesData() {  byte[] newSeries = new byte[seriesLength];  random.nextBytes(newSeries);  return new String(Base64.getEncoder().encode(newSeries)); } protected String generateTokenData() {  byte[] newToken = new byte[tokenLength];  random.nextBytes(newToken);  return new String(Base64.getEncoder().encode(newToken)); } private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,   HttpServletResponse response) {  setCookie(new String[] { token.getSeries(), token.getTokenValue() },    getTokenValiditySeconds(), request, response); }

可以看到:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 在登錄成功后,首先還是獲取到用戶(hù)名,即 username。

  3. 接下來(lái)構(gòu)造一個(gè) PersistentRememberMeToken 實(shí)例,generateSeriesData 和 generateTokenData  方法分別用來(lái)獲取 series 和 token,具體的生成過(guò)程實(shí)際上就是調(diào)用 SecureRandom 生成隨機(jī)數(shù)再進(jìn)行 Base64 編碼,不同于我們以前用的  Math.random 或者 java.util.Random 這種偽隨機(jī)數(shù),SecureRandom  則采用的是類(lèi)似于密碼學(xué)的隨機(jī)數(shù)生成規(guī)則,其輸出結(jié)果較難預(yù)測(cè),適合在登錄這樣的場(chǎng)景下使用。

  4. 調(diào)用 tokenRepository 實(shí)例中的 createNewToken 方法,tokenRepository 實(shí)際上就是我們一開(kāi)始配置的  JdbcTokenRepositoryImpl,所以這行代碼實(shí)際上就是將 PersistentRememberMeToken 存入數(shù)據(jù)庫(kù)中。

  5. 最后 addCookie,大家可以看到,就是添加了 series 和 token。

這是令牌生成的過(guò)程,還有令牌校驗(yàn)的過(guò)程,也在該類(lèi)中,方法是:processAutoLoginCookie:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,   HttpServletRequest request, HttpServletResponse response) {  final String presentedSeries = cookieTokens[0];  final String presentedToken = cookieTokens[1];  PersistentRememberMeToken token = tokenRepository    .getTokenForSeries(presentedSeries);  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."));  }  if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System    .currentTimeMillis()) {   throw new RememberMeAuthenticationException("Remember-me login has expired");  }  PersistentRememberMeToken newToken = new PersistentRememberMeToken(    token.getUsername(), token.getSeries(), generateTokenData(), new Date());  tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),     newToken.getDate());  addCookie(newToken, request, response);  return getUserDetailsService().loadUserByUsername(token.getUsername()); }

這段邏輯也比較簡(jiǎn)單:

首先從前端傳來(lái)的 cookie 中解析出 series 和 token。

根據(jù) series 從數(shù)據(jù)庫(kù)中查詢(xún)出一個(gè) PersistentRememberMeToken 實(shí)例。

如果查出來(lái)的 token 和前端傳來(lái)的 token 不相同,說(shuō)明賬號(hào)可能被人盜用(別人用你的令牌登錄之后,token 會(huì)變)。此時(shí)根據(jù)用戶(hù)名移除相關(guān)的  token,相當(dāng)于必須要重新輸入用戶(hù)名密碼登錄才能獲取新的自動(dòng)登錄權(quán)限。

接下來(lái)校驗(yàn) token 是否過(guò)期。

構(gòu)造新的 PersistentRememberMeToken 對(duì)象,并且更新數(shù)據(jù)庫(kù)中的 token(這就是我們文章開(kāi)頭說(shuō)的,新的會(huì)話(huà)都會(huì)對(duì)應(yīng)一個(gè)新的  token)。

將新的令牌重新添加到 cookie 中返回。

根據(jù)用戶(hù)名查詢(xún)用戶(hù)信息,再走一波登錄流程。

OK,這里和小伙伴們簡(jiǎn)單理了一下令牌生成和校驗(yàn)的過(guò)程,具體的流程,大家可以參考上篇文章。

2.二次校驗(yàn)

相比于上篇文章,持久化令牌的方式其實(shí)已經(jīng)安全很多了,但是依然存在用戶(hù)身份被盜用的問(wèn)題,這個(gè)問(wèn)題實(shí)際上很難完美解決,我們能做的,只能是當(dāng)發(fā)生用戶(hù)身份被盜用這樣的事情時(shí),將損失降低到最小。

因此,我們來(lái)看下另一種方案,就是二次校驗(yàn)。

二次校驗(yàn)這塊,實(shí)現(xiàn)起來(lái)要稍微復(fù)雜一點(diǎn),我先來(lái)和大家說(shuō)說(shuō)思路。

為了讓用戶(hù)使用方便,我們開(kāi)通了自動(dòng)登錄功能,但是自動(dòng)登錄功能又帶來(lái)了安全風(fēng)險(xiǎn),一個(gè)規(guī)避的辦法就是如果用戶(hù)使用了自動(dòng)登錄功能,我們可以只讓他做一些常規(guī)的不敏感操作,例如數(shù)據(jù)瀏覽、查看,但是不允許他做任何修改、刪除操作,如果用戶(hù)點(diǎn)擊了修改、刪除按鈕,我們可以跳轉(zhuǎn)回登錄頁(yè)面,讓用戶(hù)重新輸入密碼確認(rèn)身份,然后再允許他執(zhí)行敏感操作。

這個(gè)功能在 Shiro 中有一個(gè)比較方便的過(guò)濾器可以配置,Spring Security 當(dāng)然也一樣,例如我現(xiàn)在提供三個(gè)訪問(wèn)接口:

@RestController public class HelloController {     @GetMapping("/hello")     public String hello() {         return "hello";     }     @GetMapping("/admin")     public String admin() {         return "admin";     }     @GetMapping("/rememberme")     public String rememberme() {         return "rememberme";     } }
  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 第一個(gè) /hello 接口,只要認(rèn)證后就可以訪問(wèn),無(wú)論是通過(guò)用戶(hù)名密碼認(rèn)證還是通過(guò)自動(dòng)登錄認(rèn)證,只要認(rèn)證了,就可以訪問(wèn)。

  3. 第二個(gè) /admin 接口,必須要用戶(hù)名密碼認(rèn)證之后才能訪問(wèn),如果用戶(hù)是通過(guò)自動(dòng)登錄認(rèn)證的,則必須重新輸入用戶(hù)名密碼才能訪問(wèn)該接口。

  4. 第三個(gè) /rememberme 接口,必須是通過(guò)自動(dòng)登錄認(rèn)證后才能訪問(wèn),如果用戶(hù)是通過(guò)用戶(hù)名/密碼認(rèn)證的,則無(wú)法訪問(wèn)該接口。

好了,我們來(lái)看下接口的訪問(wèn)要怎么配置:

@Override protected void configure(HttpSecurity http) throws Exception {     http.authorizeRequests()             .antMatchers("/rememberme").rememberMe()             .antMatchers("/admin").fullyAuthenticated()             .anyRequest().authenticated()             .and()             .formLogin()             .and()             .rememberMe()             .key("javaboy")             .tokenRepository(jdbcTokenRepository())             .and()             .csrf().disable(); }

可以看到:

  1. /rememberme 接口是需要 rememberMe 才能訪問(wèn)。

  2. /admin 是需要 fullyAuthenticated,fullyAuthenticated 不同于  authenticated,fullyAuthenticated 不包含自動(dòng)登錄的形式,而 authenticated 包含自動(dòng)登錄的形式。

  3. 最后剩余的接口(/hello)都是 authenticated 就能訪問(wèn)。

OK,配置完成后,重啟測(cè)試,測(cè)試過(guò)程我就不再贅述了。

感謝各位的閱讀,以上就是“SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)SpringSecurity怎么降低 RememberMe 的安全風(fēng)險(xiǎn)這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI