vue-java-tutorials/spring-security/ReadMe.md

1213 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spring Security 6 入门指南
整个数据库的表构建差不多是这样的也是简化的作为一个小demo讲解。
![image-20250714202213150](./images/image-20250714202213150.png)
## 基本配置
### 添加依赖
在Maven项目中添加Spring Security依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
```
### 基础安全配置
这个注解可以放在任何的配置类或者是启动项上。
创建一个配置类启用Web安全
```java
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
}
```
## 自定义登录配置
> [!IMPORTANT]
>
> 使用自定义页面时必须在控制器中明确指定跳转地址否则Security无法正确路由即使URL正确也无法跳转。
### 启用与禁用选项
- 使用默认登录页:`.formLogin(Customizer.withDefaults())`
- 禁用表单登录:`.formLogin(AbstractHttpConfigurer::disable)`
> 上述不仅适用于登录页。同样也适用于csrf等一些其它组件。
### 配置示例
```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityWebConfiguration {
private final DbUserDetailService dbUserDetailService;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorizeRequests ->
// 访问路径为 /api 时需要进行认证
authorizeRequests
// 只认证 /api/** 下的所有接口
.requestMatchers("/api/**").authenticated()
// 其余请求都放行
.anyRequest().permitAll()
)
.formLogin(loginPage -> loginPage
// 自定义登录页路径
.loginPage("/login-page")
// 处理登录的URL默认就是/login
.loginProcessingUrl("/login")
// 登录成功跳转
.defaultSuccessUrl("/")
// 登录失败跳转
.failureUrl("/login-page?error=true")
.permitAll()
)
// 使用默认的登录
// .formLogin(Customizer.withDefaults())
// 禁用表单登录
// .formLogin(AbstractHttpConfigurer::disable)
.logout(logout -> logout
.logoutSuccessUrl("/login-page?logout=true")
.permitAll()
)
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(configurer -> configurer
// 自定无权访问返回内容
.accessDeniedHandler(new SecurityAccessDeniedHandler())
// 自定义未授权返回内容
.authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
)
.userDetailsService(dbUserDetailService)
;
return http.build();
}
}
```
1. **配置内存用户:**
> [!WARNING]
>
> 可以作为测试使用,或者是应急访问使用,比如管理员账号密码忘了。
>
> 如果是长期使用是不推荐的。
>
> ⚠️ 生产环境通常不推荐常规使用内存用户。
```java
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
String encodedPassword = passwordEncoder.encode("123456");
UserDetails user = User.builder()
.username("user")
.password(encodedPassword)
.roles("USER")
.authorities("read")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(encodedPassword)
.roles("ADMIN")
.authorities("all", "read")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
```
## URL认证与授权配置
> [!NOTE]
>
> 一般在`http.authorizeHttpRequests`配置的都是粗粒度的,在方法上是细粒度的。
>
> 所使用的内容根据业务而定。
### 1. 基本角色认证拦截
**基本介绍**
- 如果匹配的地址较多可以创建数组,之后将数组传入
- 数组可以允许全部通过也可以全部鉴权。
```java
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/api/**").authenticated()
.requestMatchers(permitAllUrls).permitAll()
)
```
> [!NOTE]
>
> 通过`SecurityContextHolder`查看下当前用户的信息。
>
> 详细的可以看下与项目的接口地址:`/api/security/current-user`
1. ##### 单角色配置
配置`/api/**`路径下的所有接口需要`ADMIN`角色才能访问:
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 注意:会自动添加"ROLE_"前缀实际检查的是ROLE_ADMIN
.requestMatchers("/api/**").hasRole("ADMIN")
)
// 其他配置...
;
return http.build();
}
```
##### 多角色配置(满足任一角色即可访问)
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 检查是否有ADMIN或USER角色自动添加ROLE_前缀
.requestMatchers("/api/**").hasAnyRole("ADMIN", "USER")
)
// 其他配置...
;
return http.build();
}
```
### 2. 基于权限的URL访问控制
#### 需要所有指定权限
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 需要同时拥有"all"和"read"权限
.requestMatchers("/api/**").hasAuthority("all")
.requestMatchers("/api/**").hasAuthority("read")
)
// 其他配置...
;
return http.build();
}
```
#### 满足任一权限即可
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 拥有"all"或"read"任一权限即可访问
.requestMatchers("/api/**").hasAnyAuthority("all", "read")
)
// 其他配置...
;
return http.build();
}
```
### 综合配置策略
#### 1. 基本配置模式
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 特定路径需要认证
.requestMatchers("/api/**").authenticated()
// 其他请求全部放行
.anyRequest().permitAll()
)
// 其他配置...
;
return http.build();
}
```
#### 2. 多路径匹配配置
```java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 定义无需认证的白名单路径
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**",
"/.well-known/**", "/favicon.ico",
"/error/**", "/swagger-ui/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorize -> authorize
// API路径需要认证
.requestMatchers("/api/**").authenticated()
// 白名单路径直接放行
.requestMatchers(permitAllUrls).permitAll()
// 其他请求需要登录(非匿名访问)
.anyRequest().authenticated()
)
// 其他配置...
;
return http.build();
}
```
### 完整配置示例
```java
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(permitAllUrls).permitAll()
.requestMatchers("/api/security/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/anonymous/**").anonymous()
.requestMatchers("/api/**").hasAnyAuthority("all", "read")
)
.formLogin(loginPage -> loginPage
.loginPage("/login-page")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login-page?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login-page?logout=true")
.permitAll()
);
return http.build();
}
}
```
## 密码校验器
> [!TIP]
>
> 这个是为下面的UserDetailsService做铺垫的。
密码校验器可以自己实现通常Security为我们提供的就足够使用了如果是一些老项目迁移等可以自定义MD5密码校验器。
```java
/**
* 配置密码编码器Bean
*
* <p>Spring Security提供了多种密码编码器实现推荐使用BCryptPasswordEncoder作为默认选择。</p>
*
* <p>特点:</p>
* <ul>
* <li>BCryptPasswordEncoder - 使用bcrypt强哈希算法自动加盐是当前最推荐的密码编码器</li>
* <li>Argon2PasswordEncoder - 使用Argon2算法抗GPU/ASIC攻击但需要更多内存</li>
* <li>SCryptPasswordEncoder - 使用scrypt算法内存密集型抗硬件攻击</li>
* <li>Pbkdf2PasswordEncoder - 使用PBKDF2算法较老但广泛支持</li>
* </ul>
*
* <p>注意不推荐使用MD5等弱哈希算法Spring官方也不推荐自定义弱密码编码器。</p>
*
* @return PasswordEncoder 密码编码器实例
* @see BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 实际项目中只需返回一个密码编码器
return new BCryptPasswordEncoder();
// 其他编码器示例(根据需求选择一种):
// return new Argon2PasswordEncoder(16, 32, 1, 1 << 14, 2);
// return new SCryptPasswordEncoder();
// return new Pbkdf2PasswordEncoder("secret", 185000, 256);
}
```
### 密码校验器的作用和特点
#### 作用
密码校验器(PasswordEncoder)在Spring Security中负责
1. 密码加密 - 将明文密码转换为不可逆的哈希值
2. 密码验证 - 比较输入的密码与存储的哈希是否匹配
3. 防止密码泄露 - 即使数据库泄露,攻击者也无法轻易获得原始密码
#### 各编码器特点
1. **BCryptPasswordEncoder**
- 使用bcrypt算法
- 自动加盐,防止彩虹表攻击
- 可配置强度参数(默认10)
- 目前最推荐的密码哈希方案
2. **Argon2PasswordEncoder**
- 使用Argon2算法(2015年密码哈希比赛获胜者)
- 抗GPU/ASIC攻击
- 内存密集型,参数配置复杂
- 适合高安全需求场景
3. **SCryptPasswordEncoder**
- 使用scrypt算法
- 内存密集型,抗硬件攻击
- 比bcrypt更抗ASIC攻击
4. **Pbkdf2PasswordEncoder**
- 使用PBKDF2算法
- 较老的算法,但广泛支持
- 需要高迭代次数才安全
#### 最佳实践
**最佳实践是使用BCryptPasswordEncoder**,原因包括:
1. 它是Spring Security默认推荐的编码器
2. 自动处理盐值,无需额外存储
3. 经过充分的安全审查和实际验证
4. 平衡了安全性和性能
5. 广泛支持,易于配置
在Spring Security 5+版本中BCryptPasswordEncoder是官方文档中首推的密码编码器实现。除非有特殊安全需求否则应优先选择它。
### 实现自定义校验器
在Spring Security中自定义密码编码器需要实现`PasswordEncoder`接口。
以下是实现MD5示例及注意事项
```java
/**
* <h1>MD5密码编码器实现</h1>
*
* <strong>安全警告:</strong>此类使用MD5算法进行密码哈希已不再安全不推荐用于生产环境。
*
* <p>MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全。即使密码哈希本身是单向的
* 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行。</p>
*
* <p>Spring Security推荐使用BCrypt、PBKDF2、Argon2或Scrypt等自适应单向函数替代MD5。</p>
*
* @see PasswordEncoder
* 一般仅用于遗留系统兼容,新系统应使用更安全的密码编码器
*/
public class MD5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为null");
}
byte[] md5Digest = DigestUtils.md5Digest(rawPassword.toString().getBytes());
return HexFormat.of().formatHex(md5Digest);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为null");
}
if (!StringUtils.hasText(encodedPassword)) {
return false;
}
return encodedPassword.equalsIgnoreCase(encode(rawPassword));
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
// MD5已不安全始终返回true建议升级到更安全的算法
return true;
}
}
```
## 自定义UserDetailsService
在Spring Security中如果需要自定义用户认证逻辑可以通过实现`UserDetailsService`接口来完成。以下是正确实现方式:
### 标准实现示例
```java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
// 推荐使用构造器注入
public CustomUserDetailsService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 这里应该根据username从数据库或其他存储中查询用户信息
// 以下是模拟数据,实际应用中应从数据库查询
// 2. 如果用户不存在抛出UsernameNotFoundException
if (!"bunny".equalsIgnoreCase(username)) {
throw new UsernameNotFoundException("User not found: " + username);
}
// 3. 构建UserDetails对象返回
return User.builder()
.username(username) // 使用传入的用户名
.password(passwordEncoder.encode("123456")) // 密码应该已经加密存储,这里仅为示例
.roles("USER") // 角色会自动添加ROLE_前缀
.authorities("read", "write") // 添加具体权限
.build();
}
}
```
### 当前用户登录信息
> [!TIP]
>
> 下面是控制器中的方法摘自同步的项目中项目模块是【step2】。
用户的信息都保存在`SecurityContextHolder.getContext()`的上下文中。
```java
/**
* 获取当前认证用户的基本信息
* 使用Spring Security的SecurityContextHolder获取当前认证信息
*/
@Operation(summary = "当前用户的信息", description = "当前用户的信息")
@GetMapping("/current-user")
public Authentication getCurrentUser() {
// 从SecurityContextHolder获取当前认证对象
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 打印当前用户名和权限信息到控制台(用于调试)
System.out.println("Current user: " + auth.getName());
System.out.println("Authorities: " + auth.getAuthorities());
// 返回完整的认证对象
return auth;
}
/**
* 获取当前用户的详细信息
* 从认证主体中提取UserDetails信息
*/
@Operation(summary = "获取用户详情", description = "获取用户详情")
@GetMapping("user-detail")
public UserDetails getCurrentUserDetail() {
// 从SecurityContextHolder获取当前认证对象
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 获取认证主体(principal)
Object principal = auth.getPrincipal();
// 检查主体是否是UserDetails实例
if (principal instanceof UserDetails) {
// 如果是则转换为UserDetails并返回
return (UserDetails) principal;
} else {
// 如果不是UserDetails类型返回null
return null;
}
}
```
### 深入UserDetailsService
> [!IMPORTANT]
>
> 在SpringSecurity6中不用显式的为角色添加`ROLE_`像这样的字符串Security会为我们亲自加上如果加上会有异常抛出`ROLE_USER cannot start with ROLE_ (it is automatically added)...`
```java
// 设置用户权限
return User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword())
.roles(roles)
// 设置用户 authorities
.authorities(authorities)
.build();
```
#### 1. UserDetailsService的核心作用
> [!NOTE]
>
> 如果是前后端分离可以引入JWT这个是前后端不分离的。
`UserDetailsService`是Spring Security的核心接口负责提供用户认证数据。它只有一个核心方法
```java
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
```
> [!WARNING]
>
> 如果是页面方式登录(非前后端分离),如果该用户的权限变化了,需要退出重新登录才会有最新的权限。
>
> 这时候就会出现一个问题,如果当前用户权限被降低了,管理员也修改了这个用户权限,但是信息还是之前的,除非用户退出权限才会刷新。
当用户尝试登录时Spring Security会自动调用这个方法来获取用户详情。
#### 2. 为什么不需要手动校验密码?
在标准的表单登录流程中Spring Security的认证流程会自动处理密码校验这是因为
1. **自动集成密码编码器**
Spring Security会自动使用配置的`PasswordEncoder`来比对:
- 用户提交的明文密码
- 数据库中存储的加密密码(通过`UserDetails`返回)
2. **认证流程内部处理**
认证管理器(`AuthenticationManager`)会自动处理以下流程:
```mermaid
graph TD
A[用户提交凭证] --> B[调用UserDetailsService]
B --> C[获取UserDetails]
C --> D[PasswordEncoder比对密码]
D --> E[认证成功/失败]
```
#### 4. 关键注意事项
1. **必须提供PasswordEncoder**
如果没有配置,会出现`There is no PasswordEncoder mapped`错误
```java
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
```
2. **UserDetails实现要求**
你的自定义`UserDetails`实现必须包含:
- 正确的用户名
- 加密后的密码
- 账号状态信息(是否过期/锁定等)
3. **自动发现机制**
当以下条件满足时Spring Boot会自动配置
- 容器中存在唯一的`UserDetailsService`实现
- 存在`PasswordEncoder` bean
- 没有显式配置`AuthenticationManager`
#### 总结对比表
| 场景 | 需要手动处理 | 自动处理 |
| ------------ | ---------------------------------- | ----------------------- |
| 用户查找 | 实现`loadUserByUsername()` | ✅ |
| 密码比对 | ❌ | 由`PasswordEncoder`处理 |
| 账号状态检查 | 通过`UserDetails`返回的状态 | ✅ |
| 权限加载 | 通过`UserDetails.getAuthorities()` | ✅ |
这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。
## 获取角色与权限
### 1. 角色处理
在`UserDetailsService`实现中获取并设置用户角色:
```java
@DS("testJwt")
@Service
@Transactional
@RequiredArgsConstructor
public class DbUserDetailService implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询当前用户
UserEntity userEntity = userMapper.selectByUsername(username);
// 判断当前用户是否存在
if (userEntity == null) {
throw new UsernameNotFoundException("用户不存在");
}
Long userId = userEntity.getId();
// 设置用户角色
String[] roles = findUserRolesByUserId(userId);
// 设置用户权限
List<String> permissionsByUserId = findPermissionByUserId(userId);
String[] permissions = permissionsByUserId.toArray(String[]::new);
// 也可以转成下面的形式
// List<String> permissions = permissionsByUserId.stream()
// .map(SimpleGrantedAuthority::new)
// .toList();
String[] authorities = ArrayUtils.addAll(roles, permissions);
// 设置用户权限
return User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword())
// 设置用户 authorities
.authorities(authorities)
.roles(roles)
.build();
}
/**
* 根据用户id查找该用户的角色内容
*
* @param userId 用户id
* @return 当前用户的角色信息
*/
public String[] findUserRolesByUserId(Long userId) {
List<RoleEntity> roleList = userMapper.selectRolesByUserId(userId);
return roleList.stream().map(RoleEntity::getRoleCode).toArray(String[]::new);
}
/**
* 根据用户id查找该用户的权限内容
*
* @param userId 用户id
* @return 当前用户的权限信息
*/
public List<String> findPermissionByUserId(Long userId) {
List<PermissionEntity> permissionList = userMapper.selectPermissionByUserId(userId);
return permissionList.stream().map(PermissionEntity::getPermissionCode).toList();
}
}
```
**关键说明**
- `roles()`方法会自动为角色添加`ROLE_`前缀(如`ADMIN`会变成`ROLE_ADMIN`
- 角色和权限在Spring Security中是不同概念角色本质是带有特殊前缀的权限
### 2. 权限处理(两种方式)
**方式一:直接使用字符串**
```java
List<String> permissions = findPermissionsByUserId(userId);
return User.withUsername(username)
.authorities(permissions.toArray(new String[0]))
// ...
.build();
```
**方式二转换为SimpleGrantedAuthority**
```java
List<GrantedAuthority> authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return User.withUsername(username)
.authorities(authorities)
// ...
.build();
```
**权限实现类对比**
| 实现类 | 适用场景 | 特点 |
| ---------------------------- | ------------- | ---------------- |
| `SimpleGrantedAuthority` | 普通权限/角色 | 最常用实现 |
| `SwitchUserGrantedAuthority` | 用户切换场景 | 包含原始用户信息 |
| `JaasGrantedAuthority` | JAAS集成 | 用于Java认证服务 |
### 3. Mapper示例
**角色查询Mapper**
```xml
<select id="selectRolesByUserId" resultType="com.example.entity.Role">
SELECT r.*
FROM t_role r
JOIN t_user_role ur ON r.id = ur.role_id
WHERE ur.user_id = #{userId}
</select>
```
**权限查询Mapper**
```xml
<select id="selectPermissionsByUserId" resultType="java.lang.String">
SELECT p.permission_code
FROM t_permission p
JOIN t_role_permission rp ON p.id = rp.permission_id
JOIN t_user_role ur ON rp.role_id = ur.role_id
WHERE ur.user_id = #{userId}
</select>
```
## 方法资源认证配置
通过在任何 `@Configuration` 类上添加 `@EnableMethodSecurity` 注解。
Spring Boot Starter Security 默认情况下不会激活方法级别的授权。
**角色与权限的区别**
- `hasRole()`会自动添加"ROLE_"前缀
- `hasAuthority()`直接使用指定的权限字符串
**匹配顺序**
- Spring Security会按照配置的顺序进行匹配
- 更具体的路径应该放在前面通用规则如anyRequest放在最后
**方法选择建议**
- `hasRole()`/`hasAnyRole()`:适合基于角色的访问控制
- `hasAuthority()`/`hasAnyAuthority()`:适合更细粒度的权限控制
- `authenticated()`:只需认证通过,不检查具体角色/权限
- `permitAll()`:完全开放访问
**提供的注解**
1. @PreAuthorize
2. @PostAuthorize
3. @PreFilter
4. @PostFilter
**最佳实践**
- 对于REST API通常使用`authenticated()`配合方法级权限控制
- 静态资源应明确配置`permitAll()`
- 生产环境不建议使用`anyRequest().permitAll()`
### 1. 基础使用说明
> [!IMPORTANT]
>
> 使用权限注解时需注意:
>
> - `hasAuthority()` 严格区分大小写
> - 类级别注解会被方法级别注解覆盖
> - 默认需要启用注解支持:`@EnableMethodSecurity`
如果类上面也加了注解,方法也加了,那么方法的会覆盖掉类上的。
```java
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.data == authentication.name")
@Operation(summary = "分页查询系统角色表", description = "分页系统角色表")
@GetMapping("{page}/{limit}")
public Result<PageResult<RoleVo>> getRolePage(
// ...
}
```
### 2. 前置与后置授权对比
#### 1. @PreAuthorize
- **执行时机**:在方法执行**之前**进行权限检查
- **行为**
- 如果当前用户没有满足注解中指定的权限条件,方法**不会被执行**,直接抛出`AccessDeniedException`
- 这是一种"先验"的权限检查方式,可以防止无权限用户触发方法执行
- **典型用途**:适用于方法执行前的权限验证,特别是当方法执行可能有副作用(如修改数据)时
```java
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// 只有ADMIN角色可以执行此方法
// 如果不是ADMIN代码不会执行到这里
}
```
#### 2. @PostAuthorize
- **执行时机**:在方法执行**之后**进行权限检查
- **行为**
- 方法**会先完整执行**,然后在返回结果前检查权限
- 如果权限检查不通过,同样会抛出`AccessDeniedException`,但方法已经执行完毕
- 可以基于方法的返回值进行权限判断(使用`returnObject`引用返回值)
- **典型用途**:适用于需要根据方法返回结果决定是否允许访问的情况
```java
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long docId) {
// 方法会先执行
// 返回前检查文档所有者是否是当前用户
return documentRepository.findById(docId);
}
```
**如果需要关闭**
如果不需要这种注解,可以按照下面的方式进行关闭。
```java
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
}
```
#### 关键区别总结
如果使用`PostAuthorize`注解,但是服务中没有标记事务注解,那么会将整个方法全部执行,即使没有权限也不会回滚。
**默认情况下Spring 事务会对未捕获的 `RuntimeException` 进行回滚**,因此:
- 如果事务仍然活跃(未提交),则会回滚。
- 但如果事务已经提交(例如方法执行完毕且事务已提交),则**不会回滚**。
1. **优先使用@PreAuthorize**:除非你需要基于返回值做判断,否则应该使用`@PreAuthorize`,因为它能更早地阻止未授权访问
2. **注意方法副作用**:使用`@PostAuthorize`时要特别注意,即使最终会拒绝访问,方法中的所有代码(包括数据库修改等操作)都已经执行了
3. **组合使用**:有时可以组合使用两者,先用`@PreAuthorize`做基本权限检查,再用`@PostAuthorize`做更精细的检查
4. **性能敏感场景**:对于性能敏感或可能产生副作用的方法,避免使用`@PostAuthorize`
| 特性 | @PreAuthorize | @PostAuthorize |
| ---------------- | -------------------- | ------------------------------ |
| 执行时机 | 方法执行前 | 方法执行后 |
| 方法是否会被执行 | 不满足条件时不执行 | 总是执行 |
| 可访问的上下文 | 方法参数 | 方法参数和返回值(returnObject) |
| 性能影响 | 更好(避免不必要执行) | 稍差(方法总会执行) |
| 主要用途 | 防止未授权访问 | 基于返回值的访问控制 |
#### 使用示例
```java
@Tag(name = "测试接口", description = "测试用的接口")
@Slf4j
@RestController
@RequestMapping("/api/test")
public class TestController {
@PreAuthorize("hasAuthority('role::read')")
@Operation(summary = "拥有 role:read 的角色可以访问", description = "当前用户拥有 role:read 角色可以访问这个接口")
@GetMapping("role-user")
public Result<String> roleUser() {
return Result.success();
}
@PreAuthorize("hasAuthority('USER')")
@Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口")
@GetMapping("upper-user")
public Result<String> upperUser() {
String data = "是区分大小写的";
return Result.success(data);
}
@PreAuthorize("hasAuthority('user')")
@Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口")
@GetMapping("lower-user")
public Result<String> lowerUser() {
String data = "如果是大写,但是在这里是小写无法访问";
return Result.success(data);
}
@PostAuthorize("returnObject.data == authentication.name")
@Operation(summary = "测试使用返回参数判断权限", description = "测试使用返回参数判断权限 用户拥有 role::read 可以访问这个接口")
@GetMapping("test-post-authorize")
public Result<String> testPostAuthorize() {
log.info("方法内容已经执行。。。");
String data = "Bunny";
return Result.success(data);
}
}
```
### 3. 高级用法
#### 元注解封装
```java
// 管理员权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface AdminOnly {}
// 资源所属校验注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.ownerId == authentication.principal.id")
public @interface ResourceOwner {}
```
#### 模板化注解
```java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('USER') or hasAuthority(#permission)")
public @interface UserOrPermission {
String permission();
}
// 使用示例
@UserOrPermission(permission = "report:read")
public Report getReport(Long id) { ... }
```
**自定义Any模板元注解**
> [!WARNING]
>
> SpringSecurity6.3.10版本与最新版的6.5.1写法不一样。
如果需要自定义任意权限都可通过需要引入下面的内容。
```java
@Bean
static PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
```
**示例**
```java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyAuthority({auth})")
public @interface HasAnyAuthority {
String[] auth();
}
@HasAnyAuthority(auth = {"'USER'", "'ADMIN'"})
@Operation(summary = "拥有 HasAnyXXX 的角色可以访问", description = "当前用户拥有 HasAnyXXX 角色可以访问这个接口")
@GetMapping("role-user")
public Result<String> roleUser() {
return Result.success();
}
```
### 4. 其他注解
#### JSR-250注解
需显式启用:
```java
@EnableMethodSecurity(jsr250Enabled = true)
```
提供注解:
- `@RolesAllowed("ROLE")` - 等效于`@PreAuthorize("hasRole('ROLE')")`
- `@PermitAll` - 允许所有访问
- `@DenyAll` - 拒绝所有访问
#### 已废弃注解
- `@Secured` 在Spring Security 6中已废弃如需使用需显式启用
```java
@EnableMethodSecurity(securedEnabled = true)
```
### 5. 示例
```java
@Tag(name = "权限测试接口")
@RestController
@RequestMapping("/api/auth-test")
public class AuthTestController {
// 精确权限控制
@AdminOnly
@GetMapping("/admin")
public Result<String> adminEndpoint() {
return Result.success("Admin access");
}
// 复合权限检查
@PreAuthorize("hasAnyAuthority('DATA_READ', 'REPORT_READ')")
@GetMapping("/reports")
public Result<List<Report>> getReports() {
return Result.success(reportService.getAll());
}
// 返回值校验
@ResourceOwner
@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
return docService.getById(id); // 执行后校验所有者
}
// 模板注解使用
@UserOrPermission(permission = "audit:read")
@GetMapping("/audit-logs")
public Result<PageResult<AuditLog>> getAuditLogs(Pageable pageable) {
return Result.success(auditService.getLogs(pageable));
}
}
```
## 通过编程方式授权方法
如果需要对权限做出自定义的需求,将传入参数作为判断权限条件,这会很有用,比如某些参数不可以传入,或者参数做权限校验等。
首先创建一个Spring组件包含自定义的授权逻辑
```java
@Component("auth")
public class AuthorizationLogic {
/**
* 自定义授权决策方法
* @param name 要检查的名称
* @return 如果授权通过返回true否则返回false
*/
public boolean decide(String name) {
// 示例逻辑仅当name为"user"(不区分大小写)时授权通过
return name.equalsIgnoreCase("user");
}
/**
* 更复杂的授权逻辑示例
* @param id 资源ID
* @param currentUsername 当前认证用户名
* @return 授权结果
*/
public boolean checkResourceAccess(Long id, String currentUsername) {
// 这里可以添加数据库查询等复杂逻辑
return id != null && id > 0 && currentUsername != null;
}
}
```
在控制器中使用
```java
@PreAuthorize("@auth.decide(#name)")
@Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口")
@GetMapping("lower-user")
public Result<String> lowerUser(String name) {
return Result.success(name);
}
```
## 使用自定义授权管理器
在实际开发中对于SpringSecurity提供的两个权限校验注解`@PreAuthorize`和`@PostAuthorize`,需要对这两个进行覆盖或者改造,需要实现两个`AuthorizationManager<T>`。
实现完成后需要显式的在配置中禁用原先的内容。
### 1. 实现前置
在方法中写入自己的校验逻辑。
```java
/**
* 处理方法调用后的授权检查
* check()方法接收的是MethodInvocationResult对象包含已执行方法的结果
* 用于决定是否允许返回某个方法的结果(后置过滤)
* 这是Spring Security较新的"后置授权"功能
*/
@Component
public class PostAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
return new AuthorizationDecision(true);
}
}
```
### 2. 实现后置
```java
/**
* 处理方法调用前的授权检查
* check()方法接收的是MethodInvocation对象包含即将执行的方法调用信息
* 用于决定是否允许执行某个方法
* 这是传统的"前置授权"模式
*/
@Component
public class PreAuthorizationManager implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
return new AuthorizationDecision(true);
}
}
```
### 3. 禁用自带的
需要加上注解`@EnableMethodSecurity(prePostEnabled = false)`。
```java
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
public class AuthorizationManagerConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(PreAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(PostAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}
```
## 将方法与自定义切入点相匹配
由于是基于 Spring AOP 构建的,您可以声明与注解无关的模式,类似于请求级别的授权。 这具有将方法级别的授权规则集中化的潜在优势。
例如,可以发布自己的 `Advisor` 或使用 `<protect-pointcut>` 将 AOP 表达式与服务层的授权规则相匹配,如下所示:
```java
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
```