2025-07-10 22:50:19 +08:00
|
|
|
|
# Spring Security 6 入门指南
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
整个数据库的表构建差不多是这样的,也是简化的,作为一个小demo讲解。
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|

|
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
## 基本配置
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
### 添加依赖
|
2025-07-15 20:20:44 +08:00
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
在Maven项目中添加Spring Security依赖:
|
2025-07-10 21:28:45 +08:00
|
|
|
|
```xml
|
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
|
|
<artifactId>spring-boot-starter-security</artifactId>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
### 基础安全配置
|
2025-07-15 20:20:44 +08:00
|
|
|
|
|
|
|
|
|
这个注解可以放在任何的配置类或者是启动项上。
|
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
创建一个配置类启用Web安全:
|
2025-07-10 21:28:45 +08:00
|
|
|
|
```java
|
|
|
|
|
@EnableWebSecurity
|
|
|
|
|
@Configuration
|
|
|
|
|
public class SecurityWebConfiguration {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
## 自定义登录配置
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!IMPORTANT]
|
|
|
|
|
>
|
|
|
|
|
> 使用自定义页面时,必须在控制器中明确指定跳转地址,否则Security无法正确路由,即使URL正确也无法跳转。
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
### 启用与禁用选项
|
|
|
|
|
- 使用默认登录页:`.formLogin(Customizer.withDefaults())`
|
|
|
|
|
- 禁用表单登录:`.formLogin(AbstractHttpConfigurer::disable)`
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> 上述不仅适用于登录页。同样也适用于csrf等一些其它组件。
|
|
|
|
|
|
|
|
|
|
### 配置示例
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
|
|
|
|
```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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
1. **配置内存用户:**
|
2025-07-10 22:42:36 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!WARNING]
|
2025-07-10 22:42:36 +08:00
|
|
|
|
>
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> 可以作为测试使用,或者是应急访问使用,比如管理员账号密码忘了。
|
|
|
|
|
>
|
|
|
|
|
> 如果是长期使用是不推荐的。
|
|
|
|
|
>
|
|
|
|
|
> ⚠️ 生产环境通常不推荐常规使用内存用户。
|
2025-07-10 22:42:36 +08:00
|
|
|
|
|
|
|
|
|
```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);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
## 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();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 满足任一权限即可
|
|
|
|
|
|
2025-07-10 22:42:36 +08:00
|
|
|
|
```java
|
2025-07-15 20:20:44 +08:00
|
|
|
|
@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();
|
|
|
|
|
}
|
2025-07-10 21:28:45 +08:00
|
|
|
|
```
|
|
|
|
|
|
2025-07-10 22:50:19 +08:00
|
|
|
|
### 完整配置示例
|
2025-07-10 21:28:45 +08:00
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@EnableMethodSecurity
|
|
|
|
|
@EnableWebSecurity
|
|
|
|
|
@Configuration
|
|
|
|
|
public class SecurityWebConfiguration {
|
|
|
|
|
|
|
|
|
|
@Bean
|
|
|
|
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
|
|
|
String[] permitAllUrls = {
|
2025-07-10 22:42:36 +08:00
|
|
|
|
"/", "/doc.html/**",
|
|
|
|
|
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
|
|
|
|
|
"/v3/api-docs/**"
|
2025-07-10 21:28:45 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
http.authorizeHttpRequests(authorizeRequests ->
|
2025-07-10 22:42:36 +08:00
|
|
|
|
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()
|
|
|
|
|
);
|
2025-07-10 21:28:45 +08:00
|
|
|
|
return http.build();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-10 22:42:36 +08:00
|
|
|
|
```
|
2025-07-10 22:50:19 +08:00
|
|
|
|
|
|
|
|
|
## 密码校验器
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!TIP]
|
|
|
|
|
>
|
|
|
|
|
> 这个是为下面的UserDetailsService做铺垫的。
|
|
|
|
|
|
|
|
|
|
密码校验器可以自己实现,通常Security为我们提供的就足够使用了,如果是一些老项目迁移等,可以自定义MD5密码校验器。
|
|
|
|
|
|
2025-07-10 22:50:19 +08:00
|
|
|
|
```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算法
|
|
|
|
|
- 较老的算法,但广泛支持
|
|
|
|
|
- 需要高迭代次数才安全
|
|
|
|
|
|
2025-07-11 09:29:13 +08:00
|
|
|
|
#### 最佳实践
|
2025-07-10 22:50:19 +08:00
|
|
|
|
|
|
|
|
|
**最佳实践是使用BCryptPasswordEncoder**,原因包括:
|
|
|
|
|
|
|
|
|
|
1. 它是Spring Security默认推荐的编码器
|
|
|
|
|
2. 自动处理盐值,无需额外存储
|
|
|
|
|
3. 经过充分的安全审查和实际验证
|
|
|
|
|
4. 平衡了安全性和性能
|
|
|
|
|
5. 广泛支持,易于配置
|
|
|
|
|
|
2025-07-11 09:29:13 +08:00
|
|
|
|
在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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-11 13:43:45 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 自定义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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-11 16:19:56 +08:00
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 当前用户登录信息
|
|
|
|
|
|
|
|
|
|
> [!TIP]
|
|
|
|
|
>
|
|
|
|
|
> 下面是控制器中的方法摘自同步的项目中,项目模块是【step2】。
|
2025-07-11 16:19:56 +08:00
|
|
|
|
|
|
|
|
|
用户的信息都保存在`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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-14 19:37:32 +08:00
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 深入UserDetailsService
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!IMPORTANT]
|
2025-07-14 23:10:58 +08:00
|
|
|
|
>
|
|
|
|
|
> 在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();
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|
#### 1. UserDetailsService的核心作用
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!NOTE]
|
|
|
|
|
>
|
|
|
|
|
> 如果是前后端分离可以引入JWT,这个是前后端不分离的。
|
|
|
|
|
|
2025-07-14 20:17:06 +08:00
|
|
|
|
`UserDetailsService`是Spring Security的核心接口,负责提供用户认证数据。它只有一个核心方法:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
> [!WARNING]
|
|
|
|
|
>
|
|
|
|
|
> 如果是页面方式登录(非前后端分离),如果该用户的权限变化了,需要退出重新登录才会有最新的权限。
|
|
|
|
|
>
|
|
|
|
|
> 这时候就会出现一个问题,如果当前用户权限被降低了,管理员也修改了这个用户权限,但是信息还是之前的,除非用户退出权限才会刷新。
|
|
|
|
|
|
2025-07-14 20:17:06 +08:00
|
|
|
|
当用户尝试登录时,Spring Security会自动调用这个方法来获取用户详情。
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|
#### 2. 为什么不需要手动校验密码?
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
|
|
|
|
在标准的表单登录流程中,Spring Security的认证流程会自动处理密码校验,这是因为:
|
|
|
|
|
|
|
|
|
|
1. **自动集成密码编码器**:
|
|
|
|
|
Spring Security会自动使用配置的`PasswordEncoder`来比对:
|
|
|
|
|
|
|
|
|
|
- 用户提交的明文密码
|
|
|
|
|
- 数据库中存储的加密密码(通过`UserDetails`返回)
|
|
|
|
|
|
|
|
|
|
2. **认证流程内部处理**:
|
|
|
|
|
认证管理器(`AuthenticationManager`)会自动处理以下流程:
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
graph TD
|
|
|
|
|
A[用户提交凭证] --> B[调用UserDetailsService]
|
|
|
|
|
B --> C[获取UserDetails]
|
|
|
|
|
C --> D[PasswordEncoder比对密码]
|
|
|
|
|
D --> E[认证成功/失败]
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|
#### 4. 关键注意事项
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
|
|
|
|
1. **必须提供PasswordEncoder**:
|
|
|
|
|
如果没有配置,会出现`There is no PasswordEncoder mapped`错误
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@Bean
|
|
|
|
|
PasswordEncoder passwordEncoder() {
|
|
|
|
|
return new BCryptPasswordEncoder();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. **UserDetails实现要求**:
|
|
|
|
|
你的自定义`UserDetails`实现必须包含:
|
|
|
|
|
|
|
|
|
|
- 正确的用户名
|
|
|
|
|
- 加密后的密码
|
|
|
|
|
- 账号状态信息(是否过期/锁定等)
|
|
|
|
|
|
|
|
|
|
3. **自动发现机制**:
|
|
|
|
|
当以下条件满足时,Spring Boot会自动配置:
|
|
|
|
|
|
|
|
|
|
- 容器中存在唯一的`UserDetailsService`实现
|
|
|
|
|
- 存在`PasswordEncoder` bean
|
|
|
|
|
- 没有显式配置`AuthenticationManager`
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|
#### 总结对比表
|
2025-07-14 20:17:06 +08:00
|
|
|
|
|
|
|
|
|
| 场景 | 需要手动处理 | 自动处理 |
|
|
|
|
|
| ------------ | ---------------------------------- | ----------------------- |
|
|
|
|
|
| 用户查找 | 实现`loadUserByUsername()` | ✅ |
|
|
|
|
|
| 密码比对 | ❌ | 由`PasswordEncoder`处理 |
|
|
|
|
|
| 账号状态检查 | 通过`UserDetails`返回的状态 | ✅ |
|
|
|
|
|
| 权限加载 | 通过`UserDetails.getAuthorities()` | ✅ |
|
|
|
|
|
|
2025-07-14 20:34:16 +08:00
|
|
|
|
这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
## 获取角色与权限
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 1. 角色处理
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
|
|
|
|
在`UserDetailsService`实现中获取并设置用户角色:
|
|
|
|
|
|
|
|
|
|
```java
|
2025-07-15 20:20:44 +08:00
|
|
|
|
@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();
|
2025-07-14 20:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
/**
|
|
|
|
|
* 根据用户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();
|
|
|
|
|
}
|
2025-07-14 20:34:16 +08:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**关键说明**:
|
|
|
|
|
|
|
|
|
|
- `roles()`方法会自动为角色添加`ROLE_`前缀(如`ADMIN`会变成`ROLE_ADMIN`)
|
|
|
|
|
- 角色和权限在Spring Security中是不同概念,角色本质是带有特殊前缀的权限
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 2. 权限处理(两种方式)
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
|
|
|
|
**方式一:直接使用字符串**
|
|
|
|
|
|
|
|
|
|
```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认证服务 |
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 3. Mapper示例
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
|
|
|
|
**角色查询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>
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
## 方法资源认证配置
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
通过在任何 `@Configuration` 类上添加 `@EnableMethodSecurity` 注解。
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
Spring Boot Starter Security 默认情况下不会激活方法级别的授权。
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**角色与权限的区别**:
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
- `hasRole()`会自动添加"ROLE_"前缀
|
|
|
|
|
- `hasAuthority()`直接使用指定的权限字符串
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**匹配顺序**:
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
- Spring Security会按照配置的顺序进行匹配
|
|
|
|
|
- 更具体的路径应该放在前面,通用规则(如anyRequest)放在最后
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**方法选择建议**:
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
- `hasRole()`/`hasAnyRole()`:适合基于角色的访问控制
|
|
|
|
|
- `hasAuthority()`/`hasAnyAuthority()`:适合更细粒度的权限控制
|
|
|
|
|
- `authenticated()`:只需认证通过,不检查具体角色/权限
|
|
|
|
|
- `permitAll()`:完全开放访问
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**提供的注解**
|
2025-07-14 20:34:16 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
1. @PreAuthorize
|
|
|
|
|
2. @PostAuthorize
|
|
|
|
|
3. @PreFilter
|
|
|
|
|
4. @PostFilter
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**最佳实践**:
|
|
|
|
|
|
|
|
|
|
- 对于REST API,通常使用`authenticated()`配合方法级权限控制
|
|
|
|
|
- 静态资源应明确配置`permitAll()`
|
|
|
|
|
- 生产环境不建议使用`anyRequest().permitAll()`
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
|
|
|
|
### 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(
|
2025-07-15 20:20:44 +08:00
|
|
|
|
// ...
|
2025-07-14 23:10:58 +08:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**如果需要关闭**
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
如果不需要这种注解,可以按照下面的方式进行关闭。
|
|
|
|
|
|
2025-07-14 23:10:58 +08:00
|
|
|
|
```java
|
|
|
|
|
@Configuration
|
|
|
|
|
@EnableMethodSecurity(prePostEnabled = false)
|
|
|
|
|
class MethodSecurityConfig {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 关键区别总结
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
如果使用`PostAuthorize`注解,但是服务中没有标记事务注解,那么会将整个方法全部执行,即使没有权限也不会回滚。
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
**默认情况下,Spring 事务会对未捕获的 `RuntimeException` 进行回滚**,因此:
|
|
|
|
|
|
|
|
|
|
- 如果事务仍然活跃(未提交),则会回滚。
|
|
|
|
|
- 但如果事务已经提交(例如方法执行完毕且事务已提交),则**不会回滚**。
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
|
|
|
|
1. **优先使用@PreAuthorize**:除非你需要基于返回值做判断,否则应该使用`@PreAuthorize`,因为它能更早地阻止未授权访问
|
|
|
|
|
|
|
|
|
|
2. **注意方法副作用**:使用`@PostAuthorize`时要特别注意,即使最终会拒绝访问,方法中的所有代码(包括数据库修改等操作)都已经执行了
|
|
|
|
|
|
|
|
|
|
3. **组合使用**:有时可以组合使用两者,先用`@PreAuthorize`做基本权限检查,再用`@PostAuthorize`做更精细的检查
|
|
|
|
|
|
|
|
|
|
4. **性能敏感场景**:对于性能敏感或可能产生副作用的方法,避免使用`@PostAuthorize`
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
| 特性 | @PreAuthorize | @PostAuthorize |
|
|
|
|
|
| ---------------- | -------------------- | ------------------------------ |
|
|
|
|
|
| 执行时机 | 方法执行前 | 方法执行后 |
|
|
|
|
|
| 方法是否会被执行 | 不满足条件时不执行 | 总是执行 |
|
|
|
|
|
| 可访问的上下文 | 方法参数 | 方法参数和返回值(returnObject) |
|
|
|
|
|
| 性能影响 | 更好(避免不必要执行) | 稍差(方法总会执行) |
|
|
|
|
|
| 主要用途 | 防止未授权访问 | 基于返回值的访问控制 |
|
|
|
|
|
|
|
|
|
|
#### 使用示例
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
|
|
|
|
```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) { ... }
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 09:19:43 +08:00
|
|
|
|
**自定义Any模板元注解**
|
|
|
|
|
|
2025-07-15 10:02:57 +08:00
|
|
|
|
> [!WARNING]
|
|
|
|
|
>
|
|
|
|
|
> SpringSecurity6.3.10版本与最新版的6.5.1写法不一样。
|
|
|
|
|
|
2025-07-15 09:19:43 +08:00
|
|
|
|
如果需要自定义任意权限都可通过需要引入下面的内容。
|
|
|
|
|
|
|
|
|
|
```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();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 4. 其他注解
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
|
|
|
|
#### JSR-250注解
|
|
|
|
|
|
|
|
|
|
需显式启用:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@EnableMethodSecurity(jsr250Enabled = true)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
提供注解:
|
|
|
|
|
|
|
|
|
|
- `@RolesAllowed("ROLE")` - 等效于`@PreAuthorize("hasRole('ROLE')")`
|
|
|
|
|
- `@PermitAll` - 允许所有访问
|
|
|
|
|
- `@DenyAll` - 拒绝所有访问
|
|
|
|
|
|
|
|
|
|
#### 已废弃注解
|
|
|
|
|
|
|
|
|
|
- `@Secured` 在Spring Security 6中已废弃,如需使用需显式启用:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@EnableMethodSecurity(securedEnabled = true)
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
### 5. 示例
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
|
|
|
|
```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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
## 通过编程方式授权方法
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
如果需要对权限做出自定义的需求,将传入参数作为判断权限条件,这会很有用,比如某些参数不可以传入,或者参数做权限校验等。
|
2025-07-14 23:10:58 +08:00
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
首先创建一个Spring组件,包含自定义的授权逻辑:
|
2025-07-15 10:02:57 +08:00
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@Component("auth")
|
|
|
|
|
public class AuthorizationLogic {
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
/**
|
|
|
|
|
* 自定义授权决策方法
|
|
|
|
|
* @param name 要检查的名称
|
|
|
|
|
* @return 如果授权通过返回true,否则返回false
|
|
|
|
|
*/
|
2025-07-15 10:02:57 +08:00
|
|
|
|
public boolean decide(String name) {
|
2025-07-15 20:20:44 +08:00
|
|
|
|
// 示例逻辑:仅当name为"user"(不区分大小写)时授权通过
|
2025-07-15 10:02:57 +08:00
|
|
|
|
return name.equalsIgnoreCase("user");
|
|
|
|
|
}
|
2025-07-15 20:20:44 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更复杂的授权逻辑示例
|
|
|
|
|
* @param id 资源ID
|
|
|
|
|
* @param currentUsername 当前认证用户名
|
|
|
|
|
* @return 授权结果
|
|
|
|
|
*/
|
|
|
|
|
public boolean checkResourceAccess(Long id, String currentUsername) {
|
|
|
|
|
// 这里可以添加数据库查询等复杂逻辑
|
|
|
|
|
return id != null && id > 0 && currentUsername != null;
|
|
|
|
|
}
|
2025-07-15 10:02:57 +08:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在控制器中使用
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@PreAuthorize("@auth.decide(#name)")
|
|
|
|
|
@Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口")
|
|
|
|
|
@GetMapping("lower-user")
|
|
|
|
|
public Result<String> lowerUser(String name) {
|
|
|
|
|
return Result.success(name);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 17:11:18 +08:00
|
|
|
|
## 使用自定义授权管理器
|
|
|
|
|
|
2025-07-15 20:20:44 +08:00
|
|
|
|
在实际开发中对于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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-07-15 17:11:18 +08:00
|
|
|
|
## 将方法与自定义切入点相匹配
|
|
|
|
|
|
|
|
|
|
由于是基于 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"))
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|