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

基礎(chǔ)-進(jìn)階-升級(jí)!圖解SpringSecurity的RememberMe流程

開發(fā) 前端
首先要注意的就是“記住我”勾選框參數(shù)名必須為“remember-me”。如果你想自定義的話也是可以的,需要將自定義的名字例如:remember-me-new 配置到配置類中。

前言

之前我已經(jīng)寫過好幾篇權(quán)限認(rèn)證相關(guān)的文章了,有想復(fù)習(xí)的同學(xué)可以查看【身份權(quán)限認(rèn)證合集】。今天我們來聊一下登陸頁面中“記住我”這個(gè)看似簡(jiǎn)單實(shí)則復(fù)雜的小功能。

如圖就是博客園登陸時(shí)的“記住我”選項(xiàng),在實(shí)際開發(fā)登陸接口以前,我一直認(rèn)為這個(gè)“記住我”就是把我的用戶名和密碼保存到瀏覽器的 cookie 中,當(dāng)下次登陸時(shí)瀏覽器會(huì)自動(dòng)顯示我的用戶名和密碼,就不用我再次輸入了。

圖片

直到我看了 Spring Security 中 Remember Me 相關(guān)的源碼,我才意識(shí)到之前的理解全錯(cuò)了,它的作用其實(shí)是讓用戶在關(guān)閉瀏覽器之后再次訪問時(shí)不需要重新登陸。

原理

如果用戶勾選了 “記住我” 選項(xiàng),Spring Security 將在用戶登錄時(shí)創(chuàng)建一個(gè)持久的安全令牌,并將令牌存儲(chǔ)在 cookie 中或者數(shù)據(jù)庫中。當(dāng)用戶關(guān)閉瀏覽器并再次打開時(shí),Spring Security 可以根據(jù)該令牌自動(dòng)驗(yàn)證用戶身份。

先來張圖感受下,然后跟著阿Q從簡(jiǎn)單的Spring Security 登陸樣例開始慢慢搭建吧!

圖片

基礎(chǔ)版

搭建

初始化sql

//用戶表
CREATE TABLE `sys_user_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
//插入用戶數(shù)據(jù)
INSERT INTO sys_user_info
(id, username, password)
VALUES(1, 'cheetah', '$2a$10$N.zJIQtKLyFe62/.wL17Oue4YFXUYmbWICsMiB7c0Q.sF/yMn5i3q');

//產(chǎn)品表
CREATE TABLE `product_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `price` decimal(10,4) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
//插入產(chǎn)品數(shù)據(jù)
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(1, '從你的全世界路過', 32.0000, '2020-11-21 21:26:12', '2021-03-27 22:17:39');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(2, '喬布斯傳', 25.0000, '2020-11-21 21:26:42', '2021-03-27 22:17:42');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(3, 'java開發(fā)', 87.0000, '2021-03-27 22:43:31', '2021-03-27 22:43:34');

依賴引入

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置類

自定義 SecurityConfig 類繼承 WebSecurityConfigurerAdapter 類,并實(shí)現(xiàn)里邊的 configure(HttpSecurity httpSecurity)方法。

/**
  * 安全認(rèn)證及授權(quán)規(guī)則配置
  **/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   .authorizeRequests()
   .anyRequest()
   //除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
   .authenticated()
   .and()
   //登陸成功之后的跳轉(zhuǎn)頁面
   .formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
   .and()
   //CSRF禁用
   .csrf().disable();
}

另外還需要指定認(rèn)證對(duì)象的來源和密碼加密方式

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoder());
}

@Bean
public BCryptPasswordEncoder passwordEncoder(){
 return new BCryptPasswordEncoder();
}

驗(yàn)證

啟動(dòng)程序,瀏覽器打開http://127.0.0.1:8080/login

圖片

輸入用戶名密碼登陸成功

圖片

我們就可以拿著 JSESSIONID 去請(qǐng)求需要登陸的資源了。

源碼分析

方框中的是類和方法名,方框外是類中的方法具體執(zhí)行到的代碼。

圖片

首先會(huì)按照?qǐng)D中箭頭的方向來執(zhí)行,最終會(huì)執(zhí)行到我們自定義的實(shí)現(xiàn)了 UserDetailsService 接口的 UserInfoServiceImpl 類中的查詢用戶的方法 loadUserByUsername()。

