Spring Security + JWT + @PreAuthorize 实战指南
本文介绍了在Spring Security + JWT架构下使用@PreAuthorize实现权限控制的三种方案。首先给出了基础配置,包括必要的依赖项。重点展示了方案一的实现细节:通过自定义UserDetailsService从数据库加载用户角色和权限,并转换为Spring Security的GrantedAuthority格式(角色需添加"ROLE_"前缀);同时JWT工具类
·
前言
本文介绍在 Spring Security + JWT 架构中使用 @PreAuthorize 实现三种权限控制方案的完整实现方法,适用于 JDK 1.8 环境。
基础配置:JWT + Spring Security
依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT (JWT 生成与解析) - JDK 1.8 兼容版本 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
方案一:基于框架原生权限控制
1. 自定义用户详情服务(关键修改点)
package com.xxx.security.service;
import com.xxx.security.entity.Permission;
import com.xxx.security.entity.Role;
import com.xxx.security.entity.User;
import com.xxx.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 自定义用户详情服务
*
* 关键作用:从数据库加载用户的角色和权限,转换为 Spring Security 的 GrantedAuthority 格式
*
* 修改要点:
* 1. 角色转换为 "ROLE_" + roleName 格式(如:ADMIN -> ROLE_ADMIN)
* 2. 权限保持原样(如:USER_READ)
* 3. JWT Token 会包含这些权限信息
*/
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
// 2. 收集用户的所有权限(来自角色和直接分配的权限)
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 3. 处理角色权限
if (user.getRoles() != null) {
for (Role role : user.getRoles()) {
// 【关键】角色权限:hasRole("ADMIN") 会匹配 ROLE_ADMIN
// 必须添加 "ROLE_" 前缀,否则 hasRole() 无法识别
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
// 角色包含的具体权限
if (role.getPermissions() != null) {
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
}
}
}
}
// 4. 处理用户直接分配的权限(可选)
if (user.getPermissions() != null) {
for (Permission permission : user.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
}
}
// 5. 构建 Spring Security 的 UserDetails 对象
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities) // 【关键】设置权限列表
.build();
}
}
2. JWT 工具类(关键修改点)
package com.xxx.security.service;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* JWT 工具服务类
*
* 关键作用:生成包含用户权限信息的 JWT Token
*
* 修改要点:
* 1. 将用户的权限列表存储到 JWT 的 claims 中
* 2. JWT 认证过滤器会从 Token 中提取这些权限
*/
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400000}")
private Long expiration;
/**
* 生成 JWT Token
*
* 【关键】将用户的权限列表存储到 JWT 的自定义声明中
* 这样 JWT 过滤器就能从 Token 中提取权限信息
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 【关键】提取用户权限并转换为字符串列表
List<String> authorities = userDetails.getAuthorities().stream()
.map(authority -> authority.getAuthority())
.collect(Collectors.toList());
// 【关键】将权限列表存入 JWT 的自定义声明中
claims.put("authorities", authorities);
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
/**
* 从 JWT Token 中提取权限列表
*
* 【关键】JWT 过滤器会调用此方法提取权限信息
*/
@SuppressWarnings("unchecked")
public List<String> extractAuthorities(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return (List<String>) claims.get("authorities");
}
/**
* 验证 JWT Token 是否有效
*/
public boolean validateToken(String token, UserDetails userDetails) {
try {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
/**
* 从 JWT Token 中提取用户名
*/
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
/**
* 检查 Token 是否过期
*/
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
}
3. JWT 认证过滤器(关键修改点)
package com.xxx.security.filter;
import com.xxx.security.service.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT 认证过滤器
*
* 关键作用:从 JWT Token 中提取权限信息,设置到 SecurityContext
*
* 修改要点:
* 1. 从 JWT Token 中提取权限列表
* 2. 将权限列表转换为 GrantedAuthority 对象
* 3. 将认证信息设置到 SecurityContext(@PreAuthorize 从这里读取权限)
*/
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 1. 从请求头中提取 Authorization 信息
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
try {
// 2. 从 Token 中提取用户名
username = jwtService.extractUsername(jwt);
// 3. 检查用户名是否存在且当前未认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 4. 加载用户详情
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 验证 Token 是否有效
if (jwtService.validateToken(jwt, userDetails)) {
// 6. 【关键】从 Token 中提取权限列表
List<String> authorities = jwtService.extractAuthorities(jwt);
// 7. 【关键】将权限字符串转换为 GrantedAuthority 对象
List<SimpleGrantedAuthority> grantedAuthorities = authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 8. 【关键】创建认证对象,包含权限列表
// 这样后续的 @PreAuthorize 就能从 SecurityContext 读取权限
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, // principal: 用户详情
null, // credentials: JWT 不需要密码
grantedAuthorities // 【关键】权限列表
);
// 9. 设置认证详情
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 10. 【关键】将认证对象设置到 SecurityContext
// @PreAuthorize 注解会从这个 Context 中读取权限信息
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
logger.error("JWT Token 验证失败: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
}
4. 安全配置类(关键修改点)
package com.xxx.security.config;
import com.xxx.security.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 主配置类
*
* 【关键】必须启用方法级安全,否则 @PreAuthorize 不生效
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 【关键】启用 @PreAuthorize
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
/**
* 配置安全过滤器链
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用 CSRF
.csrf().disable()
// 配置请求授权规则
.authorizeRequests()
// 登录接口放行
.antMatchers("/api/auth/login").permitAll()
// 其他所有请求都需要认证
.anyRequest().authenticated()
.and()
// 配置 Session 管理策略为无状态
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 【关键】添加 JWT 认证过滤器
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
}
/**
* 配置认证提供者
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5. 控制器使用示例
package com.xxx.security.controller;
import com.xxx.security.entity.User;
import com.xxx.security.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户管理控制器
* 演示基于框架原生的角色和权限控制
*/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 场景1:基于角色的权限控制
*
* 说明:只有拥有 ADMIN 角色的用户可以访问
*
* 实现原理:
* - hasRole("ADMIN") 会查找名为 "ROLE_ADMIN" 的权限
* - CustomUserDetailsService 在加载用户时,会将角色转换为 "ROLE_" + 角色名
* - JwtAuthenticationFilter 从 Token 中提取权限并设置到 SecurityContext
* - @PreAuthorize 从 SecurityContext 中读取权限信息进行判断
*/
@GetMapping("/all")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
/**
* 场景2:多角色 OR 关系
*
* 说明:拥有 ADMIN 或 MANAGER 角色的用户都可以访问
*/
@GetMapping("/list")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public ResponseEntity<List<User>> getUserList() {
return ResponseEntity.ok(userService.getAllUsers());
}
/**
* 场景3:多角色 AND 关系
*
* 说明:必须同时拥有 ADMIN 和 MANAGER 两个角色的用户才能访问
*/
@GetMapping("/exclusive")
@PreAuthorize("hasRole('ADMIN') and hasRole('MANAGER')")
public ResponseEntity<List<User>> getExclusiveData() {
return ResponseEntity.ok(userService.getAllUsers());
}
/**
* 场景4:基于权限的精确控制
*
* 说明:拥有 USER_READ 权限的用户可以访问
*
* 实现原理:
* - hasAuthority("USER_READ") 精确匹配权限字符串
* - CustomUserDetailsService 从数据库加载用户的所有权限
* - JWT Token 中包含这些权限信息
* - @PreAuthorize 进行精确权限判断
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('USER_READ')")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
/**
* 场景5:创建用户(需要 USER_WRITE 权限)
*/
@PostMapping
@PreAuthorize("hasAuthority('USER_WRITE')")
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
/**
* 场景6:删除用户(需要 USER_DELETE 权限)
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('USER_DELETE')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
/**
* 场景7:组合权限条件
*
* 说明:
* - 拥有 ADMIN 角色的用户可以访问
* - 或者同时拥有 MANAGER 角色和 REPORT_READ 权限的用户可以访问
*/
@GetMapping("/advanced-reports")
@PreAuthorize("hasRole('ADMIN') or (hasRole('MANAGER') and hasAuthority('REPORT_READ'))")
public ResponseEntity<String> getAdvancedReports() {
return ResponseEntity.ok("高级报表数据...");
}
/**
* 场景8:基于当前用户信息的权限控制
*
* 说明:用户只能查看自己的信息
*/
@GetMapping("/profile")
@PreAuthorize("authentication.principal.username == #username")
public ResponseEntity<User> getUserProfile(@RequestParam String username) {
return ResponseEntity.ok(userService.getUserByUsername(username));
}
/**
* 场景9:用户可以查看自己的信息,管理员可以查看所有用户信息
*/
@GetMapping("/profile-or-admin")
@PreAuthorize("authentication.principal.username == #username or hasRole('ADMIN')")
public ResponseEntity<User> getUserProfileOrAdmin(@RequestParam String username) {
return ResponseEntity.ok(userService.getUserByUsername(username));
}
}
方案二:基于自定义权限服务控制
1. 自定义权限服务(关键修改点)
package com.xxx.security.service;
import com.xxx.security.entity.Document;
import com.xxx.security.entity.User;
import com.xxx.security.repository.DocumentRepository;
import com.xxx.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 自定义权限服务
*
* 关键作用:实现复杂的业务权限判断逻辑
*
* 使用方式:
* 在 @PreAuthorize 注解中通过 @permissionService.methodName() 调用
* 例如:@PreAuthorize("@permissionService.hasPermission('DOCUMENT_READ')")
*
* 注意事项:
* 1. 服务类需要使用 @Service 注解,并指定 Bean 名称
* 2. Bean 名称用于在 @PreAuthorize 中引用该服务
* 3. 方法返回 boolean 值,true 表示有权限,false 表示无权限
*/
@Service("permissionService")
@RequiredArgsConstructor
public class PermissionService {
private final UserRepository userRepository;
private final DocumentRepository documentRepository;
/**
* 检查用户是否拥有指定资源的权限
*
* @param resourceCode 资源代码,如:DOCUMENT_READ, DOCUMENT_WRITE
* @return true: 有权限, false: 无权限
*/
public boolean hasPermission(String resourceCode) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 从数据库查询用户及其权限
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 遍历用户的所有权限,检查是否包含指定权限
return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.anyMatch(permission -> permission.getCode().equals(resourceCode));
}
/**
* 检查用户是否是资源的所有者
*
* @param resourceId 资源 ID
* @return true: 是所有者, false: 不是所有者
*/
public boolean isOwner(Long resourceId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 从数据库查询资源信息
Document document = documentRepository.findById(resourceId)
.orElseThrow(() -> new RuntimeException("资源不存在"));
// 3. 比较资源的创建者与当前登录用户
return document.getCreator().getUsername().equals(username);
}
/**
* 检查用户是否属于指定部门
*
* @param departmentId 部门 ID
* @return true: 属于该部门, false: 不属于该部门
*/
public boolean belongsToDepartment(Long departmentId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 从数据库查询用户信息
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 比较用户的部门 ID
return user.getDepartment() != null &&
user.getDepartment().getId().equals(departmentId);
}
/**
* 检查用户是否可以访问另一个用户的数据
*
* 权限规则:
* 1. 管理员可以访问所有用户的数据
* 2. 同部门用户可以互相访问
* 3. 其他情况不允许访问
*/
public boolean canAccessUserData(Long targetUserId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String currentUsername = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(currentUsername)
.orElseThrow(() -> new RuntimeException("当前用户不存在"));
// 3. 检查是否是管理员
boolean isAdmin = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("ADMIN"));
if (isAdmin) {
return true; // 管理员可以访问所有数据
}
// 4. 查询目标用户
User targetUser = userRepository.findById(targetUserId)
.orElseThrow(() -> new RuntimeException("目标用户不存在"));
// 5. 检查是否是同部门
if (currentUser.getDepartment() != null &&
targetUser.getDepartment() != null) {
return currentUser.getDepartment().getId()
.equals(targetUser.getDepartment().getId());
}
return false; // 不同部门,不允许访问
}
/**
* 检查用户是否可以编辑指定文档
*
* 权限规则:
* 1. 管理员可以编辑所有文档
* 2. 文档创建者可以编辑
* 3. 同部门经理可以编辑
*/
public boolean canEditDocument(Long documentId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 检查是否是管理员
boolean isAdmin = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("ADMIN"));
if (isAdmin) {
return true; // 管理员可以编辑所有文档
}
// 4. 查询文档信息
Document document = documentRepository.findById(documentId)
.orElseThrow(() -> new RuntimeException("文档不存在"));
// 5. 检查是否是文档创建者
if (document.getCreator().getId().equals(currentUser.getId())) {
return true; // 创建者可以编辑
}
// 6. 检查是否是同部门经理
boolean isManager = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("MANAGER"));
if (isManager && currentUser.getDepartment() != null &&
document.getCreator().getDepartment() != null &&
currentUser.getDepartment().getId()
.equals(document.getCreator().getDepartment().getId())) {
return true; // 同部门经理可以编辑
}
return false; // 其他情况不允许编辑
}
/**
* 检查用户是否是部门经理
*/
public boolean isDepartmentManager() {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 检查是否拥有 MANAGER 角色
return user.getRoles().stream()
.anyMatch(role -> role.getName().equals("MANAGER"));
}
}
2. 控制器使用示例
package com.xxx.security.controller;
import com.xxx.security.entity.Document;
import com.xxx.security.service.DocumentService;
import com.xxx.security.service.PermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 文档管理控制器
* 演示基于自定义权限服务的权限控制
*/
@RestController
@RequestMapping("/api/documents")
@RequiredArgsConstructor
public class DocumentController {
private final DocumentService documentService;
private final PermissionService permissionService;
/**
* 场景1:基于自定义权限服务的权限检查
*
* 说明:只有拥有 DOCUMENT_READ 权限的用户可以查看文档列表
*
* 实现原理:
* 1. @PreAuthorize 调用 permissionService.hasPermission() 方法
* 2. permissionService 从数据库查询用户的权限
* 3. 判断用户是否拥有 DOCUMENT_READ 权限
*/
@GetMapping
@PreAuthorize("@permissionService.hasPermission('DOCUMENT_READ')")
public ResponseEntity<List<Document>> getAllDocuments() {
return ResponseEntity.ok(documentService.getAllDocuments());
}
/**
* 场景2:编辑文档(所有者或管理员)
*
* 说明:
* 1. 文档的所有者可以编辑自己的文档
* 2. 管理员可以编辑所有文档
*/
@PutMapping("/{id}")
@PreAuthorize("@permissionService.isOwner(#id) or hasRole('ADMIN')")
public ResponseEntity<Document> updateDocument(
@PathVariable Long id,
@RequestBody Document document
) {
return ResponseEntity.ok(documentService.updateDocument(id, document));
}
/**
* 场景3:编辑文档(复杂的业务逻辑)
*
* 说明:调用自定义方法 canEditDocument(),该方法实现了复杂的编辑权限判断
*
* 权限规则:
* 1. 管理员可以编辑所有文档
* 2. 文档创建者可以编辑
* 3. 同部门经理可以编辑
*/
@PutMapping("/{id}/complex")
@PreAuthorize("@permissionService.canEditDocument(#id)")
public ResponseEntity<Document> updateDocumentComplex(
@PathVariable Long id,
@RequestBody Document document
) {
return ResponseEntity.ok(documentService.updateDocument(id, document));
}
/**
* 场景4:查看部门文档(需要属于该部门)
*/
@GetMapping("/department/{departmentId}")
@PreAuthorize("@permissionService.belongsToDepartment(#departmentId)")
public ResponseEntity<List<Document>> getDepartmentDocuments(
@PathVariable Long departmentId
) {
return ResponseEntity.ok(documentService.getDocumentsByDepartment(departmentId));
}
/**
* 场景5:查看用户文档(需要访问权限)
*/
@GetMapping("/user/{userId}")
@PreAuthorize("@permissionService.canAccessUserData(#userId)")
public ResponseEntity<List<Document>> getUserDocuments(@PathVariable Long userId) {
return ResponseEntity.ok(documentService.getDocumentsByUserId(userId));
}
/**
* 场景6:审批文档(部门经理或管理员)
*/
@PostMapping("/{id}/approve")
@PreAuthorize("@permissionService.isDepartmentManager() or hasRole('ADMIN')")
public ResponseEntity<Document> approveDocument(@PathVariable Long id) {
return ResponseEntity.ok(documentService.approveDocument(id));
}
}
方案三:基于方法参数的动态控制
1. 订单权限服务(关键修改点)
package com.xxx.security.service;
import com.xxx.security.entity.Order;
import com.xxx.security.entity.User;
import com.xxx.security.repository.OrderRepository;
import com.xxx.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
/**
* 订单权限服务
*
* 关键作用:根据订单业务逻辑进行权限判断
*
* 特点:
* 1. 根据订单状态判断权限
* 2. 根据用户角色和订单创建者判断权限
* 3. 支持复杂的业务权限逻辑
*/
@Service("orderPermissionService")
@RequiredArgsConstructor
public class OrderPermissionService {
private final OrderRepository orderRepository;
private final UserRepository userRepository;
/**
* 检查是否可以查看订单
*
* 权限规则:
* 1. 管理员可以查看所有订单
* 2. 订单创建者可以查看自己的订单
* 3. 同部门人员可以查看同部门订单
*/
public boolean canViewOrder(Long orderId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 检查是否是管理员
boolean isAdmin = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("ADMIN"));
if (isAdmin) {
return true; // 管理员可以查看所有订单
}
// 4. 查询订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 5. 检查是否是订单创建者
if (order.getCreator().getId().equals(currentUser.getId())) {
return true; // 创建者可以查看
}
// 6. 检查是否是同部门
if (currentUser.getDepartment() != null &&
order.getCreator().getDepartment() != null &&
currentUser.getDepartment().getId()
.equals(order.getCreator().getDepartment().getId())) {
return true; // 同部门可以查看
}
return false; // 其他情况不允许查看
}
/**
* 检查是否可以编辑订单
*
* 权限规则:
* 1. 管理员可以编辑所有订单
* 2. 部门经理可以编辑同部门订单
* 3. 订单创建者只能编辑草稿状态的订单
*/
public boolean canEditOrder(Long orderId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 查询订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 4. 检查是否是管理员
boolean isAdmin = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("ADMIN"));
if (isAdmin) {
return true; // 管理员可以编辑所有订单
}
// 5. 检查是否是部门经理且同部门
boolean isManager = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("MANAGER"));
if (isManager && currentUser.getDepartment() != null &&
order.getCreator().getDepartment() != null &&
currentUser.getDepartment().getId()
.equals(order.getCreator().getDepartment().getId())) {
return true; // 同部门经理可以编辑
}
// 6. 检查是否是创建者且订单状态为草稿
if (order.getCreator().getId().equals(currentUser.getId()) &&
"DRAFT".equals(order.getStatus())) {
return true; // 创建者只能编辑草稿状态的订单
}
return false; // 其他情况不允许编辑
}
/**
* 检查是否可以审批订单
*
* 权限规则:
* 1. 只有部门经理和管理员可以审批订单
*/
public boolean canApproveOrder(Long orderId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 检查是否有审批权限
return currentUser.getRoles().stream()
.anyMatch(role ->
role.getName().equals("MANAGER") || role.getName().equals("ADMIN")
);
}
/**
* 检查是否可以修改订单状态
*
* 权限规则:
* 1. 管理员可以修改任何状态
* 2. 部门经理只能审批到特定状态(APPROVED, REJECTED)
* 3. 创建者只能将状态修改为 PENDING
*/
public boolean canChangeOrderStatus(Long orderId, String newStatus) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 查询订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 4. 检查是否是管理员
boolean isAdmin = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("ADMIN"));
if (isAdmin) {
return true; // 管理员可以修改任何状态
}
// 5. 检查是否是部门经理
boolean isManager = currentUser.getRoles().stream()
.anyMatch(role -> role.getName().equals("MANAGER"));
if (isManager) {
// 部门经理只能审批到特定状态
return Arrays.asList("APPROVED", "REJECTED").contains(newStatus);
}
// 6. 检查是否是创建者
if (order.getCreator().getId().equals(currentUser.getId())) {
// 创建者只能将订单提交为待审批状态
return "PENDING".equals(newStatus);
}
return false;
}
/**
* 检查是否在同一部门
*/
public boolean isInSameDepartment(Long orderId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询当前用户
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
// 3. 查询订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 4. 比较部门
if (currentUser.getDepartment() != null &&
order.getCreator().getDepartment() != null) {
return currentUser.getDepartment().getId()
.equals(order.getCreator().getDepartment().getId());
}
return false;
}
/**
* 检查订单是否属于当前用户
*/
public boolean isMyOrder(Long orderId) {
// 1. 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// 2. 查询订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 3. 比较创建者
return order.getCreator().getUsername().equals(username);
}
}
2. 控制器使用示例
package com.xxx.security.controller;
import com.xxx.security.entity.Order;
import com.xxx.security.service.OrderService;
import com.xxx.security.service.OrderPermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 订单管理控制器
* 演示基于方法参数的动态权限控制
*/
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final OrderPermissionService orderPermissionService;
/**
* 场景1:用户只能查看自己的订单,管理员可以查看所有订单
*
* 说明:
* - #order.userId 引用方法参数 order 对象的 userId 属性
* - authentication.principal.id 获取当前登录用户的 ID
* - 使用 or 逻辑,满足任一条件即可
*/
@GetMapping("/{orderId}")
@PreAuthorize("#order.userId == authentication.principal.id or hasRole('ADMIN')")
public ResponseEntity<Order> getOrder(@PathVariable Long orderId) {
Order order = orderService.getOrderById(orderId);
return ResponseEntity.ok(order);
}
/**
* 场景2:查看订单(使用自定义权限服务)
*/
@GetMapping("/{orderId}/detail")
@PreAuthorize("@orderPermissionService.canViewOrder(#orderId)")
public ResponseEntity<Order> getOrderDetail(@PathVariable Long orderId) {
return ResponseEntity.ok(orderService.getOrderById(orderId));
}
/**
* 场景3:编辑订单(创建者或管理员)
*/
@PutMapping("/{orderId}")
@PreAuthorize("#order.userId == authentication.principal.id or hasRole('ADMIN')")
public ResponseEntity<Order> updateOrder(
@PathVariable Long orderId,
@RequestBody Order order
) {
return ResponseEntity.ok(orderService.updateOrder(orderId, order));
}
/**
* 场景4:编辑订单(复杂业务逻辑)
*/
@PutMapping("/{orderId}/complex")
@PreAuthorize("@orderPermissionService.canEditOrder(#orderId)")
public ResponseEntity<Order> updateOrderComplex(
@PathVariable Long orderId,
@RequestBody Order order
) {
return ResponseEntity.ok(orderService.updateOrder(orderId, order));
}
/**
* 场景5:修改订单状态(需要状态修改权限)
*/
@PutMapping("/{orderId}/status")
@PreAuthorize("@orderPermissionService.canChangeOrderStatus(#orderId, #newStatus)")
public ResponseEntity<Order> updateOrderStatus(
@PathVariable Long orderId,
@RequestParam String newStatus
) {
return ResponseEntity.ok(orderService.updateOrderStatus(orderId, newStatus));
}
/**
* 场景6:审批订单(部门经理或管理员)
*/
@PostMapping("/{orderId}/approve")
@PreAuthorize("@orderPermissionService.canApproveOrder(#orderId)")
public ResponseEntity<Order> approveOrder(@PathVariable Long orderId) {
return ResponseEntity.ok(orderService.approveOrder(orderId));
}
/**
* 场景7:删除订单(创建者或管理员)
*/
@DeleteMapping("/{orderId}")
@PreAuthorize("@orderPermissionService.isMyOrder(#orderId) or hasRole('ADMIN')")
public ResponseEntity<Void> deleteOrder(@PathVariable Long orderId) {
orderService.deleteOrder(orderId);
return ResponseEntity.ok().build();
}
}
关键要点总结
方案一:基于框架原生权限控制
关键修改点:
CustomUserDetailsService:角色必须转换为ROLE_+ 角色名JwtService:将权限列表存储到 JWT 的 claims 中JwtAuthenticationFilter:从 Token 中提取权限并设置到 SecurityContextSecurityConfig:必须使用@EnableGlobalMethodSecurity(prePostEnabled = true)
适用场景:
- 角色和权限相对固定的系统
- 不需要动态权限控制
- 权限逻辑简单的应用
方案二:基于自定义权限服务控制
关键修改点:
- 创建自定义权限服务类,使用
@Service("permissionService")指定 Bean 名称 - 在
@PreAuthorize中通过@permissionService.methodName()调用 - 每次请求时实时查询数据库进行权限判断
适用场景:
- 需要复杂的业务权限逻辑
- 需要数据级别的权限控制
- 权限逻辑可能频繁变更
方案三:基于方法参数的动态控制
关键修改点:
- 在
@PreAuthorize中使用#参数名引用方法参数 - 可以访问参数的属性,如
#resource.ownerId - 可以与当前用户信息进行比较
适用场景:
- 权限判断依赖于方法参数
- 需要实现资源所有者权限控制
- 需要根据业务状态进行权限判断
JDK 1.8 兼容性说明
- 包名:使用
javax.servlet而不是jakarta.servlet - JWT 版本:使用 0.11.5 版本(兼容 JDK 1.8)
- Security 配置:继承
WebSecurityConfigurerAdapter - 注解启用:使用
@EnableGlobalMethodSecurity(prePostEnabled = true) - JWT 构建:使用
setClaims()而不是claims() - JWT 解析:使用
parserBuilder()和parseClaimsJws()
常用 @PreAuthorize 表达式速查表
| 表达式 | 说明 |
|---|---|
hasRole('ADMIN') |
拥有 ADMIN 角色 |
hasAnyRole('ADMIN', 'USER') |
拥有 ADMIN 或 USER 角色 |
hasAuthority('READ') |
拥有 READ 权限 |
hasAnyAuthority('READ', 'WRITE') |
拥有 READ 或 WRITE 权限 |
#param == value |
参数等于指定值 |
#userId == authentication.principal.id |
参数等于当前用户 ID |
@service.method(args) |
调用自定义服务方法 |
hasRole('A') and hasRole('B') |
同时拥有两个角色 |
hasRole('A') or hasRole('B') |
拥有任意一个角色 |
更多推荐


所有评论(0)