认识

作用

  1. 保护应用程序URL,要求对应用程序的任何交互进行身份验证

只要项目里引入了 Spring Security 的依赖,它默认就会把整个网站的所有页面和接口都“锁起来”。任何人想访问,都必须先登录(证明自己是合法用户)。除了少数几个默认的端点(如 /login, /error),其他所有请求路径默认都需要认证。你可以通过配置来改变哪些路径需要认证,哪些可以公开访问。

  1. 程序启动时生成一个默认用户:user

为了方便你第一次启动项目就能登录测试,Spring Security 会自动创建一个临时账户,用户名就叫 user。只要你没有在配置文件(如 application.properties)中指定自己的用户名和密码,这个默认用户就会被创建。

  1. 生成一个默认的随机密码,并将此密码记录在控制台上

每次启动应用密码都会变,所以你必须在程序启动时,去 IDE 的控制台日志里找到它才能登录。

  1. 生成默认的登录表单,提供了基于表单的登录,以及注销流程

它自动给你做了一个简单的登录页面(你访问任何受保护的页面都会跳转到这个页面)和一个处理退出的功能。你不需要自己写这两个页面的 HTML 代码。它帮你把“用户输入用户名密码提交登录”和“用户点击退出”这一整套逻辑都做好了。你只需要用就行了。

  1. 对于Web请求,重定向到登录页面;对于服务请求,返回401未经授权

Web请求:比如你在浏览器里直接输入网站地址。如果没登录,它会跳转到登录页面,让你去登录。

服务请求:比如前端 JavaScript 用 Ajax 调用一个后端 API 接口。如果没登录,它不会返回一个 HTML 登录页面(因为前端程序看不懂),而是直接返回一个标准的 HTTP 401 状态码,告诉前端“你未经授权,请先认证”。

  1. 防止 CSRF 攻击(跨站请求伪造攻击)(默认开启)

CSRF 攻击的过程:

  1. 你登录了网银系统,网银系统给你颁发了令牌,令牌存储在浏览器的 cookie 中。
  2. 你没有关闭浏览器,在同一个浏览器中,你浏览了其他网站,黑客诱导你点击了某个链接。
  3. 这个链接自动向网银发起转账请求,只要请求路径是网银系统的路径,那么请求路径关联的 Cookie 就会自动提交,这是浏览器的默认行为。
  4. 因为网银收到了合法 Cookie,网银会认为这是合法的请求,完成向黑客账户的转账。

Spring Security 的做法:

  1. 它会为每个表单生成一个唯一的、随机的“令牌”(Token)。
  2. 当提交表单时,必须把这个令牌也一起提交上来。服务器会验证令牌是否正确。

原理:

  1. Cookie:浏览器自动带,黑客能用
  2. Spring Security 为表单生成的 Token:藏在表单里,每次不同,黑客拿不到(受同源策略保护)
  1. 提供了多种安全机制来防御会话相关的攻击(HttpOnly 默认开启,Secure 视情况而定)

安全的 Cookie 标志:登录凭证 Cookie(如 JSESSIONID)默认带 HttpOnly 属性。Secure 属性不是无条件加上的,通常在请求本身是 HTTPS 时由 Servlet 容器加上,也可以通过 server.servlet.session.cookie.secure=true 强制开启。HttpOnly 能防止 JavaScript 窃取 Cookie,Secure 要求 Cookie 只能在 HTTPS 连接下传输。

  1. 写入 Strict-Transport-Security 以确保 HTTPS(非默认开启)

Spring Security 默认不会把 Strict-Transport-Security 写入响应头。开启之后,用户通过 HTTPS 成功访问网站一次,浏览器就会记住"此站点必须使用HTTPS"的规则,后续所有访问(包括HTTP链接)都会自动升级为HTTPS请求。

  1. 写入 X-Content-Type-Options 以处理嗅探攻击(默认开启)

有些浏览器很“热心”,会主动猜测服务器返回内容的类型(比如一个文本文件,浏览器可能会猜它是 HTML 或 JavaScript)。这就有安全风险,攻击者可能利用这个特性执行恶意代码。

