配套视频:38.RememberMe简介_哔哩哔哩_bilibili

  • 简介

  • 基本使用

  • 原理分析

  • 持久化令牌

5.1 RememberMe简介

RememberMe (记住我、记住密码下次自动登录) 这个功能非常常见,下图就是QQ邮箱登录时的“记住我” 选项。

提到 RememberMe,一些初学者往往会有一些误解,认为RememberMe功能就是把用户名/密码用Cookie保存在浏览器中,下次登录时不用再次输入用户名/密码,这个理解显然是不对的。我们这里所说的 RememberMe是一种服务器端的行为,传统的登录方式基于 Session会话,一旦用户的会话超时过期(一般为会话时间为30分钟),就要再次登录,这样太过于烦琐。如果能有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多,RememberMe 就是为了解决这一需求而生的。

具体的实现思路就是通过 Cookie 来记录当前用户身份,当用户登录成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头带回前端存储在cookie中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie中的信息进行校验分析,进而确定出用户的身份,Cookie中所保存的用户信息也是有时效的,例如三天、一周等(时间越长,风险越大,没有绝对的安全)。

5.2 前期环境搭建

创建Spring Initializr项目spring-security-08,引入Spring Web、Spring Security依赖,新建config、controller包。

5.2.1 编写Security配置类

  • SecurityConfig.java

 package com.study.config;​import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.provisioning.InMemoryUserDetailsManager;​/*** @ClassName SecurityConfig* @Description TODO* @Author Jiangnan Cui* @Date 2022/8/27 20:07* @Version 1.0*/@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {//使用内存中的数据源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN").build());return inMemoryUserDetailsManager;}​//使用全局自定义配置AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}​//重写认证登录默认配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().csrf().disable();}}​

5.2.2 编写测试Controller

  • IndexController.java

 package com.study.controller;​import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;​/*** @ClassName IndexController* @Description TODO* @Author Jiangnan Cui* @Date 2022/8/27 20:05* @Version 1.0*/@RestControllerpublic class IndexController {@GetMapping("/index")public String index() {System.out.println("index is ok");return "Index is Ok";}}

5.2.3 测试

以debug方式启动项目,访问:http://localhost:8080/index ,输入用户名root、密码123进行等登录,服务器会话默认失效时间为30分钟,30分钟后需要再次登录,此处为演示此过程,将服务器端会话失效时间设置为1分钟,具体配置在application.properties中:

 # 修改服务器会话过期时间(单位:分钟)server.servlet.session.timeout=1

重启服务后,按照上述方式进行登录后,等待1分钟,刷新页面,发现此时需要再次登录,说明之前登录的用户名密码已经失效。

5.3 基本使用

配套视频:39.RememberMe的基本使用_哔哩哔哩_bilibili

5.3.1 开启RememberMe

SecurityConfig的configure方法添加rememberMe()即可开启RememberMe功能:

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {......//重写认证登录默认配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().rememberMe()//开启RememberMe功能,重启服务之后登录页面出现Remember me on this computer.选择框.and().csrf().disable();}}

       重启服务之后,访问:http://localhost:8080/index,发现登录页面中会多出一个 RememberMe 选项,勾选此选项后进行登录,登录成功后就不会在1分钟后过期了。

补充:

  • 自定义认证页面添加Remember功能:
 <input type='checkbox' name='remember-me'/> Remember me on this computer.

5.3.2 RememberMe原理分析

配套视频:40.RememberMe 原理分析_哔哩哔哩_bilibili

5.3.2.1 RememberMeAuthenticationFilter

在上图中我们在SecurityConfig配置中开启了"记住我"功能之后,在进行认证时如果勾选了"记住我"选项,此时打开浏览器控制台,进而分析整个登录过程。首先当我们登录时,在登录请求中多了一个 RememberMe 的参数。

很显然,这个参数就是告诉服务器应该开启 RememberMe功能的。如果自定义登录页面开启 RememberMe功能应该多加入一个一样的请求参数就可以啦,该请求会被 RememberMeAuthenticationFilter进行拦截然后自动登录具体参见源码:

具体过程如下:

(1)请求到达过滤器之后,首先判断 SecurityContextHolder中是否有值,没值的话表示用户尚未登录,此时调用 autoLogin方法进行自动登录。

(2)当自动登录成功后返回的rememberMeAuth不为null时,表示自动登录成功,此时调用authenticate方法对 key 进行校验,并且将登录成功的用户信息保存到 SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含 RememberMeServices 中的 loginSuccess 方法。

(3)如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调。onUnsuccessfulAuthentication 和 onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现,这就是 RememberMeAuthenticationFilter 过滤器所做的事情,成功将 RememberMeServices的服务集成进来。

5.3.2.2 RememberMeServices

 package org.springframework.security.web.authentication;​import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.security.core.Authentication;​public interface RememberMeServices {Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);​void loginFail(HttpServletRequest request, HttpServletResponse response);​void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);}

