SpringSecurity

Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是Spring Security重要核心功能。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

SSM + Shiro

•Spring Boot/Spring Cloud + Spring Security

以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行 的。

入门案例

image-20260205224131513

image-20260205224146174

添加一个配置类

1
2
3
4
5
6
7
8
9
10
11
@Configuration 
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.and()
.authorizeRequests() // 认证配置
.anyRequest() // 任何请求
.authenticated(); // 都需要身份验证
}
}

运行项目访问8080

默认的用户名:user 密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!

输入用户名,密码,这样表示可以访问了,404 表示我们没有这个控制器,但是我们可以 访问了。

权限管理中的相关概念

主体

英文单词:principal 使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系 统谁就是主体。

认证

英文单词:authentication 权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证 明自己是谁。 笼统的认为就是以前所做的登录操作。

授权

英文单词:authorization 将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功 能的能力。 所以简单来说,授权就是给用户分配权限。

完善案例

添加一个控制器进行访问

1
2
3
4
5
6
7
8
@Controller 
public class IndexController {
@GetMapping("index")
@ResponseBody
public String index(){
return "success";
}
}

SpringSecurity基本原理

SpringSecurity 本质是一个过滤器链:

从启动是可以获取到过滤器链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

代码底层流程:重点看三个过滤器:

FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。

image-20260205224855209

super.beforeInvocation(fi)表示查看之前的 filter 是否通过。

fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。

ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

image-20260205225221469

UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。

image-20260205225243878

当过滤器过滤的过程中会走到对应的接口里面执行方法。

UserDetailService 接口讲解

当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。接口定义如下:

image-20260205231112051

返回值UserDetails 这个类是系统默认的用户“主体”

在这个接口里面有很多方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 表示获取登录用户所有权限 
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();

以下是UserDetails实现类

image-20260205235029117

以后我们只需要使用User 这个实体类即可!

image-20260205235056591

方法参数 username 表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无 法接收。

PasswordEncoder 接口讲解

1
2
3
4
5
6
7
8
// 表示把参数按照特定的解析规则进行解析 
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}

接口实现类

image-20260205235256151

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析 器。 BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单 向加密。可以通过strength控制加密强度,默认10.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test 
public void test01(){
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new
BCryptPasswordEncoder();
// 对密码进行加密
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+atguigu);

//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}

当然这些都是单独使用springsecurity的方式,如果用SpringBoot他会对Security进行自动配置,可以在yml或者自定义类里面修改配置即可。

SpringSecurity Web权限方案

设置登录系统的账号、密码

方式一:在application.properties

1
2
spring.security.user.name=atguigu 
spring.security.user.password=atguigu

方式二:编写类实现接口

1
2
3
4
5
6
7
8
@Configuration 
public class SecurityConfig {
// 注入PasswordEncoder 类到spring 容器中
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

然后编写登录类实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service 
public class LoginService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 判断用户名是否存在
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在!");
}
// 从数据库中获取的密码atguigu 的密文

String pwd = "$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na";

// 第三个参数表示权限
return new User(username,pwd,AuthorityUtils.commaSeparatedStringToAuthorityList("admin,"));
}
}

实现数据库认证来完成用户登录

完成自定义登录

准备sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
create table users( 
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
); -- 密码atguigu
insert into users values(1,'张
san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na'); -- 密码atguigu
insert into users values(2,'李
si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');

create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,'管理员');
insert into role values(2,'普通用户');

create table role_user(
uid bigint,
rid bigint
);

insert into role_user values(1,1);
insert into role_user values(2,2);


create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)

);

insert into menu values(1,'系统管理','',0,'menu:system');
insert into menu values(2,'用户管理','',0,'menu:user');


create table role_menu(
mid bigint,
rid bigint
);

insert into role_menu values(1,1);


insert into role_menu values(2,1);
insert into role_menu values(2,2);

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<dependencies> 
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

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

<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

制作实体类

1
2
3
4
5
6
@Data 
public class Users {
private Integer id;
private String username;
private String password;
}

整合MybatisPlus 制作mapper

1
2
3
@Repository 
public interface UsersMapper extends BaseMapper<Users> {
}
1
2
3
4
5
6
#配置文件添加数据库配置 
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