Spring Security 的做法:通过设置 X-Content-Type-Options: nosniff 这个响应头,直接命令浏览器:“别瞎猜!我说这个文件是什么类型,它就是什么类型。” 这样就关闭了浏览器的内容类型猜测行为。

  1. 写入 Cache-Control 头来保护经过身份验证的资源(默认开启)

登录后的一些敏感页面可能被浏览器或中间代理服务器缓存下来。Spring Security 默认会对这些需要认证的请求加上 Cache-Control 头,告诉浏览器和缓存服务器“不要缓存这个页面的内容”。

  1. 写入 X-Frame-Options 以处理点击劫持攻击(默认开启)

点击劫持就是攻击者把一个透明的、你已登录的网站(如邮箱)用 <iframe> 嵌入到他的恶意网页上,然后诱骗你去点击网页上的某个按钮(比如“抽大奖”),实际上你点的是邮箱里的“删除所有邮件”按钮。

  1. 你先正常登录了126(有Cookie)
  2. 你不关浏览器,点开黑客发给你的链接(比如抽奖活动)
  3. 黑客的网页里偷偷藏了一个透明的126 iframe
  4. 你点"抽奖"按钮,实际点的是iframe里126的"删除邮件"按钮

Spring Security 的做法:通过设置 X-Frame-Options: DENY 这个响应头,告诉浏览器:“不允许任何网站用 iframe 把我嵌入进去。”这样就防止了这种攻击。

底层原理

过滤器链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 简化版的过滤器链顺序
HttpServletRequest
↓
→ SecurityContextHolderFilter (恢复SecurityContext,作用:避免每次请求都重新认证)
→ WebAsyncManagerIntegrationFilter (集成异步管理,解决异步请求中如何访问SecurityContext)
→ HeaderWriterFilter (写入安全头,如HSTS、X-Frame-Options)
→ CorsFilter (处理跨域)
→ CsrfFilter (CSRF防护)
→ LogoutFilter (处理注销:如果请求是 /logout 则执行注销逻辑)
→ UsernamePasswordAuthenticationFilter (如果请求是 /login 则执行登录认证)
→ AuthorizationFilter (授权检查)
→ ... 其他自定义过滤器 ...
→ 你的业务Controller
↓
HttpServletResponse

注:Spring Security 6 中由 SecurityContextHolderFilter 负责恢复 SecurityContext,5.x 中对应的是 SecurityContextPersistenceFilter(6 中已弃用)。

一个过滤器负责一个安全任务,排队执行,像工厂的流水线一样,每个过滤器只负责自己的"工序"。

这种设计的好处:

  1. 单一职责:每个过滤器只做一件事,代码清晰
  2. 灵活组合:可以根据需要启用/禁用特定过滤器
  3. 可插拔:可以把自定义过滤器插入到链中任意位置
  4. 灵活控制顺序:过滤器的执行顺序很关键(比如先认证后授权)

新建一个springboot项目

依赖:

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
70
71
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zzh</groupId>
<artifactId>spring-security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-demo</name>
<description>spring-security-demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

基于内存的用户认证

配置类

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
package com.zzh.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
//@EnableWebSecurity // Spring Security的开关。(不添加这个注解也可以,因为SpringBoot进行了自动配置,根据依赖自动启用Spring Security)
public class WebSecurityConfig {

@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser( // 该行添加断点可查看user对象信息
User
.withDefaultPasswordEncoder() // 使用默认的密码编码器(使用默认的bcrypt进行加密)
.username("user") // 自定义用户名
.password("123456") // 自定义密码
.roles("USER") // 自定义角色
.build()
);
return manager;
}
}

UserDetailsService 是 Spring Security 的"用户信息查询器",把它的实现类 InMemoryUserDetailsManager 注入到 IOC 容器中即可实现用户认证。

InMemoryUserDetailsManager 基于内存管理用户信息。

认证流程

一、启动阶段(准备用户数据)

  1. 加载配置:Spring Boot 启动时,安全自动配置检测到 classpath 中有 Spring Security,自动启用 @EnableWebSecurity 的效果,所以示例中注释掉这个注解也能启用安全框架。
  2. 创建用户服务:执行 userDetailsService() 方法,创建一个内存版的用户管理器(InMemoryUserDetailsManager)。
  3. 存储用户:使用 User.build() 创建用户对象(用户名、加密的密码、角色),并将其存入内存管理器的内部 Map 中(key 是用户名,value 是 UserDetails 对象)。