該流程如果不清楚的話記得復(fù)習(xí)《實(shí)戰(zhàn)篇:Security+JWT組合拳 | 附源碼》

當(dāng)認(rèn)證通過之后會(huì)在SecurityContext中設(shè)置Authentication對(duì)象

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication中的方法SecurityContextHolder.getContext().setAuthentication(authResult);

最后調(diào)用onAuthenticationSuccess方法跳轉(zhuǎn)鏈接。

圖片

進(jìn)階版

集成

接下來我們就要開始進(jìn)入正題了,快速接入“記住我”功能。

在配置類 SecurityConfig 的 configure() 方法中加入兩行代碼,如下所示

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   .authorizeRequests()
   .anyRequest()
   //除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
   .authenticated()
   .and()
   //開啟 rememberMe 功能
   .rememberMe()
   .and()
   //登陸成功之后的跳轉(zhuǎn)頁面
   .formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
   .and()
   //CSRF禁用
   .csrf().disable();
}

重啟應(yīng)用頁面上會(huì)出現(xiàn)單選框“Remember me on this computer”

圖片

可以查看下頁面的屬性,該單選框的名字為“remember-me”

圖片

點(diǎn)擊登陸,在 cookie 中會(huì)出現(xiàn)一個(gè)屬性為 remember-me 的值,在以后的每次發(fā)送請(qǐng)求都會(huì)攜帶這個(gè)值到后臺(tái)

圖片

然后我們直接輸入http://127.0.0.1:8080/productInfo/getProductList獲取產(chǎn)品信息

圖片

當(dāng)我們把 cookie 中的 JSESSIONID 刪除之后重新獲取產(chǎn)品信息,發(fā)現(xiàn)會(huì)生成一個(gè)新的 JSESSIONID。

源碼分析

認(rèn)證通過的流程和基礎(chǔ)版本一致,我們著重來分析身份認(rèn)證通過之后,跳轉(zhuǎn)鏈接之前的邏輯。

圖片

疑問1

圖中1處為啥是 AbstractRememberMeServices 類呢?

我們發(fā)現(xiàn)在項(xiàng)目啟動(dòng)時(shí),在類 AbstractAuthenticationFilterConfigurer 的 configure() 方法中有如下代碼

RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
 this.authFilter.setRememberMeServices(rememberMeServices);
}

AbstractRememberMeServices 類型就是在此處設(shè)置完成的,是不是一目了然了?

疑問2

當(dāng)代碼執(zhí)行到圖中2和3處時(shí)

@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                               Authentication successfulAuthentication) {
    if (!rememberMeRequested(request, this.parameter)) {
        this.logger.debug("Remember-me login not requested.");
        return;
    }
    onLoginSuccess(request, response, successfulAuthentication);
}

因?yàn)槲覀児催x了“記住我”,所以此時(shí)的值為“on”,即rememberMeRequested(request, this.parameter)返回 true,然后加非返回 false,最后一步就是設(shè)置 cookie 的值。

鑒權(quán)

此處的講解一定要對(duì)照著代碼來看,要不然很容易錯(cuò)位,沒有類標(biāo)記的方法都屬于RememberMeAuthenticationFilter#doFilter

當(dāng)直接調(diào)用http://127.0.0.1:8080/productInfo/index接口時(shí),會(huì)走RememberMeAuthenticationFilter#doFilter的代碼

//此處存放的是登陸的用戶信息,可以理解為對(duì)應(yīng)的cookie中的 JSESSIONID 
if (SecurityContextHolder.getContext().getAuthentication() != null) {
 this.logger.debug(LogMessage
   .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
     + SecurityContextHolder.getContext().getAuthentication() + "'"));
 chain.doFilter(request, response);
 return;
}

因?yàn)镾ecurityContextHolder.getContext().getAuthentication()中有用戶信息,所以直接返回商品信息。

當(dāng)刪掉 JSESSIONID 后重新發(fā)起請(qǐng)求,發(fā)現(xiàn)SecurityContextHolder.getContext().getAuthentication()為 null ,即用戶未登錄,會(huì)往下走Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);代碼,即自動(dòng)登陸的邏輯