制作登录实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service("userDetailsService") 
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String s) throws
UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper();
wrapper.eq("username",s);
Users users = usersMapper.selectOne(wrapper);
if(users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
System.out.println(users);
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}

未认证请求跳转到登录页

这里是让没有登录的用户跳转到登录页而不能访问服务器页面

引入前端模板依赖

1
2
3
4
<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

引入登录页面

将准备好的登录页面导入项目中

编写控制器

1
2
3
4
5
6
7
8
9
10
11
12
@Controller 
public class IndexController {
@GetMapping("index")
public String index(){
return "login";
}
@GetMapping("findAll")
@ResponseBody
public String findAll(){
return "findAll";
}
}

编写配置类放行登录页面以及静态资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration 
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入PasswordEncoder 类到spring 容器中
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/layui/**","/index") //表示配置请求路径
.permitAll() // 指定 URL 无需保护。
.anyRequest() // 其他请求
.authenticated(); //需要认证
}
}

设置未授权的请求跳转到登录页

这个是登陆后没有权限的回到登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override 
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
.loginPage("/index") // 配置哪个url 为登录页面
.loginProcessingUrl("/login") // 设置哪个是登录的url。
.successForwardUrl("/success") // 登录成功之后跳转到哪个url
.failureForwardUrl("/fail");// 登录失败之后跳转到哪个url
http.authorizeRequests()
.antMatchers("/layui/**","/index") //表示配置请求路径
.permitAll() // 指定 URL 无需保护。
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭csrf
http.csrf().disable();
}

控制器

1
2
3
4
5
6
7
8
9
  
@PostMapping("/success")
public String success(){
return "success";
}
@PostMapping("/fail")
public String fail(){
return "fail";
}
1
2
3
4
5
<form action="/login"method="post"> 
用户名:<input type="text"name="username"/><br/>
密码:<input type="password"name="password"/><br/>
<input type="submit"value="提交"/>
</form>

注意:页面提交方式必须为post 请求,所以上面的页面不能使用,用户名,密码必须为 username,password 原因: 在执行登录的时候会走一个过滤器UsernamePasswordAuthenticationFilter

如果修改配置可以调用usernameParameter()和passwordParameter()方法。

1
2
3
4
5
<form action="/login"method="post"> 
用户名:<input type="text"name="loginAcct"/><br/>
密码:<input type="password"name="userPswd"/><br/>
<input type="submit"value="提交"/>
</form>

image-20260206003606648

基于角色或者权限进行访问控制

hasAuthority方法

如果当前的主体具有指定的权限,则返回 true,否则返回false

修改配置类

image-20260207091902085

添加一个控制器

1
2
3
4
5
@GetMapping("/find") 
@ResponseBody
public String find(){
return "find";
}

给用户登录主体赋予权限

image-20260207091955634

测试: http://localhost:8090/findAll 访问findAll 进入登录页面

认证完成之后返回登录成功

hasAnyAuthority 方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.

访问 http://localhost:8090/find

hasRole 方法

如果用户具备给定角色就允许访问,否则出现403。

如果当前主体具有指定的角色,则返回true。

底层源码:

image-20260207093212708

给用户添加角色:

image-20260207093344580

修改配置文件: 注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。

image-20260207093417258

关闭csrf 是因为纯API服务,前后端分离,无Cookie自动携带,用Token认证如JWT

hasAnyRole

表示用户具备任何一个条件都可以访问。 给用户添加角色:

image-20260207093708285

修改配置文件:

image-20260207093730239

基于数据库实现权限认证

添加实体类

1
2
3
4
5
6
7
8
@Data 
public class Menu {
private Long id;
private String name;
private String url;
private Long parentId;
private String permission;
}
1
2
3
4
5
@Data 
public class Role {
private Long id;
private String name;
}

编写接口与实现类

UserInfoMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
/** 
* 根据用户Id 查询用户角色
* @param userId
* @return
*/
List<Role> selectRoleByUserId(Long userId);

/**
* 根据用户Id 查询菜单
* @param userId
* @return
*/
List<Menu> selectMenuByUserId(Long userId);