结果:框架现在知道有一个合法的用户 user,密码是 123456。

二、认证阶段(验证用户登录)

  1. 拦截请求:当访问受保护页面时,过滤器链发现用户未登录,将其重定向到登录页。
  2. 提交凭证:用户在登录页输入用户名和密码,点击登录。
  3. 验证:
    • 查用户:认证管理器调用 InMemoryUserDetailsManager.loadUserByUsername("user"),根据用户名 user 从内存Map里查找对应的用户信息(UserDetails)。
    • 比密码:认证器将用户提交的明文密码(123456)进行编码,然后与查出来的用户信息中存储的已编码密码进行比对。
  4. 成功/失败:
    • 成功:密码一致,认证通过。用户信息被存入本次会话的安全上下文中(SecurityContext),然后跳转到最初想访问的页面。
    • 失败:密码不一致,返回登录错误页。

核心代码

用户名密码认证过滤器UsernamePasswordAuthenticationFilter中的 attemptAuthentication 方法负责获取用户提交的用户名和密码,并进入认证流程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}

InMemoryUserDetailsManager 中的 loadUserByUsername 方法根据用户名找到内存中存储的用户,代码如下:

1
2
3
4
5
6
7
8
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}

DaoAuthenticationProvider 中的 additionalAuthenticationChecks 方法拿内存中的密码和用户提交的密码对比,密码一致则认证成功,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.get().matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}

基于数据库的用户认证

环境

准备数据库表和数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 创建数据库
CREATE DATABASE `spring-security`;
USE `spring-security`;

-- 创建用户表
CREATE TABLE `user`(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) DEFAULT NULL ,
`password` VARCHAR(500) DEFAULT NULL,
`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_unique_index` ON `user`(`username`);

-- 插入用户数据(密码是 password )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$.GyF8EJtw4xeX/MrYTp6xOA2Sz1gC.jj7Hv7v5o8j/B4.rvtlwvKW', TRUE),
('jack', '{bcrypt}$2a$10$bsuU4Bxx9ks3jFmp5GKymOvZKGeXsgGJR.acmIqELyVRN3VjLQkIq', TRUE),
('lucy', '{bcrypt}$2a$10$UUpDXTOLhFUEBCAcuHKQjOnPWKcCt4n893aMDInjVZveR1xWlsO9G', TRUE);

引入依赖

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<groupId>com.zzh</groupId>
<artifactId>spring-security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-demo</name>
<description>spring-security-demo</description>

<properties>
<java.version>17</java.version>
</properties>

<!-- 添加仓库(阿里云),让 Maven 优先从该处拉取 -->
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>

<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>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- 数据库 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

配置

1
2
3
4
5
6
7
8
#MySQL数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring-security
spring.datasource.username=root
spring.datasource.password=123456

#SQL日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

代码

pojo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.zzh.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private Boolean enabled;
}

mapper

1
2
3
4
5
6
7
8
9
10
package com.zzh.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zzh.pojo.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public
interface UserMapper extends BaseMapper<User> {
}

service

1
2
3
4
5
6
7
8
package com.zzh.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.zzh.pojo.User;

public interface UserService extends IService<User> {
}
1
2
3
4
5
6
7
8
9
10
11
package com.zzh.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zzh.mapper.UserMapper;
import com.zzh.pojo.User;
import com.zzh.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.zzh.controller;

import com.zzh.pojo.User;
import com.zzh.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

private final UserService userService;

@GetMapping("/list")
public List<User> listUser(){
return userService.list();
}
}

启动类加上MapperScan注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.zzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.zzh.mapper")
public class SpringSecurityDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class, args);
}

}

基于数据库的用户认证

参考InMemoryUserDetailsManager实现DBUserDetailsManager

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
70
71
72
73
74
75
package com.zzh.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zzh.mapper.UserMapper;
import com.zzh.pojo.User;
import jakarta.annotation.Resource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import java.util.ArrayList;
import java.util.Collection;

public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}

@Override
public void createUser(UserDetails user) {

}

@Override
public void updateUser(UserDetails user) {

}

