前言

本文介绍在 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();
    }
}

关键要点总结

方案一:基于框架原生权限控制

关键修改点

  1. CustomUserDetailsService:角色必须转换为 ROLE_ + 角色名
  2. JwtService:将权限列表存储到 JWT 的 claims 中
  3. JwtAuthenticationFilter:从 Token 中提取权限并设置到 SecurityContext
  4. SecurityConfig:必须使用 @EnableGlobalMethodSecurity(prePostEnabled = true)

适用场景

  • 角色和权限相对固定的系统
  • 不需要动态权限控制
  • 权限逻辑简单的应用

方案二:基于自定义权限服务控制

关键修改点

  1. 创建自定义权限服务类,使用 @Service("permissionService") 指定 Bean 名称
  2. @PreAuthorize 中通过 @permissionService.methodName() 调用
  3. 每次请求时实时查询数据库进行权限判断

适用场景

  • 需要复杂的业务权限逻辑
  • 需要数据级别的权限控制
  • 权限逻辑可能频繁变更

方案三:基于方法参数的动态控制

关键修改点

  1. @PreAuthorize 中使用 #参数名 引用方法参数
  2. 可以访问参数的属性,如 #resource.ownerId
  3. 可以与当前用户信息进行比较

适用场景

  • 权限判断依赖于方法参数
  • 需要实现资源所有者权限控制
  • 需要根据业务状态进行权限判断

JDK 1.8 兼容性说明

  1. 包名:使用 javax.servlet 而不是 jakarta.servlet
  2. JWT 版本:使用 0.11.5 版本(兼容 JDK 1.8)
  3. Security 配置:继承 WebSecurityConfigurerAdapter
  4. 注解启用:使用 @EnableGlobalMethodSecurity(prePostEnabled = true)
  5. JWT 构建:使用 setClaims() 而不是 claims()
  6. 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') 拥有任意一个角色
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