32-Spring Security 授权模型详解

3 阅读7分钟

Spring Security 授权模型详解

本章导读

授权是在认证之后决定用户能否访问特定资源的关键机制。本章系统讲解 Spring Security 的授权体系,从基础的 URL 和方法级别权限控制,到灵活的 RBAC 角色权限模型,帮助你掌握细粒度权限控制的设计与实现方法。

学习目标

  • 目标1:理解授权流程和访问决策机制,掌握多种授权配置方式
  • 目标2:能够实现基于角色的访问控制(RBAC)和动态权限管理
  • 目标3:掌握方法级别安全和自定义权限评估器的应用

前置知识:Spring Security 认证机制、Spring AOP 基础

阅读时长:约 50 分钟

一、知识概述

授权(Authorization)是在认证之后确定用户是否有权限访问特定资源的过程。Spring Security 提供了灵活的授权模型,支持 URL 级别、方法级别的权限控制,以及 RBAC(基于角色的访问控制)模型。

授权的核心概念:

  • Authority:权限(如 READ_USER)
  • Role:角色(如 ROLE_ADMIN)
  • Permission:权限项
  • AccessDecisionManager:访问决策管理器

理解授权模型的原理,是构建安全应用的关键。

二、知识点详细讲解

2.1 授权模型分类

RBAC(基于角色的访问控制)
用户 → 角色 → 权限 → 资源
ACL(访问控制列表)
用户 → 资源 → 权限
ABAC(基于属性的访问控制)
用户属性 + 环境属性 + 资源属性 → 决策

2.2 授权流程

请求
  │
  ▼
FilterSecurityInterceptor(安全拦截器)
  │
  ├── 获取 Authentication
  │
  ├── 获取请求资源所需权限
  │
  ▼
AccessDecisionManager(访问决策管理器)
  │
  ├── 遍历 AccessDecisionVoter
  │
  ▼
AccessDecisionVoter(投票器)
  │
  ├── 投票:赞成/反对/弃权
  │
  ▼
决策结果
  │
  ├── 通过:继续请求
  │
  └── 拒绝:抛出 AccessDeniedException

2.3 授权配置级别

级别说明注解
URL路径级别http.authorizeHttpRequests()
方法方法级别@PreAuthorize
对象对象级别ACL
表达式SpEL 表达式@PreAuthorize("hasRole('ADMIN')")

2.4 内置权限表达式

表达式说明
hasRole('role')拥有角色
hasAnyRole('r1','r2')拥有任一角色
hasAuthority('auth')拥有权限
hasAnyAuthority('a1','a2')拥有任一权限
permitAll允许所有
denyAll拒绝所有
isAnonymous匿名用户
isAuthenticated已认证用户
isFullyAuthenticated完全认证
isRememberMe记住我登录
principal当前用户主体

三、代码示例

