八、RememberMe


简介

RememberMe 这个功能非常常见,无论是在 QQ、邮箱…都有这个选项。提到 RememberMe,往往会有一些误解,认为 RememberMe 功能就是把 用户名/密码 用 Cookie 保存在浏览器中,下次登陆时不用再次输入 用户名/密码。这个理解显然是不对的。我们这里所说的 RememberMe 是一种服务器端的行为。传统的登录方式基于 Session 会话,一旦用户的会话超时过期,就要再次登录,这样太过于繁琐。如果有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多。RememberMe 就是为了解决这一需求而生

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

8.1 基本使用

开启记住我

8.2 原理分析

RememberMeAuthenticationFilter

从上图中,当在 SecurityConfig 配置中开启了"记住我"功能之后,在进行认证时如果勾选了"记住我"选项,此时打开浏览器控制台,查看network 中的请求头信息。首先我们登陆时,在登陆请求中多了一个 remember-me 的参数

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

记住我: <input type="checkbox" name="remember-me">

  • 流程

    1. 请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没有值的话表示用户尚未登陆,此时调用 autoLogin() 方法进行自动登录
    2. 当自动登录成功后返回的rememberMeAuth不为null时,表示自动登陆成功,此时调用 authenticate() 方法对 key 进行校验,并将登陆成功的用户信息保存到 SecurityContextHolder 对象中,然后发布登录成功事件,调用登陆成功回调。需要关注的是,登陆成功的回调并不包含 RememberMeServices 中的 loginSuccess() 方法
    3. 如果自动登陆失败,则调用 rememberMeServices.loginFail 方法处理登陆失败的回调。onUnSuccessfulAuthentication 和 onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现,这就是 RememberMeAuthenticationFilter 过滤器所做的事情,成功将 RememberMeServices 的服务集成进来

RememberMeServices

RememberMeServices 一共定义了三个方法

  • autoLogin:可以从请求中提取需要的参数,完成自动登录功能
  • loginFail:方法是自动登陆失败的回调
  • loginSuccess:方法是自动登录成功的回调

TokenBasedRememberMeServices

在开启记住我后,如果没有加入额外配置默认实现就是由 TokenBasedRememberMeServices 进行实现的。查看这个类源码中 processAutoLoginCookie() 方法实现(用于使用 Cookie 进行自动登录)

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 编码后响应给前端
  • 生成 token

  • 登陆认证成功之后的操作

  • 将生成的 token 存储到 Cookie 中

  • 对传递的 token 进行编码后存入当前 cookie 中

总结

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

8.3 内存令牌

PersistentTokenBasedRememberMeServices

  • 流程

    1. 不同于 TokonBasedRememberMeServices 中的 processAutologinCookie 方法,这里cookieTokens数组的长度为2,第一项是 series,第二项是 token
    2. cookieTokens 数组中分到提取出seriestoken然后根据series去内存中查询出一个 PersistentRememberMeToken 对象。如果查询出来的对象为 null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的token和从cookieTokens中解析出来的token不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常
    3. 根据数据库中查询出来的结果判断令牌是否过期。如果过期就抛出异常
    4. 生成一个新的 PersistentRememberMeToken 对象,用户名和series不变,token重新生成,date也使用当前时间。 newToken 生成后,根据series去修改内存中的tokendate(即每次自动登录后都会产生新的token和date)
    5. 调用addCookie()方法添加 Cookie,在addCookie()方法中,会调用到我们前面所说的setCookie()方法,但是要注意第一个数组参数中只有两项:seriestoken(即返回到前端的令牌是通过对seriestoken进行Base64编码得到的)
    6. 最后将根据用户名查询用户对象并返回

使用内存中令牌实现