@Override
public void deleteUser(String username) {

}

@Override
public void changePassword(String oldPassword, String newPassword) {

}

@Override
public boolean userExists(String username) {
return false;
}

@Resource
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
if(user == null){
throw new UsernameNotFoundException(username);
}else{

// 查询数据库获取这个用户所关联的权限(现在给一个空的List集合)
Collection<GrantedAuthority> authorities = new ArrayList<>();

return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
// 账号是否未过期
true,
// 凭证是否未过期
true,
// 用户是否未被锁定
true,
authorities // 权限列表
);
}
}
}

加入IOC容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zzh.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
//@EnableWebSecurity // Spring Security的开关。(不添加这个注解也可以,因为SpringBoot进行了自动配置,根据依赖自动启用Spring Security)
public class WebSecurityConfig {

@Bean
public DBUserDetailsManager userDetailsService(){
DBUserDetailsManager dbUserDetailsManager = new DBUserDetailsManager();
return dbUserDetailsManager;
}
}

测试:输入用户名 admin,密码 password,查看是否登录成功。

也可以直接在 DBUserDetailsManager 类上添加 @Component 注解,只要纳入 IoC 容器的管理即可。

过滤器链配置

默认的过滤器链配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception{

// 配置所有HTTP请求都需要登录才能访问
http.authorizeHttpRequests(authorizeHttpRequestsCustomizer -> {
authorizeHttpRequestsCustomizer.anyRequest().authenticated();
});
// 关闭csrf攻击防御机制
http.csrf(csrf -> csrf.disable());

// 开启默认的表单登录页面(/login)
http.formLogin(Customizer.withDefaults());

// 开启HTTP Basic认证(浏览器弹窗登录)
http.httpBasic(Customizer.withDefaults());

// 构建并返回配置好的 SecurityFilterChain 对象
return http.build();
}

默认情况下安全过滤器链上有 16 个过滤器,可以在 DefaultSecurityFilterChain 中查看,如下图所示:

实现用户添加

代码

关闭csrf攻击防御

1
2
3
4
// 关闭csrf攻击防御功能(生产环境需要删除的代码)
http.csrf(csrf -> {
csrf.disable();
});

controller类

1
2
3
4
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}

service类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Resource
private DBUserDetailsManager manager;

@Override
public void saveUserDetails(User user) {
// 密码加密
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withDefaultPasswordEncoder()
.username(user.getUsername())
.password(user.getPassword())
.build();
manager.createUser(userDetails);
}

过滤器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception{

// 关闭csrf攻击防御机制
http.csrf(csrf -> csrf.disable());

// 开启默认的表单登录页面(/login)
// http.formLogin(Customizer.withDefaults());

// 开启HTTP Basic认证(浏览器弹窗登录)
// http.httpBasic(Customizer.withDefaults());

// 构建并返回配置好的 SecurityFilterChain 对象
return http.build();
}

测试

接口地址:http://localhost:8080/user/add

1
2
3
4
{
"username": "zenty",
"password": "123456"
}

密码加密

参考文档:https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html

Spring Security 的 PasswordEncoder 是专门用于密码加密和验证的接口。它有多种实现类,不同的实现采用不同的密码哈希算法(如 BCrypt、PBKDF2 等),用于安全地存储和验证用户密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface PasswordEncoder {

// 将明文密码编码为加密后的密码字符串
String encode(CharSequence rawPassword);

// 验证原始密码是否与加密后的密码匹配
// rawPassword 是原始密码
// encodedPassword 加密后的密码
boolean matches(CharSequence rawPassword, String encodedPassword);

default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

注册到项目中示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class WebSecurityConfig {

// 1. 注册 PasswordEncoder Bean
@Bean
public PasswordEncoder passwordEncoder() {
// strength=10 是默认值,表示哈希计算的迭代次数(2^10),建议 10-12
return new BCryptPasswordEncoder(10);
}
}

前后端分离认证

流程

  1. 用户提交登录
    • 前端 POST 发送 JSON({"username": "admin", "password": "password"})到登录接口(如 /login)
  2. 认证过滤器拦截
    • UsernamePasswordAuthenticationFilter 拦截请求
    • 将用户名密码封装为 UsernamePasswordAuthenticationToken
  3. AuthenticationManager 认证
    • 调用 UserDetailsService.loadUserByUsername(username)
    • 使用 PasswordEncoder 校验密码
  4. 认证成功
    • 生成 Authentication 对象(凭证对象)
    • 存入 SecurityContextHolder
    • 调用 AuthenticationSuccessHandler → 返回成功JSON
  5. 认证失败
    • 调用 AuthenticationFailureHandler → 返回错误JSON

关键在两个回调:

  1. 认证成功自动调用:AuthenticationSuccessHandler
  2. 认证失败自动调用:AuthenticationFailureHandler

依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.59</version>
</dependency>

代码

登录功能

controller类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.zzh.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

@GetMapping("/login")
public String login(){
return "login"; // 对应 src/main/resources/templates/login.html
}
}

