简介
RememberMe 这个功能非常常见,无论是在 QQ、邮箱…都有这个选项。提到 RememberMe,往往会有一些误解,认为 RememberMe 功能就是把 用户名/密码 用 Cookie 保存在浏览器中,下次登陆时不用再次输入 用户名/密码。这个理解显然是不对的。我们这里所说的 RememberMe 是一种服务器端的行为。传统的登录方式基于 Session 会话,一旦用户的会话超时过期,就要再次登录,这样太过于繁琐。如果有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多。RememberMe 就是为了解决这一需求而生
具体的实现思路就是通过 Cookie 来记录当前用户身份。当用户登陆成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成之后,通过响应头带回给前端存储在 Cookie 中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie 中考的信息进行校验分析,进而确定出用户的身份,Cookie 中所保存的用户信息也是有效的,例如三天、一周等
开启记住我
RememberMeAuthenticationFilter
从上图中,当在 SecurityConfig 配置中开启了"记住我"功能之后,在进行认证时如果勾选了"记住我"选项,此时打开浏览器控制台,查看network 中的请求头信息。首先我们登陆时,在登陆请求中多了一个 remember-me 的参数
很显然,这个参数就是告诉服务器应该开启 RememberMe 这个功能的。如果自定义登陆页面开启 Remember 功能应该多加入一个一样的请求参数就可以了。请求最终会被 RememberMeAuthenticationFilter 进行拦截,然后自动登录具体参见源码
记住我: <input type="checkbox" name="remember-me">
rememberMeAuth
不为null
时,表示自动登陆成功,此时调用 authenticate() 方法对 key 进行校验,并将登陆成功的用户信息保存到 SecurityContextHolder 对象中,然后发布登录成功事件,调用登陆成功回调。需要关注的是,登陆成功的回调并不包含 RememberMeServices 中的 loginSuccess() 方法RememberMeServices
RememberMeServices 一共定义了三个方法
TokenBasedRememberMeServices
在开启记住我后,如果没有加入额外配置默认实现就是由 TokenBasedRememberMeServices 进行实现的。查看这个类源码中 processAutoLoginCookie() 方法实现(用于使用 Cookie 进行自动登录)
processAutoLoginCookie() 方法主要用来验证 Cookie 中的令牌信息是否合法
:
隔开成功登录回调过程
生成 token
总结
当用户通过 用户名/密码 的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名,令牌过期时间以及签名拼接成一个字符串,中间用:
隔开,对拼接好的字符串进行 Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当关闭浏览器再次打开,访问系统资源时会自动携带上 Cookie 中的令牌,服务端拿到 Cookie 中的令牌后,先进行 Bae64 解码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息;接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败
PersistentTokenBasedRememberMeServices
cookieTokens
数组的长度为2,第一项是 series,第二项是 tokenseries
和token
然后根据series
去内存中查询出一个 PersistentRememberMeToken 对象。如果查询出来的对象为 null,表示内存中并没有series
对应的值,本次自动登录失败。如果查询出来的token
和从cookieTokens
中解析出来的token
不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token
变了),此时移除当前用户的所有自动登录记录并抛出异常series
不变,token
重新生成,date
也使用当前时间。 newToken 生成后,根据series
去修改内存中的token
和date
(即每次自动登录后都会产生新的token和date)series
和token
(即返回到前端的令牌是通过对series
和token
进行Base64编码得到的)使用内存中令牌实现
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注入使用@Resource
private final DivUserDetailsService userDetailsService;
// UserDetailsService
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
// AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 拦配置http拦截
@Override
protected 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();
}
// 指定记住我的实现
@Bean
public RememberMeServices rememberMeServices() {
return new PersistentTokenBasedRememberMeServices(
UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
userDetailsService, // 认证数据源
new InMemoryTokenRepositoryImpl()); // 令牌存储方式(不建议使用内存的方式存储令牌,如果服务器重启,那么内存将全部失效)
}
}
<dependencies>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
dependencies>
spring:
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
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
// 指定记住我的实现
@Bean
public RememberMeServices rememberMeServices() {
// 配置 token 数据源,保证服务重启之后仍然有存储记录
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 配置数据源
tokenRepository.setDataSource(dataSource);
// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
// tokenRepository.setCreateTableOnStartup(true);
return new PersistentTokenBasedRememberMeServices(
UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
userDetailsService, // 认证数据源
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注入使用@Resource
private final DivUserDetailsService userDetailsService;
// token 存储数据源
private final DataSource dataSource;
// UserDetailsService
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
// AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 拦配置http拦截
@Override
protected 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();
}
// 指定记住我的实现
@Bean
public RememberMeServices rememberMeServices() {
// 配置 token 数据源,保证服务重启之后仍然有存储记录
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 配置数据源
tokenRepository.setDataSource(dataSource);
// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
// tokenRepository.setCreateTableOnStartup(true);
return new PersistentTokenBasedRememberMeServices(
UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
userDetailsService, // 认证数据源
tokenRepository); // 令牌存储方式(不建议使用内存的方式存储令牌)
}
}
第一次登录
重启服务测试,发现依然可以自动登录
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
dependencies>
application.yml
配置文件# 端口号
server:
port: 3035
servlet:
session:
# 设置session过期时间
timeout: 1
# 服务应用名称
spring:
application:
name: SpringSecurity08
# 关闭thymeleaf缓存(用于修改完之后立即生效)
thymeleaf:
cache: false
# thymeleaf默认配置
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
mode: HTML
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
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;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", nameZh='" + nameZh + '\'' +
'}';
}
}
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; // 用户id
private String username; // 用户名
private String password; // 密码
private boolean enabled; // 是否可用
private boolean accountNonExpired; // 账户过期
private boolean accountNonLocked; // 账户锁定
private boolean credentialsNonExpired; // 凭证过期
private List<Role> roles = new ArrayList<>(); // 用户角色信息
// 返回权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public 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;
}
}
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>
记住我: <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>
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>
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 + mybatis
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public 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;
}
}
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注入使用@Resource
private final DivUserDetailsService userDetailsService;
// token 存储数据源
private final DataSource dataSource;
// UserDetailsService
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
// AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 拦配置http拦截
@Override
protected 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();
}
// 指定记住我的实现
@Bean
public RememberMeServices rememberMeServices() {
// 配置 token 数据源,保证服务重启之后仍然有存储记录
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 配置数据源
tokenRepository.setDataSource(dataSource);
// 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
// tokenRepository.setCreateTableOnStartup(true);
return new PersistentTokenBasedRememberMeServices(
UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
userDetailsService, // 认证数据源
tokenRepository); // 令牌存储方式(不建议使用内存的方式存储令牌)
}
}
在根据之前源码分析中,发现是根据 remember-me 设置记住我的参数,但是如果使用前后端分离,请求中的类型为 JSON 数据,又如何提取出来 remember-me 的参数呢?而又要如何在 Cookie 中设置我们的 token 令牌呢?
对于登录认证成功之后的操作,见如下图
这里调用了 rememberMeRequested()方法,传递的是一个 HttpServletRequest 和 String 类型的参数,而这个 rememberMeRequested()函数是在 AbstractRememberMeServices 抽象类中的,所以我们需要对其进行重写
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
dependencies>
application.yml
配置文件# 端口号
server:
port: 3035
servlet:
session:
# 设置session过期时间
timeout: 1
# 服务应用名称
spring:
application:
name: SpringSecurity09security
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
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;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", nameZh='" + nameZh + '\'' +
'}';
}
}
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; // 用户id
private String username; // 用户名
private String password; // 密码
private boolean enabled; // 是否可用
private boolean accountNonExpired; // 账户过期
private boolean accountNonLocked; // 账户锁定
private boolean credentialsNonExpired; // 凭证过期
private List<Role> roles = new ArrayList<>(); // 用户角色信息
// 返回权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public 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;
}
}
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;
@Override
public 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);
}
@Override
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
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
*/
@Override
protected 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);
}
}
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 + mybatis
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public 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;
}
}
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;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
// 自定义AuthenticationManager(自定义需要暴露该bean)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 暴露AuthenticationManager,使得这个bean能在组件中进行注入
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public LoginFilter loginFilter() throws Exception {
// 1.创建自定义的LoginFilter对象
LoginFilter loginFilter = new LoginFilter();
// 2.设置登陆操作的请求
loginFilter.setFilterProcessesUrl("/login");
// 3.动态设置传递的参数key
loginFilter.setUsernameParameter("uname"); // 指定 json 中的用户名key
loginFilter.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
@Bean
public RememberMeServices rememberMeServices() {
// 使用持久化存储数据
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 设置持久化数据源
tokenRepository.setDataSource(dataSource);
return new DivPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService, tokenRepository);
}
@Override
protected 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: 添加一个过滤器,追加到某个具体过滤器之后
*/
}
}