package com.vinjcent.config.security;import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;import java.util.UUID;/***  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {// 构造注入使用@Autowired,set注入使用@Resourceprivate final DivUserDetailsService userDetailsService;// UserDetailsService@Autowiredpublic WebSecurityConfiguration(DivUserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}// AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}// 拦配置http拦截@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/toLogin").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/toLogin").loginProcessingUrl("/login").usernameParameter("uname").passwordParameter("passwd").defaultSuccessUrl("/toIndex", true).failureUrl("/toLogin").and().logout().logoutUrl("/logout").logoutSuccessUrl("/toLogin").and().rememberMe().rememberMeServices(rememberMeServices())// .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数// .alwaysRemember(true)   // 总是记住我,只针对服务后台设置.and().csrf().disable();}// 指定记住我的实现@Beanpublic RememberMeServices rememberMeServices() {return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUIDuserDetailsService,     // 认证数据源new InMemoryTokenRepositoryImpl());     // 令牌存储方式(不建议使用内存的方式存储令牌,如果服务器重启,那么内存将全部失效)}}

8.4 持久化令牌(就如Shiro中的session缓存)

  1. 导入数据库相关依赖
<dependencies><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</version></dependency>
</dependencies>
  1. 配置数据源
spring:# 数据源datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456
mybatis:# 注意 mapper 映射文件必须使用"/"type-aliases-package: com.vinjcent.pojomapper-locations: com/vinjcent/mapper/**/*.xml
  1. 创建对应的表结构