上述接口需要进行多表管理查询: 需要在resource/mapper目录下自定义UserInfoMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0"encoding="utf-8"?> 
<!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper
3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.mapper.UserInfoMapper">
<!--根据用户Id 查询角色信息-->
<select id="selectRoleByUserId"resultType="com.atguigu.bean.Role">
SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON
ru.rid=r.id where ru.uid=#{0}
</select>
<!--根据用户Id 查询权限信息-->
<select id="selectMenuByUserId"resultType="com.atguigu.bean.Menu">
SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m
INNER JOIN role_menu rm ON m.id=rm.mid
INNER JOIN role r ON r.id=rm.rid
INNER JOIN role_user ru ON r.id=ru.rid
WHERE ru.uid=#{0}
</select>
</mapper>

UsersServiceImpl

image-20260207095123656

在配置文件中添加映射

在配置文件中application.yml添加

1
2
mybatis: 
mapper-locations: classpath:mapper/*.xml

修改访问配置类

image-20260207095337116

自定义403页面

修改访问配置类

1
http.exceptionHandling().accessDeniedPage("/unauth"); 

添加对应控制器

1
2
3
4
@GetMapping("/unauth") 
public String accessDenyPage(){
return "unauth";
}

unauth.html

1
2
3
<body> 
<h1>对不起,您没有访问权限!</h1>
</body>

注解使用

@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。

使用注解先要开启注解功能!

@EnableGlobalMethodSecurity(securedEnabled=true)

1
2
3
4
5
6
7
@SpringBootApplication 
@EnableGlobalMethodSecurity(securedEnabled=true)
public class DemosecurityApplication {
public static void main(String[] args) {
SpringApplication.run(DemosecurityApplication.class, args);
}
}

在控制器方法上添加注解

1
2
3
4
5
// 测试注解:  
@RequestMapping("testSecured")
@ResponseBody
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() { return "hello,user"; }

登录之后直接访问:

http://localhost:8090/testSecured

将上述的角色改为 @Secured({“ROLEnormal”,”ROLE管理员”})即可访问

@PreAuthorize

先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true)

@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的roles/permissions参数传到方法中。

1
2
3
4
5
6
7
8
@RequestMapping("/preAuthorize") 
@ResponseBody
//@PreAuthorize("hasRole('ROLE_管理员')")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}

@PostAuthorize

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.

1
2
3
4
5
6
7
8
@RequestMapping("/testPostAuthorize") 
@ResponseBody
@PostAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("test--PostAuthorize");
return "PostAuthorize";
}

@PostFilter

@PostFilter :权限验证之后对数据进行过滤 留下用户名是admin1的数据

表达式中的 filterObject 引用的是方法返回值List中的某一个元素

1
2
3
4
5
6
7
8
9
10
@RequestMapping("getAll") 
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}

@PreFilter

@PreFilter: 进入控制器之前对数据进行过滤

1
2
3
4
5
6
7
8
9
10
@RequestMapping("getTestPreFilter") 
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}

还有很多权限表达式,都是内置的具体要用的时候查找就行

表达式 说明 示例
hasRole('ROLE_ADMIN') 用户是否有角色(自动加 ROLE_ 前缀) .access("hasRole('ADMIN')")
hasAnyRole('ADMIN','USER') 拥有任意一个角色
hasAuthority('user:delete') 用户是否有指定权限(Authority) .access("hasAuthority('menu:system')")
hasAnyAuthority('a','b') 拥有任意一个权限
permitAll 允许所有访问 .access("permitAll")
denyAll 拒绝所有访问
isAuthenticated() 用户已认证(非匿名)
isAnonymous() 用户是匿名的
isRememberMe() 通过 Remember-Me 登录
principal 当前用户主体(UserDetails 对象) principal.username == 'admin'
authentication 当前认证对象 authentication.principal.username == '...'

基于数据库的记住我

创建表

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

添加数据库的配置文件

1
2
3
4
5
6
spring: 
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.128:3306/test
username: root
password: root

编写配置类

创建配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration 
public class BrowserSecurityConfig {
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);

return jdbcTokenRepository;
}
}

这个配置类是将token注入到数据库中并且建表,所以执行一次会创建,后来要删除掉,但是这也只是为了实验,后续token可以存在redis中就不用写这个了。

很多现代系统(Vue/React + Spring Boot API)根本不使用 Spring Security 的“记住我”功能,而是:

  • 用户登录 → 后端返回 JWT
  • 前端保存 JWT → 每次请求带 Authorization Header
  • 这种架构下,完全不需要 PersistentTokenRepository

所以“后续 token 可以存在 Redis 中就不用写这个”,前提是你的系统仍然在用 Spring Security 的“记住我”机制
但如果已经改用 JWT 或 OAuth2,那连“记住我”都不用了,自然也不需要这个配置。

修改安全配置类

1
2
3
4
5
6
7
8
9
10
@Autowired 
private UsersServiceImpl usersService;

@Autowired
private PersistentTokenRepository tokenRepository;

// 开启记住我功能
http.rememberMe()
.tokenRepository(tokenRepository)
.userDetailsService(usersService);

页面添加记住我复选框

1
记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/> 

此处:name 属性值必须位remember-me.不能改为其他值

使用张三进行登录测试

登录成功之后,关闭浏览器再次访问http://localhost:8090/find,发现依然可以使用!

设置有效期

默认2周时间。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登 录。 在配置文件中设置

image-20260209124005577

用户注销

在登录页面添加一个退出连接

success.html

1
2
3
4
<body> 
登录成功<br>
<a href="/logout">退出</a>
</body>

在配置类中添加退出映射地址

1
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll()

CSRF

CSRF理解

跨站请求伪造,这个是一种让用户在当前已经登录的Web应用程序上执行非本意操作的攻击方法,跟跨网站脚本XSS相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。

XSS 是一种攻击者向网页中注入恶意脚本(通常是 JavaScript),当其他用户浏览该页面时,脚本会在其浏览器中执行,从而窃取信息、劫持会话或进行其他恶意操作。XSS也就是在输入框里面输入JS脚本注入到正常页面,然后就会把用户的信息发送到目标服务器。所以需要对输入进行过滤转义或者安全渲染

  • 输出转义:在将用户输入插入 HTML 前,对 < > & " ' 等字符转义(如 <<
  • 使用安全 API:避免 innerHTML,改用 textContent
  • 设置 Cookie 的 HttpOnly(防止 JS 读取)
  • 启用 CSP(内容安全策略)
  • 对富文本使用白名单过滤(如 DOMPurify)

真正的 CSRF 是在恶意页面中“静默提交表单”或“发起 AJAX 请求”到银行,利用浏览器自动携带的 Cookie 冒充用户操作。**如果银行没有 CSRF 防护(如 Token),这种攻击就会成功。**

假设你已登录银行网站。攻击者构造一个隐藏表单,指向银行的转账接口,并诱导你访问包含该表单的恶意网页。当你打开该网页时,浏览器自动带上你的银行 Cookie,完成转账。

SpringSecurity 实现CSRF的原理

1、生成csrfToken 保存到HttpSession 或者Cookie 中。

image-20260210095045628

SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository

image-20260210095212964

image-20260210095332702

然后这个接口也有很多实现类。

当前接口实现类:HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository

点进去看就会发现在里面生成_csrf的UUID作为Token

2、然后请求到来时,从请求中提取csrfToken,和保存的csrfToken 做比较,进而判断当 前请求是否合法。主要通过CsrfFilter 过滤器来完成。

SpringSecurity微服务权限方案

1、认证授权过程分析

(1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。

(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限 信息中去

image-20260210100233768

如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式 进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息 生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带 到header 请求头中,Spring-security 解析 header 头获取 token 信息,解析token 获取当前 用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前 请求是否有权限访问 。

权限管理数据模型

image-20260210101620355

jwt介绍

1、访问令牌类型

自包含令牌:令牌自身包含所有必要的信息(如用户身份、权限、有效期等),资源服务器无需查询外部服务即可验证和解析令牌。JWT比如。

透明令牌:令牌是一个无意义的随机字符串(如 a1b2c3d4-e5f6-7890),本身不包含任何信息。资源服务器必须向授权服务器发起 introspection(内省)请求才能验证其有效性并获取用户信息。

2、JWT的组成

典型的JWT如下

image-20260210114024300

该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。 每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

JWT头

JWT 头部分是一个描述JWT元数据的JSON对象,通常如下所示。

image-20260210114116871

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);

typ 属性表示令牌的类型,JWT令牌统一写为JWT。

最后,使用Base64 URL算法将上述 JSON 对象转换为字符串保存。

有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID 用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

1
{"sub": "1234567890",  "name": "Helen",  "admin": true  }  

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息 字段,存放保密信息,以防止信息泄露。 JSON 对象也使用Base64 URL算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成 签名。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)  

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个 部分用”.”分隔,就构成整个JWT对象。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算 法类似,稍有差别。 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个 字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换: “=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法。

具体代码实现

用redis存储用户的权限,然后登录成功后从redis获得权限,然后根据用户名生成token,也就是用头记录加密算法,然后有效载荷记录权限信息等,然后签名是服务器公钥通过加密算法生成的签名,然后放到cookie里面然后放在header里面,然后springsecurity就能从token里面获得用户权限,然后赋予权限,然后token是放在浏览器里面也就是客户端里面。

image-20260210121647564

编写核心配置类

Spring Security的核心配置就是继承WebSecurityConfigurerAdapter并注解 @EnableWebSecurity的配置。这个配置指明了用户名密码的处理方式、请求路径、登录 登出控制等和安全相关的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

// 自定义查询数据库用户名、密码和权限信息
private final UserDetailsService userDetailsService;

// Token 管理工具类(生成 token)
private final TokenManager tokenManager;

// 密码管理工具类
private final DefaultPasswordEncoder defaultPasswordEncoder;

// Redis 操作工具类
private final RedisTemplate redisTemplate;

@Autowired
public TokenWebSecurityConfig(
UserDetailsService userDetailsService,
DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

/**
* 配置安全设置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/admin/acl/index/logout")
.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate))
.and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate))
.httpBasic();
}

/**
* 配置认证管理器:指定 UserDetailsService 和密码编码器
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(defaultPasswordEncoder);
}

/**
* 配置忽略安全拦截的路径(静态资源、开放接口等)
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**", "/swagger-ui.html/**");
}
}

创建认证授权相关的工具类

image-20260211102921513

(1)DefaultPasswordEncoder:密码处理的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component 
public class DefaultPasswordEncoder implements PasswordEncoder {

public DefaultPasswordEncoder() {
this(-1);
}
/**
* @param strength
* the log rounds to use, between 4 and 31
*/
public DefaultPasswordEncoder(int strength) {
}
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}