这里一共定义了三个方法:

  1. autoLogin 方法:可以从请求中提取出需要的参数,完成自动登录功能。

  2. loginFail 方法:自动登录失败的回调。

  3. 1oginSuccess 方法:自动登录成功的回调。

       RememberMeServices的实现类为AbstractRememberMeServices,AbstractRememberMeServices的子类为PersistentTokenBasedRememberMeServices和TokenBasedRememberMeServices,默认实现的是TokenBasedRememberMeServices:

5.3.2.3 TokenBasedRememberMeServices

在开启RememberMe后,如果没有加入额外配置,默认实现就是由TokenBasedRememberMeServices进行的实现。查看这个类源码中 processAutoLoginCookie 方法实现:

processAutoLoginCookie 方法主要用来验证 Cookie 中的令牌信息是否合法:

  1. 首先判断cookieTokens的长度是否为3,不为3时说明格式不对,直接抛出异常。

  2. 从cookieTokens 数组中提取出第 1项,也就是过期时间,判断令牌是否过期,如果己经过期,则拋出异常。

  3. 根据用户名 (cookieTokens 数组的第0项)查询出当前用户对象。

  4. 调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:首先将用户名、令牌过期时间、用户密码以及 key 组成一个宇符串,中间用“:”隔开,然后通过 MD5 消息摘要算法对该宇符串进行加密,并将加密结果转为一个字符串返回。

  5. 判断第4 步生成的签名和通过 Cookie 传来的签名是否相等(即 cookieTokens 数组的第2项),如果相等,表示令牌合法,则直接返回用户对象,否则拋出异常。

  1. 在这个回调中,首先获取用户经和密码信息,如果用户密码在用户登录成功后从successfulAuthentication对象中擦除,则从数据库中重新加载出用户密码。

  2. 计算出令牌的过期时间,令牌默认有效期是两周。

  3. 根据令牌的过期时间、用户名以及用户密码,计算出一个签名。

  4. 调用 setCookie 方法设置 Cookie, 第一个参数是一个数组,数组中一共包含三项:用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。

总结

当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名、令牌过期时间以及签名拼接成一个字符串,中间用“:” 隔开,对拼接好的字符串进行Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当会话过期之后,访问系统资源时会自动携带上Cookie中的令牌,服务端拿到 Cookie中的令牌后,先进行 Bae64解码,解码后分别提取出令牌中的三项数据;接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息;接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败。

以上过程中可以在浏览器的cookies中获得token信息,说明还不安全,下面会围绕此进行展开。

配套视频:41.RememberMe 原理分析图解_哔哩哔哩_bilibili

5.3.4 内存令牌

配套视频:42.Remember-Me 提高安全性_哔哩哔哩_bilibili

5.3.4.1 PersistentTokenBasedRememberMeServices

基于TokenBasedRememberMeServices生成的Cookie信息是固定的,容易被不法分子拦截,而基于PersistentTokenBasedRememberMeServices生成的Cookie信息是不断更新的,生成新的Cookie信息后,之前的Cookie信息会过期,不能再利用。