CREATE TABLE `persistent_logins`
(username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8
  1. RememberMeServices 进行持久化配置
    // 指定记住我的实现@Beanpublic RememberMeServices rememberMeServices() {// 配置 token 数据源,保证服务重启之后仍然有存储记录JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();// 配置数据源tokenRepository.setDataSource(dataSource);// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)// tokenRepository.setCreateTableOnStartup(true);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUIDuserDetailsService,     // 认证数据源tokenRepository);     // 令牌存储方式}
  • 总体配置
package com.vinjcent.config.security;import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;import javax.sql.DataSource;
import java.util.UUID;/***  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {// 构造注入使用@Autowired,set注入使用@Resourceprivate final DivUserDetailsService userDetailsService;// token 存储数据源private final DataSource dataSource;// UserDetailsService@Autowiredpublic WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {this.userDetailsService = userDetailsService;this.dataSource = dataSource;}// AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}// 拦配置http拦截@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/toLogin").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/toLogin").loginProcessingUrl("/login").usernameParameter("uname").passwordParameter("passwd").defaultSuccessUrl("/toIndex", true).failureUrl("/toLogin").and().logout().logoutUrl("/logout").logoutSuccessUrl("/toLogin").and().rememberMe().rememberMeServices(rememberMeServices())// .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数// .alwaysRemember(true)   // 总是记住我,只针对服务后台设置.and().csrf().disable();}// 指定记住我的实现@Beanpublic RememberMeServices rememberMeServices() {// 配置 token 数据源,保证服务重启之后仍然有存储记录JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();// 配置数据源tokenRepository.setDataSource(dataSource);// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)// tokenRepository.setCreateTableOnStartup(true);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUIDuserDetailsService,     // 认证数据源tokenRepository);     // 令牌存储方式(不建议使用内存的方式存储令牌)}}
  • 测试效果

第一次登录

重启服务测试,发现依然可以自动登录

8.5 自定义记住我(传统web版)

  1. 导入依赖
<dependencies><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--thymeleaf--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!--thymeleaf-security--><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version></dependency><!--security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
  1. application.yml配置文件
# 端口号
server:port: 3035servlet:session:# 设置session过期时间timeout: 1
# 服务应用名称
spring:application:name: SpringSecurity08# 关闭thymeleaf缓存(用于修改完之后立即生效)thymeleaf:cache: false# thymeleaf默认配置prefix: classpath:/templates/suffix: .htmlencoding: UTF-8mode: HTML# 数据源datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456
mybatis:# 注意 mapper 映射文件必须使用"/"type-aliases-package: com.vinjcent.pojomapper-locations: com/vinjcent/mapper/**/*.xml# 日志处理,为了展示 mybatis 运行 sql 语句
logging:level:com:vinjcent:debug
  1. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
  • Role
package com.vinjcent.pojo;import java.io.Serializable;public class Role implements Serializable {private Integer id;private String name;private String nameZh;public Role() {}public Role(Integer id, String name, String nameZh) {this.id = id;this.name = name;this.nameZh = nameZh;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getNameZh() {return nameZh;}public void setNameZh(String nameZh) {this.nameZh = nameZh;}@Overridepublic String toString() {return "Role{" +"id=" + id +", name='" + name + '\'' +", nameZh='" + nameZh + '\'' +'}';}
}
  • User
package com.vinjcent.pojo;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.*;// 自定义用户User
public class User implements UserDetails {private Integer id; // 用户idprivate String username;    // 用户名private String password;    // 密码private boolean enabled;    // 是否可用private boolean accountNonExpired;  // 账户过期private boolean accountNonLocked;   // 账户锁定private boolean credentialsNonExpired;  // 凭证过期private List<Role> roles = new ArrayList<>();   // 用户角色信息// 返回权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Set<SimpleGrantedAuthority> authorities = new HashSet<>();roles.forEach(role -> {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());authorities.add(simpleGrantedAuthority);});return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic boolean isEnabled() {return enabled;}public void setId(Integer id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setEnabled(boolean enabled) {this.enabled = enabled;}public void setAccountNonExpired(boolean accountNonExpired) {this.accountNonExpired = accountNonExpired;}public void setAccountNonLocked(boolean accountNonLocked) {this.accountNonLocked = accountNonLocked;}public void setCredentialsNonExpired(boolean credentialsNonExpired) {this.credentialsNonExpired = credentialsNonExpired;}public void setRoles(List<Role> roles) {this.roles = roles;}public Integer getId() {return id;}public List<Role> getRoles() {return roles;}
}
  1. 视图页面
  • login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页面</title>
</head>
<body><h1>用户登录</h1><form th:action="@{/login}" method="post">用户名: <input type="text" name="uname"> <br>密码: <input type="password" name="passwd"> <br><!-- value 可选值有:true yes on 1  -->记住我: <input type="checkbox" name="remember-me" value="true"><input type="submit" value="登录"></form>
<h3><div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h3>
</body>
</html>
  • index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head><meta charset="UTF-8"><title>系统主页</title>
</head>
<body>
<h1>欢迎<span sec:authentication="principal.username"></span>,进入我的主页!</h1><hr>
<h1>获取认证用户信息</h1>
<ul><li sec:authentication="principal.username"></li><li sec:authentication="principal.authorities"></li><li sec:authentication="principal.accountNonExpired"></li><li sec:authentication="principal.accountNonLocked"></li><li sec:authentication="principal.credentialsNonExpired"></li>
</ul><a th:href="@{/logout}">退出登录</a></body>
</html>
  1. 自定义认证数据源 UserDetailsService
package com.vinjcent.config.security;import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;import java.util.List;@Component
public class DivUserDetailsService implements UserDetailsService {// dao ===> springboot + mybatisprivate final UserService userService;private final RoleService roleService;@Autowiredpublic DivUserDetailsService(UserService userService, RoleService roleService) {this.userService = userService;this.roleService = roleService;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1.查询用户User user = userService.queryUserByUsername(username);if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");// 2.查询权限信息List<Role> roles = roleService.queryRolesByUid(user.getId());user.setRoles(roles);return user;}
}
  1. 配置拦截请求 WebSecurityConfigurerAdapter
package com.vinjcent.config.security;import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;import javax.sql.DataSource;
import java.util.UUID;/***  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {// 构造注入使用@Autowired,set注入使用@Resourceprivate final DivUserDetailsService userDetailsService;// token 存储数据源private final DataSource dataSource;// UserDetailsService@Autowiredpublic WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {this.userDetailsService = userDetailsService;this.dataSource = dataSource;}// AuthenticationManager@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}// 拦配置http拦截@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/toLogin").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/toLogin").loginProcessingUrl("/login").usernameParameter("uname").passwordParameter("passwd").defaultSuccessUrl("/toIndex", true)    // 重定向.failureUrl("/toLogin")     // 失败重定向.and().logout().logoutUrl("/logout").logoutSuccessUrl("/toLogin").and().rememberMe().rememberMeServices(rememberMeServices())// .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数,注意前端传递的参数// .alwaysRemember(true)   // 总是记住我,只针对服务后台设置,无论前端是否点击"记住我"都默认使用记住我.and().csrf().disable();}// 指定记住我的实现@Beanpublic RememberMeServices rememberMeServices() {// 配置 token 数据源,保证服务重启之后仍然有存储记录JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();// 配置数据源tokenRepository.setDataSource(dataSource);// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)// tokenRepository.setCreateTableOnStartup(true);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUIDuserDetailsService,     // 认证数据源tokenRepository);     // 令牌存储方式(不建议使用内存的方式存储令牌)}}

8.6 自定义记住我(前后端分离)

在根据之前源码分析中,发现是根据 remember-me 设置记住我的参数,但是如果使用前后端分离,请求中的类型为 JSON 数据,又如何提取出来 remember-me 的参数呢?而又要如何在 Cookie 中设置我们的 token 令牌呢?

对于登录认证成功之后的操作,见如下图

这里调用了 rememberMeRequested()方法,传递的是一个 HttpServletRequest 和 String 类型的参数,而这个 rememberMeRequested()函数是在 AbstractRememberMeServices 抽象类中的,所以我们需要对其进行重写

  1. 导入依赖pom.xml
    <dependencies><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</version></dependency></dependencies>
  1. application.yml配置文件
# 端口号
server:port: 3035servlet:session:# 设置session过期时间timeout: 1
# 服务应用名称
spring:application:name: SpringSecurity09security# 数据源datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456
mybatis:# 注意 mapper 映射文件必须使用"/"type-aliases-package: com.vinjcent.pojomapper-locations: com/vinjcent/mapper/**/*.xml# 日志处理,为了展示 mybatis 运行 sql 语句
logging:level:com:vinjcent:debug
  1. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
  • Role
package com.vinjcent.pojo;import java.io.Serializable;public class Role implements Serializable {private Integer id;private String name;private String nameZh;public Role() {}public Role(Integer id, String name, String nameZh) {this.id = id;this.name = name;this.nameZh = nameZh;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getNameZh() {return nameZh;}public void setNameZh(String nameZh) {this.nameZh = nameZh;}@Overridepublic String toString() {return "Role{" +"id=" + id +", name='" + name + '\'' +", nameZh='" + nameZh + '\'' +'}';}
}
  • User
package com.vinjcent.pojo;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.*;// 自定义用户User
public class User implements UserDetails {private Integer id; // 用户idprivate String username;    // 用户名private String password;    // 密码private boolean enabled;    // 是否可用private boolean accountNonExpired;  // 账户过期private boolean accountNonLocked;   // 账户锁定private boolean credentialsNonExpired;  // 凭证过期private List<Role> roles = new ArrayList<>();   // 用户角色信息// 返回权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Set<SimpleGrantedAuthority> authorities = new HashSet<>();roles.forEach(role -> {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());authorities.add(simpleGrantedAuthority);});return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic boolean isEnabled() {return enabled;}public void setId(Integer id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setEnabled(boolean enabled) {this.enabled = enabled;}public void setAccountNonExpired(boolean accountNonExpired) {this.accountNonExpired = accountNonExpired;}public void setAccountNonLocked(boolean accountNonLocked) {this.accountNonLocked = accountNonLocked;}public void setCredentialsNonExpired(boolean credentialsNonExpired) {this.credentialsNonExpired = credentialsNonExpired;}public void setRoles(List<Role> roles) {this.roles = roles;}public Integer getId() {return id;}public List<Role> getRoles() {return roles;}
}
  1. 自定义登录过滤器
  • LoginFilter
package com.vinjcent.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.ObjectUtils;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;/*** 自定义前后端分离的 Filter,重写 UsernamePasswordAuthenticationFilter*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {// 用于指定请求类型private boolean postOnly = true;@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {// 如果是json格式,需要转化成对象并从中获取用户输入的用户名和密码进行认证 {"username": "root", "password": "123", "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 rememberMe = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);// 如果 rememberMe 不为空if (!ObjectUtils.isEmpty(rememberMe)) {// 将其存储request作用域request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberMe);}System.out.println("用户名: " + username + " 密码: " + password +  " 是否记住我: " + rememberMe);UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, token);return this.getAuthenticationManager().authenticate(token);} catch (IOException e) {e.printStackTrace();}}return super.attemptAuthentication(request, response);}@Overridepublic void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}
}
  1. 自定义记住我 services 实现类
  • DivPersistentTokenBasedRememberMeServices
package com.vinjcent.config.security;import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;import javax.servlet.http.HttpServletRequest;/*** 自定义记住我 services 实现类*/
public class DivPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {/*** 自定义前后端分离获取 rememberMe 请求参数* @param request 请求* @param rememberMe 记住我参数* @return 返回boolean*/@Overrideprotected boolean rememberMeRequested(HttpServletRequest request, String rememberMe) {String paramValue = (String) request.getAttribute(rememberMe);if (paramValue != null) {if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {return true;}}this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", paramValue));return false;}public DivPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService, tokenRepository);}
}
  1. 认证数据源
package com.vinjcent.config.security;import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;import java.util.List;@Component
public class DivUserDetailsService implements UserDetailsService {// dao ===> springboot + mybatisprivate final UserService userService;private final RoleService roleService;@Autowiredpublic DivUserDetailsService(UserService userService, RoleService roleService) {this.userService = userService;this.roleService = roleService;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1.查询用户User user = userService.queryUserByUsername(username);if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");// 2.查询权限信息List<Role> roles = roleService.queryRolesByUid(user.getId());user.setRoles(roles);return user;}
}
  1. 过滤器适配器
  • WebSecurityConfiguration
package com.vinjcent.config.security;import com.vinjcent.filter.LoginFilter;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
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.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;import javax.sql.DataSource;
import java.util.UUID;@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {// 注入数据源认证private final DivUserDetailsService userDetailsService;// 注入数据源private final DataSource dataSource;@Autowiredpublic WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {this.userDetailsService = userDetailsService;this.dataSource = dataSource;}// 自定义AuthenticationManager(自定义需要暴露该bean)@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}// 暴露AuthenticationManager,使得这个bean能在组件中进行注入@Override@Beanpublic AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}@Beanpublic LoginFilter loginFilter() throws Exception {// 1.创建自定义的LoginFilter对象LoginFilter loginFilter = new LoginFilter();// 2.设置登陆操作的请求loginFilter.setFilterProcessesUrl("/login");// 3.动态设置传递的参数keyloginFilter.setUsernameParameter("uname");  // 指定 json 中的用户名keyloginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key// 4.设置自定义的用户认证管理者loginFilter.setAuthenticationManager(authenticationManager());// 5.配置认证成功/失败处理(前后端分离)loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());  // 认证成功处理loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());  // 认证失败处理// 6.设置认证成功时使用自定义 rememberMeServices// 下面也设置了一次,因为第一次认证需要生成token传递给客户端,第二次是因为,当session过期之后,能够从数据库中去查找对应的持久化记录(二者缺一不可)loginFilter.setRememberMeServices(rememberMeServices());return loginFilter;}// 自定义rememberMeServices@Beanpublic RememberMeServices rememberMeServices() {// 使用持久化存储数据JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();// 设置持久化数据源tokenRepository.setDataSource(dataSource);return new DivPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService, tokenRepository);}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe()   // 开启记住我功能// 1.认证成功之后根据记住我,将 cookie 保存到客户端// 2.只有 cookie 写入到客户端成功才能实现自动登录功能.rememberMeServices(rememberMeServices())   // 设置自动登录使用哪个 rememberMeServices.and().logout().logoutUrl("/logout").logoutSuccessHandler(new DivLogoutSuccessHandler()).and().exceptionHandling().authenticationEntryPoint(((req, resp, ex) -> {resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("请认证之后再操作!");})).and().csrf().disable();// 替换原始 UsernamePasswordAuthenticationFilter 过滤器http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);/**http.addFilter();   // 添加一个过滤器http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前http.addFilterAfter();  // after: 添加一个过滤器,追加到某个具体过滤器之后*/}
}
  1. 测试登陆后,将服务停止,再次开启访问系统资源能够正常访问

SpringSecurity(八)【RememberMe记住我】相关推荐

  1. SpringBoot学习:整合shiro(rememberMe记住我功能)

    项目下载地址:http://download.csdn.NET/detail/aqsunkai/9805821 首先在shiro配置类中注入rememberMe管理器 /*** cookie对象;* ...

  2. Spring Security 入门 Remember-Me 记住我功能

    用户选择了"记住我"成功登录后,将会把username.随机生成的序列号.生成的token存入一个数据库表中,同时将它们的组合生成一个cookie发送给客户端浏览器. 当没有登录的 ...

  3. SpringSecurity基础:记住我

    记住我 我们的网站还有一个重要的功能,就是记住我,也就是说我们可以在登陆之后的一段时间内,无需再次输入账号和密码进行登陆,相当于服务端已经记住当前用户,再次访问时就可以免登陆进入,这是一个非常常用的功 ...

  4. SpringSecurity的rememberme

    记住我功能原理分析 还记得前面咱们分析认证流程时,提到的记住我功能吗? 现在继续跟踪找到AbstractRememberMeServices对象的loginSuccess方法: 再点进去上面if判断中 ...

  5. SpringSecurity之RememberMe功能的原理

    在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,下面,主要讲解如何使用Spring Security实现记住我这个功能以及深入源 ...

  6. SpringSecurity之RememberMe

    浏览器开发记住我功能 因为我已经实现过Oauth认证,在那种情况下已经实现记住我功能(token有效期),所以这里只是简单记录一下,功能实现即可(用户登录的token一般存储在Redis中). 原理: ...

  7. BCryptPasswordEncoder加密和匹配的原理 和 springsecurity 的 rememberme原理

    只知道这个的用法,心里着实难受,所以看了看底层,简单做下总结. BCryptPasswordEncoder算法和shiro的区别: 其实和shiro中区别就是shiro 中的salt是自己指定的,然后 ...

  8. SpringSecurity之RememberMe实现

    RememberMe的实现一定离不开Token的持久化存储.来看看使用Security应该怎么用RememberMe功能 RememberMe实现的两个模式 Security中有着两个实现Rememb ...

  9. SpringBoot集成SpringSecurity(二) 个性化登录配置(remember-me mongodb)

    前言 本文件所记录的是使用SpringSecurity实现remember me功能,有兴趣的朋友可以继续阅读,有何不足之处还请各位指出(本文未对用户 -  角色 - 权限三者的关系进行详细介绍详情见 ...

最新文章

  1. 使用一阶微分对图像锐化
  2. JavaScript----BOM(浏览器对象模型)
  3. Python面试题总结(6)--数据类型(综合)
  4. 速读《文献管理与信息分析》笔记
  5. C4D双十一促销海报模板,参考一下!
  6. 页面导航【WP7学习札记之七】
  7. iOS开发之应用内检测手机锁屏,解锁状态
  8. LeetCode—1.快速排序算法
  9. 中国上市公司2001-2020年历史股票数据下载(获得方式见网页 http://yanzw.cn )
  10. js 生成条形码(JsBarcode.all.min.js)
  11. 人工智能(12)大数据
  12. Paypal移动快速支付流程
  13. 记录一下java的常用单词
  14. 有好看的女生用的黑色壁纸吗?
  15. maven核心,pom.xml详解
  16. 历代诗词咏宁夏注释1----常星景: 六盘
  17. python搜索文献 速成_0基础5天速成Python,你也能发top期刊
  18. 苹果主题商店_盒子电视模拟器电视直播+影视点播!苹果安卓手机影视点播!...
  19. Linux-Samba文件共享服务
  20. “逐梦太空,情系北斗” 北斗导航技术与产业应用

热门文章

  1. 面对海量资产运维的行云管家应对之道
  2. 大白话讲解JDK源码系列:从头到尾再讲一遍ThreadLocal
  3. 创建数据库是列名无效咋办_列创建后,sql server上的列名无效
  4. 把Linux下外设的USB端口号映射到固定的名字
  5. mysql 伪表查询语句_MySql系列05:MySql中DQL查询操作
  6. 如何发布一个本地网站
  7. 职称计算机Word2003是考什么,职称计算机考试:word2003考点
  8. 英国电信第四财季利润大幅下滑 宣布裁员4000人
  9. Nginx四层代理和7层反向代理
  10. 神经网络中的网络优化和正则化(三)之超参数优化