(2)TokenManager:token操作的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Component
public class TokenManager {

private long tokenExpiration = 24 * 60 * 60 * 1000; // 24小时(毫秒)
private String tokenSignKey = "123456";

/**
* 根据用户名生成 JWT Token
*/
public String createToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
}

/**
* 从 Token 中解析出用户名
*/
public String getUserFromToken(String token) {
return Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}

/**
* 移除 Token(JWT 为无状态令牌,实际无需删除,客户端丢弃即可)
*/
public void removeToken(String token) {
// JWT 本身无法主动失效,通常依赖过期时间或配合黑名单机制。
// 此处仅作占位,实际可结合 Redis 实现 token 黑名单(如登出场景)。
}
}

(3)TokenLogoutHandler:退出实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TokenLogoutHandler implements LogoutHandler { 
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
if (token != null) {
tokenManager.removeToken(token);
//清空当前用户缓存中的权限数据
String userName = tokenManager.getUserFromToken(token);


redisTemplate.delete(userName);
}
ResponseUtil.out(response, R.ok());
}
}

(4)UnauthorizedEntryPoint:未授权统一处理

1
2
3
4
5
6
7
8
9
10
11
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

ResponseUtil.out(response, R.error());
}
}

创建认证授权实体类

image-20260211110112897

(1) SecutityUser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Data 
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