PersistentTokenBasedRememberMeServices源码:

 package org.springframework.security.web.authentication.rememberme;​import java.security.SecureRandom;import java.util.Arrays;import java.util.Base64;import java.util.Date;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.core.log.LogMessage;import org.springframework.security.core.Authentication;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.util.Assert;​public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();private SecureRandom random = new SecureRandom();public static final int DEFAULT_SERIES_LENGTH = 16;public static final int DEFAULT_TOKEN_LENGTH = 16;private int seriesLength = 16;private int tokenLength = 16;​public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService);this.tokenRepository = tokenRepository;}​protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {if (cookieTokens.length != 2) {throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");} else {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);} else 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."));} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {throw new RememberMeAuthenticationException("Remember-me login has expired");} else {this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());​try {this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());this.addCookie(newToken, request, response);} catch (Exception var9) {this.logger.error("Failed to update token: ", var9);throw new RememberMeAuthenticationException("Autologin failed due to data access problem");}​return this.getUserDetailsService().loadUserByUsername(token.getUsername());}}}​protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {String username = successfulAuthentication.getName();this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());​try {this.tokenRepository.createNewToken(persistentToken);this.addCookie(persistentToken, request, response);} catch (Exception var7) {this.logger.error("Failed to save persistent token ", var7);}​}​public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {super.logout(request, response, authentication);if (authentication != null) {this.tokenRepository.removeUserTokens(authentication.getName());}​}​protected String generateSeriesData() {byte[] newSeries = new byte[this.seriesLength];this.random.nextBytes(newSeries);return new String(Base64.getEncoder().encode(newSeries));}​protected String generateTokenData() {byte[] newToken = new byte[this.tokenLength];this.random.nextBytes(newToken);return new String(Base64.getEncoder().encode(newToken));}​private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);}​public void setSeriesLength(int seriesLength) {this.seriesLength = seriesLength;}​public void setTokenLength(int tokenLength) {this.tokenLength = tokenLength;}​public void setTokenValiditySeconds(int tokenValiditySeconds) {Assert.isTrue(tokenValiditySeconds > 0, "tokenValiditySeconds must be positive for this implementation");super.setTokenValiditySeconds(tokenValiditySeconds);}}

源码分析:

  1. 不同于 TokonBasedRemornberMeServices 中的 processAutologinCookie 方法,这里cookieTokens 数组的长度为2,第一项是series,第二项是 token。

  2. 从cookieTokens数组中分到提取出 series和token,然后根据 series 去内存中查询出一个 PersistentRememberMeToken对象。如果查询出来的对象为null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的 token 和从 cookieTokens 中解析出来的token不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。

  3. 根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。

  4. 生成一个新的 PersistentRememberMeToken 对象,用户名和series 不变,token 重新生成,date 也使用当前时间。newToken生成后,根据 series 去修改内存中的 token和 date(即每次自动登录后都会产生新的 token和 date)。

  5. 调用 addCookie 方法添加 Cookie, 在addCookie 方法中,会调用到我们前面所说的setCookie 方法,但是要注意第一个数组参数中只有两项:series 和 token(即返回到前端的令牌是通过对 series 和 token 进行 Base64 编码得到的)。

  6. 最后将根据用户名查询用户对象并返回。

