自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

SpringSecurity系列之降低 RememberMe 的安全風(fēng)險(xiǎn)

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

[[394666]]

在上篇文章中,我們提到了 Spring Boot 自動(dòng)登錄存在的一些安全風(fēng)險(xiǎn),在實(shí)際應(yīng)用中,我們肯定要把這些安全風(fēng)險(xiǎn)降到最低,今天就來和大家聊一聊如何降低安全風(fēng)險(xiǎn)的問題。

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

  1. 持久化令牌方案
  2. 二次校驗(yàn)

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

好了,我們就不廢話了,來看今天的文章。

1.持久化令牌

1.1 原理

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

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

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

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

而用來保存令牌的處理類則是 PersistentRememberMeToken,該類的定義也很簡潔命令:

  1. public class PersistentRememberMeToken { 
  2.  private final String username; 
  3.  private final String series; 
  4.  private final String tokenValue; 
  5.  private final Date date
  6.     //省略 getter 

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

1.2 代碼演示

接下來,我通過代碼來給大家演示一下持久化令牌的具體用法。

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

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

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

  1. CREATE TABLE `persistent_logins` ( 
  2.   `username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL
  3.   `series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL
  4.   `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL
  5.   `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  6.   PRIMARY KEY (`series`) 
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 

首先我們在數(shù)據(jù)庫中準(zhǔn)備好這張表。

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

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-jdbc</artifactId> 
  4. </dependency> 
  5. <dependency> 
  6.     <groupId>mysql</groupId> 
  7.     <artifactId>mysql-connector-java</artifactId> 
  8. </dependency> 

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

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

接下來,我們修改 SecurityConfig,如下:

  1. @Autowired 
  2. DataSource dataSource; 
  3. @Bean 
  4. JdbcTokenRepositoryImpl jdbcTokenRepository() { 
  5.     JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); 
  6.     jdbcTokenRepository.setDataSource(dataSource); 
  7.     return jdbcTokenRepository; 
  8. @Override 
  9. protected void configure(HttpSecurity http) throws Exception { 
  10.     http.authorizeRequests() 
  11.             .anyRequest().authenticated() 
  12.             .and() 
  13.             .formLogin() 
  14.             .and() 
  15.             .rememberMe() 
  16.             .key("javaboy"
  17.             .tokenRepository(jdbcTokenRepository()) 
  18.             .and() 
  19.             .csrf().disable(); 

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

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

1.3 測試

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

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

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

  1. emhqATk3ZDBdR8862WP4Ig%3D%3D:ZAEv6EIWqA7CkGbYewCh8g%3D%3D 

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

  1. emhqATk3ZDBdR8862WP4Ig==:ZAEv6EIWqA7CkGbYewCh8g== 

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

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

1.4 源碼分析

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

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

  1. protected void onLoginSuccess(HttpServletRequest request, 
  2.   HttpServletResponse response, Authentication successfulAuthentication) { 
  3.  String username = successfulAuthentication.getName(); 
  4.  PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( 
  5.    username, generateSeriesData(), generateTokenData(), new Date()); 
  6.  tokenRepository.createNewToken(persistentToken); 
  7.  addCookie(persistentToken, request, response); 
  8. protected String generateSeriesData() { 
  9.  byte[] newSeries = new byte[seriesLength]; 
  10.  random.nextBytes(newSeries); 
  11.  return new String(Base64.getEncoder().encode(newSeries)); 
  12. protected String generateTokenData() { 
  13.  byte[] newToken = new byte[tokenLength]; 
  14.  random.nextBytes(newToken); 
  15.  return new String(Base64.getEncoder().encode(newToken)); 
  16. private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, 
  17.   HttpServletResponse response) { 
  18.  setCookie(new String[] { token.getSeries(), token.getTokenValue() }, 
  19.    getTokenValiditySeconds(), request, response); 

可以看到:

  1. 在登錄成功后,首先還是獲取到用戶名,即 username。
  2. 接下來構(gòu)造一個(gè) PersistentRememberMeToken 實(shí)例,generateSeriesData 和 generateTokenData 方法分別用來獲取 series 和 token,具體的生成過程實(shí)際上就是調(diào)用 SecureRandom 生成隨機(jī)數(shù)再進(jìn)行 Base64 編碼,不同于我們以前用的 Math.random 或者 java.util.Random 這種偽隨機(jī)數(shù),SecureRandom 則采用的是類似于密碼學(xué)的隨機(jī)數(shù)生成規(guī)則,其輸出結(jié)果較難預(yù)測,適合在登錄這樣的場景下使用。
  3. 調(diào)用 tokenRepository 實(shí)例中的 createNewToken 方法,tokenRepository 實(shí)際上就是我們一開始配置的 JdbcTokenRepositoryImpl,所以這行代碼實(shí)際上就是將 PersistentRememberMeToken 存入數(shù)據(jù)庫中。
  4. 最后 addCookie,大家可以看到,就是添加了 series 和 token。

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

  1. protected UserDetails processAutoLoginCookie(String[] cookieTokens, 
  2.   HttpServletRequest request, HttpServletResponse response) { 
  3.  final String presentedSeries = cookieTokens[0]; 
  4.  final String presentedToken = cookieTokens[1]; 
  5.  PersistentRememberMeToken token = tokenRepository 
  6.    .getTokenForSeries(presentedSeries); 
  7.  if (!presentedToken.equals(token.getTokenValue())) { 
  8.   tokenRepository.removeUserTokens(token.getUsername()); 
  9.   throw new CookieTheftException( 
  10.     messages.getMessage( 
  11.       "PersistentTokenBasedRememberMeServices.cookieStolen"
  12.       "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); 
  13.  } 
  14.  if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System 
  15.    .currentTimeMillis()) { 
  16.   throw new RememberMeAuthenticationException("Remember-me login has expired"); 
  17.  } 
  18.  PersistentRememberMeToken newToken = new PersistentRememberMeToken( 
  19.    token.getUsername(), token.getSeries(), generateTokenData(), new Date()); 
  20.  tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), 
  21.     newToken.getDate()); 
  22.  addCookie(newToken, request, response); 
  23.  return getUserDetailsService().loadUserByUsername(token.getUsername()); 

這段邏輯也比較簡單:

首先從前端傳來的 cookie 中解析出 series 和 token。

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

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

接下來校驗(yàn) token 是否過期。

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

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

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

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

2.二次校驗(yàn)

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

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

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

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

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

  1. @RestController 
  2. public class HelloController { 
  3.     @GetMapping("/hello"
  4.     public String hello() { 
  5.         return "hello"
  6.     } 
  7.     @GetMapping("/admin"
  8.     public String admin() { 
  9.         return "admin"
  10.     } 
  11.     @GetMapping("/rememberme"
  12.     public String rememberme() { 
  13.         return "rememberme"
  14.     } 
  1. 第一個(gè) /hello 接口,只要認(rèn)證后就可以訪問,無論是通過用戶名密碼認(rèn)證還是通過自動(dòng)登錄認(rèn)證,只要認(rèn)證了,就可以訪問。
  2. 第二個(gè) /admin 接口,必須要用戶名密碼認(rèn)證之后才能訪問,如果用戶是通過自動(dòng)登錄認(rèn)證的,則必須重新輸入用戶名密碼才能訪問該接口。
  3. 第三個(gè) /rememberme 接口,必須是通過自動(dòng)登錄認(rèn)證后才能訪問,如果用戶是通過用戶名/密碼認(rèn)證的,則無法訪問該接口。

好了,我們來看下接口的訪問要怎么配置:

  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests() 
  4.             .antMatchers("/rememberme").rememberMe() 
  5.             .antMatchers("/admin").fullyAuthenticated() 
  6.             .anyRequest().authenticated() 
  7.             .and() 
  8.             .formLogin() 
  9.             .and() 
  10.             .rememberMe() 
  11.             .key("javaboy"
  12.             .tokenRepository(jdbcTokenRepository()) 
  13.             .and() 
  14.             .csrf().disable(); 

可以看到:

  1. /rememberme 接口是需要 rememberMe 才能訪問。
  2. /admin 是需要 fullyAuthenticated,fullyAuthenticated 不同于 authenticated,fullyAuthenticated 不包含自動(dòng)登錄的形式,而 authenticated 包含自動(dòng)登錄的形式。
  3. 最后剩余的接口(/hello)都是 authenticated 就能訪問。

OK,配置完成后,重啟測試,測試過程我就不再贅述了。

好了,今天從兩個(gè)方面和小伙伴們分享了在 Spring Boot 自動(dòng)登錄中,如何降低系統(tǒng)風(fēng)險(xiǎn),感興趣的小伙伴趕快試一把吧~

 

責(zé)任編輯:武曉燕 來源: 江南一點(diǎn)雨
相關(guān)推薦

2023-06-09 14:01:56

2022-07-14 09:20:24

供應(yīng)鏈網(wǎng)絡(luò)攻擊

2019-03-29 15:11:13

2014-04-11 11:18:23

2011-08-30 14:57:41

2021-05-08 10:44:35

SpringSecur登錄詳情

2011-07-18 09:01:47

2010-11-29 09:49:59

2018-12-18 09:20:44

欺騙技術(shù)物聯(lián)網(wǎng)安全物聯(lián)網(wǎng)

2009-01-15 09:55:00

局域網(wǎng)安全風(fēng)險(xiǎn)

2019-03-17 16:18:39

影子物聯(lián)網(wǎng)物聯(lián)網(wǎng)IOT

2016-10-28 13:21:36

2010-09-26 09:57:41

2021-12-12 11:49:02

電子商務(wù)安全網(wǎng)絡(luò)犯罪

2021-08-31 16:05:19

數(shù)據(jù)安全數(shù)據(jù)風(fēng)險(xiǎn)網(wǎng)絡(luò)安全

2021-07-02 10:45:53

SpringBootCAS登錄

2022-05-11 10:21:47

物聯(lián)網(wǎng)安全網(wǎng)絡(luò)安全物聯(lián)網(wǎng)

2024-01-17 12:05:21

2009-11-30 11:40:52

2010-04-06 15:14:08

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號