3.1 URL 授权配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class UrlAuthorizationConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // 公开资源
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
                
                // 静态资源
                .requestMatchers("/static/**").permitAll()
                
                // API 接口
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/user/**").hasRole("USER")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                
                // 特定 HTTP 方法
                .requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/articles/**").hasRole("AUTHOR")
                .requestMatchers(HttpMethod.DELETE, "/api/articles/**").hasRole("ADMIN")
                
                // 正则表达式匹配
                .requestMatchers(RegexRequestMatcher.regexMatcher("/api/users/\\d+"))
                    .hasRole("USER")
                
                // 其他请求需要认证
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
}

3.2 方法级别授权

import org.springframework.security.access.prepost.*;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ArticleService {
    
    // 需要 USER 角色
    @PreAuthorize("hasRole('USER')")
    public Article getArticle(Long id) {
        return articleRepository.findById(id);
    }
    
    // 需要 ADMIN 角色或拥有者
    @PreAuthorize("hasRole('ADMIN') or #article.authorId == authentication.principal.id")
    public void updateArticle(Article article) {
        articleRepository.save(article);
    }
    
    // 需要 ADMIN 角色
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteArticle(Long id) {
        articleRepository.deleteById(id);
    }
    
    // 需要特定权限
    @PreAuthorize("hasAuthority('ARTICLE_CREATE')")
    public Article createArticle(ArticleDTO dto) {
        return articleRepository.save(dto);
    }
    
    // 多条件组合
    @PreAuthorize("hasRole('ADMIN') and hasAuthority('USER_MANAGE')")
    public void manageUser(Long userId) {
        // 用户管理逻辑
    }
    
    // 后置授权(验证返回结果)
    @PostAuthorize("returnObject.authorId == authentication.principal.id")
    public Article getMyArticle(Long id) {
        return articleRepository.findById(id);
    }
    
    // 后置过滤(过滤集合结果)
    @PostFilter("filterObject.status == 'PUBLISHED'")
    public List<Article> getAllArticles() {
        return articleRepository.findAll();
    }
    
    // 前置过滤(过滤输入参数)
    @PreFilter("filterObject.status == 'DRAFT'")
    public void publishDrafts(List<Article> articles) {
        articles.forEach(article -> {
            article.setStatus("PUBLISHED");
            articleRepository.save(article);
        });
    }
}

3.3 启用方法授权

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.*;

@Configuration
@EnableMethodSecurity(
    prePostEnabled = true,      // 启用 @PreAuthorize/@PostAuthorize
    securedEnabled = true,      // 启用 @Secured
    jsr250Enabled = true        // 启用 @RolesAllowed
)
public class MethodSecurityConfig {
}
import javax.annotation.security.RolesAllowed;
import org.springframework.security.access.annotation.Secured;

@Service
public class UserService {
    
    // @Secured 注解(需要角色)
    @Secured("ROLE_ADMIN")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
    
    // @RolesAllowed 注解(JSR-250)
    @RolesAllowed({"ADMIN", "MANAGER"})
    public void updateUser(UserDTO dto) {
        userRepository.save(dto);
    }
}

3.4 自定义权限评估器

import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Autowired
    private ArticleService articleService;
    
    @Override
    public boolean hasPermission(
            Authentication authentication, 
            Object targetDomainObject, 
            Object permission) {
        
        if (targetDomainObject instanceof Article) {
            Article article = (Article) targetDomainObject;
            String perm = (String) permission;
            
            // 检查权限
            return checkArticlePermission(authentication, article, perm);
        }
        
        return false;
    }
    
    @Override
    public boolean hasPermission(
            Authentication authentication, 
            Serializable targetId, 
            String targetType, 
            Object permission) {
        
        if ("Article".equals(targetType)) {
            Article article = articleService.findById((Long) targetId);
            return checkArticlePermission(authentication, article, (String) permission);
        }
        
        return false;
    }
    
    private boolean checkArticlePermission(
            Authentication auth, Article article, String permission) {
        
        UserDetails user = (UserDetails) auth.getPrincipal();
        
        // 检查是否是作者
        if ("WRITE".equals(permission)) {
            return article.getAuthorId().equals(user.getId());
        }
        
        // 检查是否是管理员
        if ("DELETE".equals(permission)) {
            return auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        }
        
        return false;
    }
}
// 使用自定义权限评估器
@Service
public class ArticleSecurityService {
    
    @PreAuthorize("hasPermission(#id, 'Article', 'READ')")
    public Article readArticle(Long id) {
        return articleRepository.findById(id);
    }
    
    @PreAuthorize("hasPermission(#id, 'Article', 'WRITE')")
    public void updateArticle(Long id, ArticleDTO dto) {
        // 更新逻辑
    }
    
    @PreAuthorize("hasPermission(#article, 'DELETE')")
    public void deleteArticle(Article article) {
        articleRepository.delete(article);
    }
}

3.5 RBAC 实现

// 用户实体
@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String password;
    
    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Role> roles = new HashSet<>();
    
    // getter/setter
}

// 角色实体
@Entity
public class Role {
    @Id
    private Long id;
    private String name;
    
    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Permission> permissions = new HashSet<>();
    
    // getter/setter
}

// 权限实体
@Entity
public class Permission {
    @Id
    private Long id;
    private String name;      // 如:user:read
    private String resource;  // 如:/api/users/**
    private String action;    // 如:GET, POST, DELETE
    
    // getter/setter
}
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;

@Service
public class RbacUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> 
                new UsernameNotFoundException("用户不存在"));
        
        // 收集所有权限
        Set<GrantedAuthority> authorities = new HashSet<>();
        
        for (Role role : user.getRoles()) {
            // 添加角色
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
            
            // 添加权限
            for (Permission permission : role.getPermissions()) {
                authorities.add(new SimpleGrantedAuthority(permission.getName()));
            }
        }
        
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            authorities
        );
    }
}

3.6 动态权限配置

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.util.*;

@Component
public class DynamicSecurityMetadataSource 
        implements FilterInvocationSecurityMetadataSource {
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) {
        FilterInvocation fi = (FilterInvocation) object;
        String requestUrl = fi.getRequestUrl();
        String method = fi.getHttpRequest().getMethod();
        
        // 查询该 URL 需要的权限
        List<Permission> permissions = permissionService.findByUrlAndMethod(
            requestUrl, method);
        
        if (permissions.isEmpty()) {
            return null;  // 不需要权限
        }
        
        // 转换为配置属性
        return permissions.stream()
            .map(p -> new SecurityConfig(p.getName()))
            .collect(Collectors.toList());
    }
    
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

3.7 自定义投票器

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.*;

@Component
public class CustomVoter implements AccessDecisionVoter<Object> {
    
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
    
    @Override
    public int vote(
            Authentication authentication, 
            Object object, 
            Collection<ConfigAttribute> attributes) {
        
        // 自定义投票逻辑
        for (ConfigAttribute attribute : attributes) {
            String needPermission = attribute.getAttribute();
            
            // 检查用户是否拥有该权限
            boolean hasPermission = authentication.getAuthorities().stream()
                .anyMatch(auth -> auth.getAuthority().equals(needPermission));
            
            if (hasPermission) {
                return ACCESS_GRANTED;  // 赞成
            }
        }
        
        return ACCESS_DENIED;  // 反对
    }
}

四、实战应用场景

4.1 数据权限控制

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DataPermissionService {
    
    // 只能查看自己部门的数据
    @PreAuthorize("hasRole('USER')")
    @PostFilter("filterObject.departmentId == authentication.principal.departmentId")
    public List<Order> getOrders() {
        return orderRepository.findAll();
    }
    
    // 只能操作自己的数据
    @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
    public User getUser(Long userId) {
        return userRepository.findById(userId);
    }
    
    // 组合条件
    @PreAuthorize("""
        hasRole('ADMIN') or 
        (hasRole('MANAGER') and #user.departmentId == authentication.principal.departmentId)
    """)
    public void updateUser(User user) {
        userRepository.save(user);
    }
}

4.2 权限注解工具

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
public class SecurityUtils {
    
    // 获取当前用户
    public static UserDetails getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.getPrincipal() instanceof UserDetails) {
            return (UserDetails) auth.getPrincipal();
        }
        return null;
    }
    
    // 检查是否有角色
    public static boolean hasRole(String role) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_" + role));
    }
    
    // 检查是否有权限
    public static boolean hasAuthority(String authority) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals(authority));
    }
    
    // 需要权限检查
    public static void requirePermission(String permission) {
        if (!hasAuthority(permission)) {
            throw new AccessDeniedException("无权限: " + permission);
        }
    }
    
    // 需要角色检查
    public static void requireRole(String role) {
        if (!hasRole(role)) {
            throw new AccessDeniedException("无角色: " + role);
        }
    }
}

五、总结与最佳实践

授权模型选择

场景推荐模型
简单系统URL + 角色
中等系统RBAC
复杂系统RBAC + 数据权限
企业系统ABAC

最佳实践

  1. 最小权限原则:只授予必要权限
  2. 角色职责分离:避免角色重叠
  3. 动态权限:支持运行时修改
  4. 审计日志:记录权限变更

Spring Security 授权模型提供了灵活的权限控制能力,掌握其原理和配置,能够构建出安全可控的应用系统。


六、思考与练习

思考题

  1. 基础题:URL 级别授权和方法级别授权有什么区别?它们分别在什么场景下使用?

  2. 进阶题:RBAC 和 ABAC 两种授权模型各有什么特点?如何根据业务需求选择合适的授权模型?

  3. 实战题:如何设计一个支持动态权限配置的系统,实现权限的在线修改和实时生效?

编程练习

练习:基于 RBAC 模型实现一个完整的权限管理系统,要求:

  1. 用户、角色、权限的增删改查接口
  2. URL 和方法级别的权限控制
  3. 动态权限配置(支持数据库配置)
  4. 数据权限控制(用户只能查看自己部门的数据)
  5. 权限变更的审计日志

章节关联

  • 前置章节:《认证机制详解》- 授权基于认证,需要先理解认证流程
  • 后续章节:《CSRF 与 CORS 详解》- 掌握授权后,学习跨域安全防护
  • 扩展阅读

📝 下一章预告

下一章将讲解 Web 安全中的 CSRF 和 CORS 攻击防护机制,这是前后端分离架构中必须掌握的重要安全知识。


本章完