用户实体类

1
2
3
4
5
6
7
8
9
@Data 
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private String username;
private String password;
private String nickName;
private String salt;
private String token;
}

创建认证和授权filter

(1)TokenLoginFilter:认证的filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}

@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);

return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* 登录成功
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());

redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}

/**
* 登录失败
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException,ServletException {
ResponseUtil.out(response, R.error());
}
}

(2)TokenAuthenticationFilter:授权filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class TokenAuthenticationFilter extends BasicAuthenticationFilter { 
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager,
TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("================="+req.getRequestURI());
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.out(res, R.error());
}
if (authentication != null) {

SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}


chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken
getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = tokenManager.getUserFromToken(token);
List<String> permissionValueList = (List<String>)
redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new
SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token,
authorities);
}
return null;
}
return null;
}
}

SpringSecurity 原理总结

SpringSecurity 的过滤器介绍

SpringSecurity采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤 器链的15个过滤器进行说明:

(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于 处理异步请求映射的 WebAsyncManager 进行集成。

(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上 下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信 息就是这个过滤器处理的。

(3) HeaderWriterFilter:用于将头信息加入响应中。

(4) CsrfFilter:用于处理跨站请求伪造。

(5)LogoutFilter:用于处理退出登录。

(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中 获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码 时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个 过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会 配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

(8)BasicAuthenticationFilter:检测和处理 http basic 认证。

(9)RequestCacheAwareFilter:用来处理请求的缓存。

(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。

(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

(12)SessionManagementFilter:管理 session 的过滤器

(13)ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。 (14)FilterSecurityInterceptor:可以看做过滤器链的出口。

(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

基本流程

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个 过滤器:

image-20260215101404206

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以 使用Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认 证过滤器要在configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重 点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式 的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会 直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源 权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并 由ExceptionTranslationFilter 过滤器进行捕获和处理。

认证流程

认证流程是在UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下 所示:

image-20260215101531847

UsernamePasswordAuthenticationFilter 源码

当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认 证。该过滤器的 doFilter() 方法实现在其抽象父类 AbstractAuthenticationProcessingFilter 中,查看相关源码:

image-20260215101943462

image-20260215102002851

上述的 第二 过程调用了UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法,源码如下:

image-20260215102030578

image-20260215102038823

上述的(3)过程创建的UsernamePasswordAuthenticationToken 是 Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认 证的用户信息,一个用于封装认证成功后的用户信息:

image-20260215102103045

Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:

image-20260215102305687

ProviderManager 源码

上述过程中,UsernamePasswordAuthenticationFilter 过滤器的 attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。 ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接 口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个 List列表,存放多种认证方式,实际上这是委托者模式 (Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider, AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托 对应的 AuthenticationProvider 进行用户认证。

image-20260215102846815

image-20260215102858466

image-20260215102916785

image-20260215102924993

上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的 eraseCredentials() 方法去除敏感信息。查看 UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方 法实现在其父类中:

image-20260215103016908

认证成功 / 失败处理

上述过程就是认证流程的最核心部分,接下来重新回到 UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成 功/失败的处理:

image-20260215103058294

image-20260215103126645

image-20260215103148036

image-20260215103159291

SpringSecurity 权限访问流程

上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对 ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。

ExceptionTranslationFilter 过滤器

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后 续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:

image-20260215103626052

FilterSecurityInterceptor 过滤器

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链 的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果 访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器

ExceptionTranslationFilter 进行捕获和处理。具体源码如下:

image-20260215103713429

需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件 DispatcherServlet 运行之前。也就是说,请求通过Spring Security 的所有过滤器, 不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

SpringSecurity请求间共享认证信息

一般认证成功后的用户信息是通过Session在多个请求之间共享,那么SpringSecurity中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行具体分析。

image-20260215210148190

在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:

image-20260215210226507

查看 SecurityContext 接口及其实现类 SecurityContextImpl,该类其实就是对 Authentication 的封装:

查看 SecurityContextHolder 类,该类其实是对 ThreadLocal 的封装,存储 SecurityContext 对象:

image-20260215210328718

image-20260215210343472

image-20260215210352025

image-20260215210400897

SecurityContextHolder:这是Spring Security的核心。它默认使用ThreadLocal来存储当前请求的安全上下文(SecurityContext)。因为一次请求由一个线程处理,所以在请求的任何地方都能通过它拿到用户信息

SecurityContextPersistenceFilter:这个过滤器在过滤器链的最前端 。它的doFilter方法做了三件事 :

  • 请求前:从Session中读取SecurityContext,并设置到SecurityContextHolder中。
  • 请求中:放行请求,让后续的Filter和Servlet处理业务。
  • 请求后:从SecurityContextHolder中取出SecurityContext(可能已被修改),保存回Session,然后清空SecurityContextHolder