登录页面login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xml:ns="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>自定义登录页</title>
</head>
<body>
<h1>自定义登录页</h1>
<form th:action="@{/login}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>

进行跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 跨域配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

// 允许的前端地址(协议+域名+端口)
config.setAllowedOrigins(Arrays.asList("http://localhost:5173"));

// 允许的HTTP请求方法(必须包含OPTIONS)
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));

// 允许的请求头(*表示全部允许)
config.setAllowedHeaders(Arrays.asList("*"));

// 允许携带凭证(Cookie等),开启时allowedOrigins不能为*
config.setAllowCredentials(true);

// 将配置应用到所有接口
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

回调配置

请求成功回调

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
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
// 认证成功时的回调
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 获取认证主体(通常是用户标识,如用户名、UserDetails对象)
Object principal = authentication.getPrincipal();
// 获取用户权限列表(角色和权限信息)
//Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 获取用户凭证(通常是密码,认证成功后出于安全考虑会清除)
//Object credentials = authentication.getCredentials();

// 封装数据
Map<String, Object> map = new HashMap<>();
map.put("code", "1");
map.put("msg", "登录成功");
map.put("data", principal);

// 转换JSON
String json = JSON.toJSONString(map);

// 响应JSON
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
}
}

请求失败回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 获取异常信息
String localizedMessage = exception.getLocalizedMessage();

// 封装数据
Map<String, Object> map = new HashMap<>();
map.put("code", "-1");
map.put("msg", "用户名或密码错误");

// 转换JSON
String json = JSON.toJSONString(map);

// 响应JSON
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
}
}

注销回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 封装数据
Map<String,Object> map = new HashMap<>();
map.put("code", "0");
map.put("msg", "注销成功");

// 转换JSON
String json = JSON.toJSONString(map);

// 响应JSON
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
}
}

请求未认证的接口的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 通过它也可以获取错误信息
String localizedMessage = authException.getLocalizedMessage();

// 封装数据
Map<String,Object> map = new HashMap<>();
map.put("code", -1);
map.put("msg", "没有登录");
map.put("localizedMessage", localizedMessage);

// 转换JSON
String json = JSON.toJSONString(map);

// 响应JSON
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
}
}

过滤器配置

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
@Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception{

http.authorizeHttpRequests(auth ->
auth.anyRequest().authenticated()
);

// 关闭csrf攻击防御机制
http.csrf(csrf -> csrf.disable());

http.exceptionHandling(exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); // 请求需要认证的接口时
});
// 也可以不用链式调用,直接使用 http. 调用。
http.logout(logout -> {
logout.logoutSuccessHandler(new MyLogoutSuccessHandler());
});

http.formLogin(form->{
form.loginPage("/login").permitAll();
form.failureHandler(new MyAuthenticationFailureHandler()); // 认证失败的回调
form.successHandler(new MyAuthenticationSuccessHandler()); // 认证成功的回调
});

http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

return http.build();
}

获取用户信息