@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    //該方法的this.cookieName 的值為"remember-me",所以該處返回的是 cookie中remember-me的值
 String rememberMeCookie = extractRememberMeCookie(request);
 if (rememberMeCookie == null) {
  return null;
 }
 this.logger.debug("Remember-me cookie detected");
 if (rememberMeCookie.length() == 0) {
  this.logger.debug("Cookie was empty");
  cancelCookie(request, response);
  return null;
 }
 try {
        //對(duì)rememberMeCookie進(jìn)行解碼:
  String[] cookieTokens = decodeCookie(rememberMeCookie);
        //重點(diǎn):執(zhí)行TokenBasedRememberMeServices#processAutoLoginCookie下的 UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
        //就又回到我們自定義的 UserInfoServiceImpl 類中執(zhí)行代碼,返回user
  UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
  this.userDetailsChecker.check(user);
  this.logger.debug("Remember-me cookie accepted");
  return createSuccessfulAuthentication(request, user);
 }
 catch (CookieTheftException ex) {
  cancelCookie(request, response);
  throw ex;
 }
 catch (UsernameNotFoundException ex) {
  this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
 }
 catch (InvalidCookieException ex) {
  this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
 }
 catch (AccountStatusException ex) {
  this.logger.debug("Invalid UserDetails: " + ex.getMessage());
 }
 catch (RememberMeAuthenticationException ex) {
  this.logger.debug(ex.getMessage());
 }
 cancelCookie(request, response);
 return null;
}

執(zhí)行完之后接著執(zhí)行RememberMeAuthenticationFilter#doFilter中的rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);

當(dāng)執(zhí)行到ProviderManager#authenticate中的result = provider.authenticate(authentication);時(shí),會(huì)走RememberMeAuthenticationProvider 中的方法返回 Authentication 對(duì)象。

SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);將登錄成功信息保存到 SecurityContextHolder 對(duì)象中,然后返回商品信息。

升級(jí)版

如果記錄在服務(wù)器 session 中的 token 因?yàn)榉?wù)重啟而失效,就會(huì)導(dǎo)致前端用戶明明勾選了“記住我”的功能,但是仍然提示需要登陸。

這就需要我們對(duì) session 中的 token 做持久化處理,接下來我們就對(duì)他進(jìn)行升級(jí)。

集成

初始化sql

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL COMMENT '用戶名',
  `series` varchar(64) NOT NULL COMMENT '主鍵',
  `token` varchar(64) NOT NULL COMMENT 'token',
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次使用的時(shí)間',
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

不要問我為啥這樣創(chuàng)建表,我會(huì)在下邊告訴你??

配置類

//在SecurityConfig的configure方法中增加一行
.rememberMe().tokenRepository(persistentTokenRepository());
    
//引入依賴,注入bean
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository(){
 JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
 tokenRepository.setDataSource(dataSource);
 return tokenRepository;
}

驗(yàn)證

重啟項(xiàng)目,訪問http://127.0.0.1:8080/login之后返回?cái)?shù)據(jù),查看表中數(shù)據(jù),完美。

圖片

源碼分析

前邊的流程和升級(jí)版是相同的,區(qū)別就在于創(chuàng)建 token 之后是保存到 session 中還是持久化到數(shù)據(jù)庫中,接下來我們從源碼分析一波。

定位到AbstractRememberMeServices#loginSuccess中的 onLoginSuccess()方法,實(shí)際執(zhí)行的是PersistentTokenBasedRememberMeServices#onLoginSuccess方法。

/**
 * 使用新的序列號(hào)創(chuàng)建新的永久登錄令牌,并將數(shù)據(jù)存儲(chǔ)在
 * 持久令牌存儲(chǔ)庫,并將相應(yīng)的 cookie 添加到響應(yīng)中。
 *
 */
@Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
  Authentication successfulAuthentication) {
 ......
 try {
        //重點(diǎn)代碼創(chuàng)建token并保存到數(shù)據(jù)庫中
  this.tokenRepository.createNewToken(persistentToken);
  addCookie(persistentToken, request, response);
 }
 ......
}

因?yàn)槲覀冊(cè)谂渲妙愔卸x的是JdbcTokenRepositoryImpl,所以進(jìn)入改類的createNewToken方法。

@Override
public void createNewToken(PersistentRememberMeToken token) {
 getJdbcTemplate().update(this.insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(),
   token.getDate());
}