5.3.4.2 内存令牌具体实现

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {......//重写认证登录默认配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//.mvcMatchers("/index").rememberMe()//指定资源开启记住我功能,其它不开启,需要认证.anyRequest().authenticated().and().formLogin().and().rememberMe()//开启RememberMe功能,重启服务之后登录页面出现Remember me on this computer.选择框//.rememberMeParameter("RememberMe")//修改RememberMe名称.rememberMeServices(rememberMeServices()).and().csrf().disable();}​//使用PersistentTokenBasedRememberMeServices更新Cookie,提高安全性@Beanpublic RememberMeServices rememberMeServices() {return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());}}​

重新启动服务,访问:http://localhost:8080/index ,输入root、123进行登录。等待1分钟后在进行刷新,发现两次得到的Cookie信息不一样,说明Cookie在会话过期后会更新,且前后Cookie信息不一致,相对地提高了安全性。

(1)登录成功时

(2)1分钟会话过期后,刷新页面

 由于以上令牌都是保存在内存中的,内存中的令牌在应用程序重启之后,即使之前做过“记住我”操作,之后也无法再实现记住我功能。

5.3.5 持久化令牌(基于数据库实现)

配套视频:43.Remember-Me 令牌数据库的持久化_哔哩哔哩_bilibili

基于数据库实现持久化令牌操作时要用到PersistentTokenRepository,源码如下:

 package org.springframework.security.web.authentication.rememberme;​import java.util.Date;​public interface PersistentTokenRepository {void createNewToken(PersistentRememberMeToken token);​void updateToken(String series, String tokenValue, Date lastUsed);​PersistentRememberMeToken getTokenForSeries(String seriesId);​void removeUserTokens(String username);}​

该接口主要实现类为InMemoryTokenRepositoryImpl(基于内存)和JdbcTokenRepositoryImpl(基于数据库),接下来主要使用JdbcTokenRepositoryImpl实现代替InMemoryTokenRepositoryImpl,JdbcTokenRepositoryImpl源码如下:

 package org.springframework.security.web.authentication.rememberme;​import java.sql.ResultSet;import java.sql.SQLException;import java.util.Date;import org.springframework.core.log.LogMessage;import org.springframework.dao.DataAccessException;import org.springframework.dao.EmptyResultDataAccessException;import org.springframework.dao.IncorrectResultSizeDataAccessException;import org.springframework.jdbc.core.support.JdbcDaoSupport;​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 = ?";private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";private String removeUserTokensSql = "delete from persistent_logins where username = ?";private boolean createTableOnStartup;​public JdbcTokenRepositoryImpl() {}​protected void initDao() {if (this.createTableOnStartup) {this.getJdbcTemplate().execute("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 void createNewToken(PersistentRememberMeToken token) {this.getJdbcTemplate().update(this.insertTokenSql, new Object[]{token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()});}​public void updateToken(String series, String tokenValue, Date lastUsed) {this.getJdbcTemplate().update(this.updateTokenSql, new Object[]{tokenValue, lastUsed, series});}​public PersistentRememberMeToken getTokenForSeries(String seriesId) {try {return (PersistentRememberMeToken)this.getJdbcTemplate().queryForObject(this.tokensBySeriesSql, this::createRememberMeToken, new Object[]{seriesId});} catch (EmptyResultDataAccessException var3) {this.logger.debug(LogMessage.format("Querying token for series '%s' returned no results.", seriesId), var3);} catch (IncorrectResultSizeDataAccessException var4) {this.logger.error(LogMessage.format("Querying token for series '%s' returned more than one value. Series should be unique", seriesId));} catch (DataAccessException var5) {this.logger.error("Failed to load token for series " + seriesId, var5);}​return null;}​private PersistentRememberMeToken createRememberMeToken(ResultSet rs, int rowNum) throws SQLException {return new PersistentRememberMeToken(rs.getString(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4));}​public void removeUserTokens(String username) {this.getJdbcTemplate().update(this.removeUserTokensSql, new Object[]{username});}​public void setCreateTableOnStartup(boolean createTableOnStartup) {this.createTableOnStartup = createTableOnStartup;}}​

       在该实现类中自动定义了表(persistent_logins)及其表结构,后续连接数据库后会创建此表进行Token信息记录。

5.3.5.1 pom.xml引入依赖

 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</version></dependency>​<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version></dependency>​<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency>

5.3.5.2 配置数据源

 spring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=rootmybatis.mapper-locations=classpath:com/study/mapper/*.xmlmybatis.type-aliases-package=com.study.entity

5.3.5.3 配置持久化令牌

  • 方式1:使用RememberMeServices

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {//注入数据源private final DataSource dataSource;​@Autowiredpublic SecurityConfig(DataSource dataSource) {this.dataSource = dataSource;}​//使用内存中的数据源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN").build());return inMemoryUserDetailsManager;}​//使用全局自定义配置AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}​//重写认证登录默认配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//.mvcMatchers("/index").rememberMe()//指定资源开启记住我功能,其它不开启,需要认证.anyRequest().authenticated().and().formLogin().and().rememberMe()//开启RememberMe功能,重启服务之后登录页面出现Remember me on this computer.选择框//.rememberMeParameter("RememberMe")//修改RememberMe名称.rememberMeServices(rememberMeServices()).and().csrf().disable();}​​//使用PersistentTokenBasedRememberMeServices更新Cookie,提高安全性//方式1:@Beanpublic RememberMeServices rememberMeServices() {//return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());//基于数据库实现,使用JdbcTokenRepositoryJdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();//指定数据源jdbcTokenRepository.setDataSource(dataSource);//使用rememberMeServices时第一次需要手动创建表结构,数据库直接使用security即可,启动服务进行登录后,会存储此次登录认证信息//jdbcTokenRepository.setCreateTableOnStartup(true);/*** create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)*/return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), jdbcTokenRepository);}}​
  • 方式2:直接指定tokenRepository

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {//注入数据源private final DataSource dataSource;​@Autowiredpublic SecurityConfig(DataSource dataSource) {this.dataSource = dataSource;}​//使用内存中的数据源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN").build());return inMemoryUserDetailsManager;}​//使用全局自定义配置AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}​//重写认证登录默认配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//.mvcMatchers("/index").rememberMe()//指定资源开启记住我功能,其它不开启,需要认证.anyRequest().authenticated().and().formLogin().and().rememberMe()//开启RememberMe功能,重启服务之后登录页面出现Remember me on this computer.选择框.tokenRepository(persistentTokenRepository())//方式2//.rememberMeParameter("RememberMe")//修改RememberMe名称//.rememberMeServices(rememberMeServices())//方式1//.alwaysRemember(true)//总是记住我.and().csrf().disable();}//方式2:指定数据库持久化@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);jdbcTokenRepository.setCreateTableOnStartup(false);//第一次新建表结构时需要设置为true,第二次之后表已经存在需要设置为false,需要手动改一下return jdbcTokenRepository;}//使用PersistentTokenBasedRememberMeServices更新Cookie,提高安全性//方式1://    @Bean//    public RememberMeServices rememberMeServices() {//        //return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());//        //基于数据库实现,使用JdbcTokenRepository//        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();//        //指定数据源//        jdbcTokenRepository.setDataSource(dataSource);//        //使用rememberMeServices时第一次需要手动创建表结构,数据库直接使用security即可,启动服务进行登录后,会存储此次登录认证信息//        //jdbcTokenRepository.setCreateTableOnStartup(true);//        /**//         * create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)//         *///        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), jdbcTokenRepository);//    }}​

5.3.5.4 启动项目登录+查看数据库

注意:启动项目后会自动在数据库中创建一个表persistent_logins,用来保存记住我的token信息

再次测试记住我:在测试发现即使服务器重新启动,依然可以自动登录。

5.3.6 自定义记住我

配套视频:44.传统web 开发自定义记住我功能_哔哩哔哩_bilibili

5.3.6.1 查看记住我源码

AbstractUserDetailsAuthenticationProvider类中authenticate方法在最后认证成功之后实现了记住我功能,但是查看源码得知,如果开启记住我,必须进行相关的设置 :

successfulAuthentication方法: 

loginSuccess方法: 

rememberMeRequested方法:

5.3.6.2 传统 web 开发记住我实现

通过源码分析得知,必须在认证请求中加入参数remember-me值为"true,on,yes,1"其中任意一个才可以完成记住我功能,这个时候修改认证界面:

  • 引入Thymeleaf依赖,配置Thymeleaf,新建登录页面引入remember-me

 <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>登录</title></head><body><h1>用户登录</h1><form method="post" th:action="@{/doLogin}">用户名:<input name="uname" type="text"/><br>密码:<input name="passwd" type="password"/><br>记住我: <input type="checkbox" name="remember-me" value="on|yes|true|1"/><br><input type="submit" value="登录"/></form></body></html>
  • SecurityConfig配置中开启记住我

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()......and().rememberMe() //开启记住我//.alwaysRemember(true) 总是记住我.and().csrf().disable();}}

5.3.6.3 前后端分离开发记住我实现

配套视频:45.前后端分离开发记住我实现_哔哩哔哩_bilibili

  • 自定义认证类 LoginFilter

 /*** 自定义前后端分离认证 Filter*/public class LoginFilter extends UsernamePasswordAuthenticationFilter {​@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {System.out.println("========================================");//1.判断是否是 post 方式请求if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//2.判断是否是 json 格式请求类型if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {//3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx","remember-me":true}try {Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String username = userInfo.get(getUsernameParameter());String password = userInfo.get(getPasswordParameter());String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);if (!ObjectUtils.isEmpty(rememberValue)) {request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);}System.out.println("用户名: " + username + " 密码: " + password + " 是否记住我: " + rememberValue);UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);} catch (IOException e) {e.printStackTrace();}}return super.attemptAuthentication(request, response);}}
  • 自定义 RememberMeService

 /*** 自定义记住我 services 实现类*/public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService, tokenRepository);}/*** 自定义前后端分离获取 remember-me 方式*/@Overrideprotected boolean rememberMeRequested(HttpServletRequest request, String parameter) {String paramValue = request.getAttribute(parameter).toString();if (paramValue != null) {if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {return true;}}return false;}}
  • 配置记住我SecurityConfig

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic UserDetailsService userDetailsService() {//.....return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}​@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}​//自定义 filter 交给工厂管理@Beanpublic LoginFilter loginFilter() throws Exception {LoginFilter loginFilter = new LoginFilter();loginFilter.setFilterProcessesUrl("/doLogin");//指定认证 urlloginFilter.setUsernameParameter("uname");//指定接收json 用户名 keyloginFilter.setPasswordParameter("passwd");//指定接收 json 密码 keyloginFilter.setAuthenticationManager(authenticationManagerBean());loginFilter.setRememberMeServices(rememberMeServices()); //设置认证成功时使用自定义rememberMeService//认证成功处理loginFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录成功");result.put("用户信息", authentication.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});//认证失败处理loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录失败: " + ex.getMessage());resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());resp.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});return loginFilter;}​@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated()//所有请求必须认证.and().formLogin().and().rememberMe() //开启记住我功能  cookie 进行实现  1.认证成功保存记住我 cookie 到客户端   2.只有 cookie 写入客户端成功才能实现自动登录功能.rememberMeServices(rememberMeServices())  //设置自动登录使用哪个 rememberMeServices.and().exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("请认证之后再去处理!");}).and().logout().logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),new AntPathRequestMatcher("/logout", HttpMethod.GET.name()))).logoutSuccessHandler((req, resp, auth) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "注销成功");result.put("用户信息", auth.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);}).and().csrf().disable();​​// at: 用来某个 filter 替换过滤器链中哪个 filter// before: 放在过滤器链中哪个 filter 之前// after: 放在过滤器链中那个 filter 之后http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);}​@Beanpublic RememberMeServices rememberMeServices() {return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());}}

前后端分离记住我实现有待完善!!!

【编程不良人】SpringSecurity实战学习笔记04---RememberMe相关推荐

  1. 【编程不良人】SpringSecurity实战学习笔记07---授权

    配套视频:61.授权之授权核心概念_哔哩哔哩_bilibili 什么是权限管理? 权限管理核心概念 Spring Security权限管理策略 基于URL地址方式实现的权限管理 基于方法实现的权限管理 ...

  2. 【编程不良人】MongoDB最新实战教程学习笔记

    简介 视频链接:01.简介和历史_哔哩哔哩_bilibili 文档地址: https://docs.mongodb.com/manual/ MongoDB教程:MongoDB 教程 | 菜鸟教程 注意 ...

  3. Hadoop 从入门到精通----leo学习编程不良人视频的笔记--part01

    编程不良人原版笔记 - https://blog.csdn.net/wei198621/article/details/111280555 part 01 hadoop 集群的搭建 – https:/ ...

  4. jwt实战详解--B站编程不良人视频笔记

    文章目录 前言 一.什么是JWT 二.JWT能做什么 1.授权 2.信息交换 三.为什么使用JWT 四.JWT的结构是什么 五.使用JWT 1.引入依赖 2.生成token 3.根据令牌和签名解析数据 ...

  5. 【编程不良人】快速入门Spring学习笔记08---事务属性、Spring整合Structs2框架(SM)、Spring整合Mybatis+Struts2(SSM)、Spring注解、SSM注解式开发

    1. 事务属性 1.1 事务传播属性 配套视频:[编程不良人]快速入门Spring,SpringBoot.SpringCloud学不好完全是因为Spring没有掌握!_哔哩哔哩_bilibili # ...

  6. 【编程不良人】快速入门SpringBoot学习笔记06---RestFul、异常处理、CORS跨域、Jasypt加密

    1. RestFul 配套视频:[编程不良人]2021年SpringBoot最新最全教程_哔哩哔哩_bilibili 1.1 引言 REST全称是(Resources) Representationa ...

  7. Hadoop 从入门到精通----编程不良人笔记

    编程不良人原版笔记 - https://blog.csdn.net/wei198621/article/details/111280555 part 01 hadoop 集群的搭建 – https:/ ...

  8. Linux性能优化实战学习笔记:第十讲==中断

    Linux性能优化实战学习笔记:第十讲 一.坏境准备 1.拓扑图 2.安装包 在第9节的基础上 在VM2上安装hping3依奈包 ? 1 2 3 4 5 6 7 wget http://www.tcp ...

  9. java后验条件_JAVA并发实战学习笔记——3,4章~

    JAVA并发实战学习笔记 第三章 对象的共享 失效数据: java程序实际运行中会出现①程序执行顺序对打乱:②数据对其它线程不可见--两种情况 上述两种情况导致在缺乏同步的程序中出现失效数据这一现象, ...

  10. MySQL学习笔记04【数据库的查询操作、今日内容、表的约束】

    MySQL 文档-黑马程序员(腾讯微云):https://share.weiyun.com/RaCdIwas 1-MySQL基础.pdf.2-MySQL约束与设计.pdf.3-MySQL多表查询与事务 ...

最新文章

  1. 巧用Windows 7命令,修复系统故障!
  2. hdu 1116 Play on Words
  3. Java异常机制及异常处理建议
  4. 解决 Visual Studio 2017 RC 不兼容低版本 Visual Studio 创建的 MVC 4 项目的问题
  5. 计算机科目三教学设计,信息技术-教学设计模板(科目三).pdf
  6. 使用DBUtils编写通用的DAO
  7. 交换机用python定时备份
  8. SASS+Compass基本使用,结合JavaScript实现随机点名小系统
  9. 后处理安装_核燃料后处理设施中的热室
  10. go oracle编程,go基础编程(一):第一个go程序-hello word
  11. 这就是数据分析之算法认知
  12. Linux内核进程管理:进程的“内核栈”、current宏、进程描述符
  13. 使用java语言操作,如何来实现MySQL中Blob字段的存取
  14. c语言中rm无法删除文件夹,rm:无法删除'-rf':没有这样的文件或目录
  15. htmlh1 h6,HTML 5 h1 至 h6 标签 - HTML 参考手册
  16. 【v1.4.4】H5匿名信一封来信更新公告,新版升级教程
  17. Linux新手需要熟稔于心的Linux常用命令
  18. 同样是大逃杀类影片,《鱿鱼游戏》凭什么能火遍全球?
  19. 奉劝学弟学妹,学完JavaScript就该学TypeScript了,让我们一起了解TypeScript和如何去搭建运行环境吧
  20. 微信公众号免扫码登录的方法

热门文章

  1. [Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content.
  2. The client-side rendered virtual DOM tree is not matching server-rendered content. 问题
  3. pip20恢复官方源出现bug:confirming the ssl certificate: HTTPSConnectionPool(host=‘pypi.org‘, port=443)
  4. 高频leetcode数学部分:679. 24 点游戏
  5. PC常见故障及解决思路汇总(网络方面)
  6. matlibplot之条形图
  7. mysql连接超时的原因_数据库提示连接超时是什么原因?
  8. Linux_2022/7/19_Day1
  9. 配置babel-plugin-import报错的深坑
  10. 浅谈大数据时代web数据可视化探析