Spring Security 框架中,SecurityContextHolder、SecurityContext、Authentication、Principal 和 Credentials 是与身份验证和授权相关的核心概念。它们之间的关系如下:

  1. SecurityContextHolder:安全上下文持有者,存储在 ThreadLocal 中,线程安全。
  2. SecurityContext:安全上下文,包含并管理当前用户的 Authentication 对象。
  3. Authentication:该对象在安全上下文中扮演两个角色:
    • 在身份验证之前,它代表用户提交的登录请求,其中包含 Principal(通常是用户名)和 Credentials(通常是密码)。
    • 在身份验证之后,它代表已认证的用户,此时 Principal 通常是一个包含更多详细信息的用户对象(如 UserDetails 实例),而 Credentials 出于安全原因会被清除(通常为 null)。此外,它还包含用户的权限信息(GrantedAuthority 集合)。
  4. Principal:通过 Authentication 对象的 getPrincipal() 方法获取,代表主体的身份标识。认证成功后,它是一个能够识别用户的实体对象。
  5. Credentials:代表证明主体身份的凭证,最常见的是密码。认证流程完成后,为了安全起见,框架通常会把这个凭证信息从 Authentication 对象中清除掉,调用 getCredentials() 方法通常返回 null。(密码可以被存储,可以被用于比较,不可用于返回前端)
  6. GrantedAuthority:表示授予主体的权限。认证成功后,可以从 Authentication 对象的 getAuthorities() 方法获取。

controller 类

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
@RestController
public class IndexController {

@GetMapping("/")
public Map<String, Object> index() {

// 1. 从SecurityContextHolder中获取当前线程的安全上下文
SecurityContext context = SecurityContextHolder.getContext();
// 2. 从安全上下文中获取当前用户的认证信息
Authentication authentication = context.getAuthentication();
// 3. 获取当前登录用户的标识(通常是用户名)
String name = authentication.getName();
// 4. 获取代表用户身份的主体对象(认证前通常是用户名,认证后是UserDetails等对象)
Object principal = authentication.getPrincipal();
// 5. 获取用户的凭证(如密码,认证成功后通常返回null)
Object credentials = authentication.getCredentials();
// 6. 获取当前用户被授予的所有权限(角色和权限列表)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

System.out.println(name);
System.out.println(principal);
System.out.println(credentials);
System.out.println(authorities);

// 封装数据
Map<String, Object> map = new HashMap<>();
map.put("code", 0);
map.put("data", name);

// 响应JSON
return map;
}
}

测试的时候先访问/login登录页面,再请求http://localhost:8080/

授权

两种常见的权限控制方式

Spring Security 的授权管理可以实现两种常见的权限控制需求:

第一种:用户 → 权限 → 资源
这种方式比较直接,直接规定每个用户有哪些具体的操作权限。比如:

  • 张三拥有“添加用户”和“查看用户列表”的权限
  • 李四只有“查看用户列表”的权限

第二种:用户 → 角色 → 权限 → 资源
这种方式更加常用,通过角色来管理权限分组。比如:

  • 张三的角色是“管理员”,拥有所有操作权限
  • 李四的角色是“普通用户”,只能查看信息,不能修改

简单来说,第一种是直接给用户分配具体权限,第二种是先给角色分配权限,再把角色分配给用户,这样管理起来更方便。

这里提到的资源是什么?资源就是权限控制所要保护的具体对象。它可以是:

  • 一个URL地址
  • 一个页面元素(如按钮、菜单)
  • 一条数据或一个数据字段

授权策略

Spring Security中提供了两种授权策略:

  • 基于 Request 的授权
  • 基于方法的授权
方面 基于 Request 的授权 基于方法的授权
控制层面 全局层面(过滤器) 业务方法层面(AOP)
控制粒度 粗粒度(按URL分组控制) 细粒度(按具体操作控制)
好比 大楼保安 部门经理
常用场景 控制页面/菜单访问 控制具体业务操作、数据权限

两者通常结合使用:

  • 先用基于 Request 的授权把守大门,阻止非法用户进入敏感区域。
  • 再用基于方法的授权在内部做精细控制,确保用户只能执行被允许的具体操作。

基于 Request 授权的代码实现

用户-权限-资源

需求:

  • 具有 USER_LIST 权限的用户可以访问 /user/list 接口
  • 具有 USER_ADD 权限的用户可以访问 /user/add 接口

第一步:配置哪个权限可以访问哪个资源

先在 SecurityFilterChain 中配好规则:

1
2
3
4
5
6
7
8
9
10
11
12
http.authorizeHttpRequests(
// authorize 是 AuthorizeHttpRequestsConfigurer 对象,用于定义具体的授权规则
authorize -> authorize
// 有 USER_LIST 权限的用户可以访问 /user/list
.requestMatchers("/user/list").hasAuthority("USER_LIST")
// 有 USER_ADD 权限的用户可以访问 /user/add
.requestMatchers("/user/add").hasAuthority("USER_ADD")
// 匹配所有传入的 HTTP 请求
.anyRequest()
// 要求所有请求都必须经过身份认证(用户必须已登录)
.authenticated()
)

第二步:授予哪个用户拥有哪些权限

在DBUserDetailsManager 中的 loadUserByUsername()方法上编写代码:

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
// 通过用户名加载用户的信息。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 封装查询条件:根据用户名获取用户的信息。(MP)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);

// 根据用户名获取用户信息。
User user = userMapper.selectOne(queryWrapper);

if(user == null){
throw new UsernameNotFoundException("Username not found");
}

// 从数据库当中查询这个用户所关联的权限
Collection<GrantedAuthority> authorities = new ArrayList<>();

// 用户拥有了两个权限。
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");

// 用户信息是存在的
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 账户未过期
true, // 认证未过期
true, // 账户未锁定
authorities
);

}

登录响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"msg": "登录成功",
"code": "1",
"data": {
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [
{
"authority": "USER_ADD"
},
{
"authority": "USER_LIST"
}
],
"credentialsNonExpired": true,
"enabled": true,
"username": "zenty"
}
}

去测试 /user/list 和 /user/add 接口是否可以访问。

编写权限不足的回调处理器

handler 类

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
package com.zzh.handler;

import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 封装数据
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("msg", "没有权限");
// 转换JSON
String json = JSON.toJSONString(map);
// 响应JSON
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
}
}

过滤器配置

1
2
3
4
5
http.exceptionHandling(exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
// 这是新增的代码
exception.accessDeniedHandler(new MyAccessDeniedHandler()); // 请求未授权的接口时
});

用户-角色-资源