此時(shí)我們發(fā)現(xiàn)他就是做了插入數(shù)據(jù)庫的操作,并且this.insertTokenSql為

insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)

同時(shí)我們看到了熟悉的建表語句

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
   + "token varchar(64) not null, last_used timestamp not null)

這樣是不是就決解了上邊的疑惑了呢。

執(zhí)行完P(guān)ersistentTokenBasedRememberMeServices#onLoginSuccess方法之后又進(jìn)入到RememberMeAuthenticationFilter#doFilter()方法中結(jié)束。

有了持久化之后就不用擔(dān)心服務(wù)重啟了,接著我們重啟服務(wù),繼續(xù)訪問獲取商品接口,成功返回商品信息。

鑒權(quán)

鑒權(quán)的邏輯也是和進(jìn)階版相似的,區(qū)別在于刪除瀏覽器的 JSESSIONID 之后的邏輯。

定位到AbstractRememberMeServices#autoLogin中的UserDetails user = processAutoLoginCookie(cookieTokens, request, response);執(zhí)行的是PersistentTokenBasedRememberMeServices#processAutoLoginCookie。

//刪減版代碼
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
  HttpServletResponse response) {
 ......
 String presentedSeries = cookieTokens[0];
 String presentedToken = cookieTokens[1];
 PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
 if (token == null) {
  throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
 }
 if (!presentedToken.equals(token.getTokenValue())) {
  this.tokenRepository.removeUserTokens(token.getUsername());
  throw new CookieTheftException(this.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());
 try {
  this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
  addCookie(newToken, request, response);
 }
 ......
 return getUserDetailsService().loadUserByUsername(token.getUsername());
}

流程

  • 通過數(shù)據(jù)庫中的 series 字段找到對(duì)應(yīng)的記錄;
  • 記錄是否為空判斷以及記錄中的 token 是否和傳入的相同;
  • 記錄中的 last_used 加上默認(rèn)的兩周后是否大于當(dāng)前時(shí)間,即是否 token 失效;
  • 更新該記錄并將新生成的 token 放到 cookie 中;

后續(xù)的邏輯和進(jìn)階版一致。

擴(kuò)展版

看到這有的小伙伴肯定會(huì)問了,如果我不用默認(rèn)的登錄頁面,想用自己的登錄頁需要注意些什么呢?

首先要注意的就是“記住我”勾選框參數(shù)名必須為“remember-me”。如果你想自定義的話也是可以的,需要將自定義的名字例如:remember-me-new 配置到配置類中。

.rememberMe().rememberMeParameter("remember-me-new")

token 的有效期也是可以自定義的,例如設(shè)置有效期為2天

.rememberMe().tokenValiditySeconds(2*24*60*60)

我們還可以自定義保存在瀏覽器中的 cookie 的名稱

.rememberMe().rememberMeCookieName("remember-me-cookie")

責(zé)任編輯:武曉燕 來源: 阿Q說代碼
相關(guān)推薦

2021-04-21 10:38:44

Spring Boot RememberMe安全

2022-06-07 10:40:56

流程數(shù)據(jù)庫MySQL

2009-06-04 15:51:46

Struts流程圖

2017-03-03 09:10:41

2019-02-26 14:33:22

JVM內(nèi)存虛擬機(jī)

2021-09-06 06:45:06

WebpackMindMasterEntry

2010-05-06 16:20:33

eigrp負(fù)載均衡

2024-09-05 08:28:25

2024-08-23 16:04:45

2011-11-24 09:16:32

虛擬化vmwareVMware Work

2010-05-06 09:57:45

RHEL 5.5升級(jí)

2022-11-26 00:00:02

2022-03-06 12:15:38

NettyReactor線程

2011-03-29 14:11:20

Cacti基礎(chǔ)知識(shí)

2018-01-02 09:17:24

機(jī)器學(xué)習(xí)廣告推薦系統(tǒng)

2010-06-11 17:34:37

UML對(duì)象圖

2011-10-08 10:43:06

軟件工程

2022-08-28 20:34:42

LinuxLinux Mint

2011-03-08 10:40:42

IE9正式版

2010-03-23 13:09:11

升級(jí)內(nèi)置無線網(wǎng)卡
點(diǎn)贊
收藏

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