需求:角色为 ADMIN 的用户才可以访问 /user/** 路径下的资源

配置哪个角色可以访问哪些资源

在 SecurityFilterChain中进行配置,先将之前的权限配置注释掉,进行角色配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http.authorizeHttpRequests(
// authorize 是 AuthorizeHttpRequestsConfigurer 对象,用于定义具体的授权规则
authorize -> authorize
// 有 USER_LIST 权限的用户可以访问 /user/list
//.requestMatchers("/user/list").hasAuthority("USER_LIST")
// 有 USER_ADD 权限的用户可以访问 /user/add
//.requestMatchers("/user/add").hasAuthority("USER_ADD")
// 角色ADMIN可以访问/user/**
.requestMatchers("/user/**").hasRole("ADMIN")
// 匹配所有传入的 HTTP 请求
.anyRequest()
// 要求所有请求都必须经过身份认证(用户必须已登录)
.authenticated()
)

授予哪个用户拥有哪些角色

在实际开发中,用户对应的角色信息同样是存储到数据库表当中的,应该查询数据库,动态获取该用户拥有哪些角色。学习阶段就硬编码了。先把之前授予权限的代码注释掉,再编写以下代码授予角色:

在 DBUserDetailsManager 中的 loadUserByUsername 方法中编写代码:

1
2
3
4
5
6
7
8
9
10
// 不是 return new 了。
// 是通过调用 User类的静态方法了。
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.accountLocked(false) // 用户未锁定
.credentialsExpired(false) // 凭证未过期
.disabled(false) // 账户启用
.roles("ADMIN") // 授予角色(可以授予多个角色) .roles("ADMIN", "USER")
.build();

访问 /login 接口结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"msg": "登录成功",
"code": "1",
"data": {
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [
{
"authority": "ROLE_ADMIN"
}
],
"credentialsNonExpired": true,
"enabled": true,
"username": "zenty"
}
}

去测试 /user/list 和 /user/add 接口是否可以访问

用户-角色-权限-资源

RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的 RBAC 数据库设计方案的示例:

  1. 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
列名 数据类型 描述
user_id int 用户ID
username varchar 用户名
password varchar 密码
email varchar 电子邮件地址
… … …
  1. 角色表(Role table):存储所有可能的角色及其描述。
列名 数据类型 描述
role_id int 角色ID
role_name varchar 角色名称
description varchar 角色描述
… … …
  1. 权限表(Permission table):定义系统中所有可能的权限。
列名 数据类型 描述
permission_id int 权限ID
permission_name varchar 权限名称
description varchar 权限描述
… … …
  1. 用户角色关联表(User-Role table):将用户与角色关联起来。
列名 数据类型 描述
user_role_id int 用户角色关联ID
user_id int 用户ID
role_id int 角色ID
… … …
  1. 角色权限关联表(Role-Permission table):将角色与权限关联起来。
列名 数据类型 描述
role_permission_id int 角色权限关联ID
role_id int 角色ID
permission_id int 权限ID
… … …

在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。

它的好处是可以批量管理:不用给成百上千的用户一个个分配具体权限,只需要给几个角色分配好权限,再把用户放进对应角色里;修改权限时也只需修改角色,所有属于该角色的用户权限会自动更新。

基于方法授权的代码实现

开启方法授权

如果要开启细粒度的方法授权,需要在配置类 WebSecurityConfig 上添加如下注解:

1
@EnableMethodSecurity

去除之前的授权代码

1
2
3
4
5
http.authorizeHttpRequests(auth ->
auth.requestMatchers("/login").permitAll()
// .requestMatchers("/user/*").hasRole("ADMIN")
.anyRequest().authenticated()
);

给用户授予角色

在 DBUserDetailsManager的 loadUserByUsername方法中授予角色:

1
2
3
4
5
6
7
8
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.accountLocked(false) // 用户未锁定
.credentialsExpired(false) // 凭证未过期
.disabled(false) // 账户启用
.roles("ADMIN") // 授予角色
.build();

使用授权注解标注要控制的方法,找到你要精确控制的方法,例如 UserController类中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 用户的角色是ADMIN,并且用户名是admin时,才能访问这个方法。
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'admin'")
@GetMapping("/list")
public List<User> listUser(){
return userService.list();
}

// 用户的角色是USER才可以访问这个方法
@PreAuthorize("hasRole('USER')")
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}

测试:

第一步:使用 admin 用户登录,看看能不能访问 /user/list

第二步:使用 admin 用户登录,看看能不能访问 /user/add

给用户授予权限

在 DBUserDetailsManager的 loadUserByUsername方法中授予权限:

1
2
3
4
5
6
7
8
9
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.accountLocked(false) // 用户未锁定
.credentialsExpired(false) // 凭证未过期
.disabled(false) // 账户启用
//.roles("ADMIN") // 授予角色
.authorities("USER_ADD", "USER_EDIT") // 授予权限(注意授予权限的代码和授予角色的代码不可共存)
.build();

使用授权注解标注要控制的方法,找到你要精确控制的方法,例如 UserController类中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 有 USER_LIST 权限的用户可以访问该方法
@PreAuthorize("hasAuthority('USER_LIST')")
@GetMapping("/list")
public List<User> listUser(){
return userService.list();
}

// 有 USER_ADD 权限的用户可以访问该方法
@PreAuthorize("hasAuthority('USER_ADD')")
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}

测试:

第一步:使用任意用户登录,看看能不能访问 /user/list(预期是不能访问)

第二步:使用任意用户登录,看看能不能访问 /user/add(预期是能访问)

方法上的 SpEL 表达式

针对方法进行授权时,@PreAuthorize 注解的 value 属性值是一个 SpEL 表达式。Spring Security 中常用的表达式如下:

表达式 说明
hasRole('ROLE') 是否有指定角色
hasAnyRole('ROLE1','ROLE2') 是否有任一角色
hasAuthority('AUTH') 是否有指定权限
hasAnyAuthority('AUTH1','AUTH2') 是否有任一权限
authentication 当前认证对象
principal 当前用户主体
#argumentName 方法参数
@beanName.method() 调用Spring Bean方法
returnObject 返回值(PostAuthorize)
permitAll() 永远允许
denyAll() 永远拒绝
isAnonymous() 是否匿名用户
isAuthenticated() 是否已认证
isRememberMe() 是否记住我登录
isFullyAuthenticated() 是否完整认证(非记住我登录)