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 |
最佳实践
- 最小权限原则:只授予必要权限
- 角色职责分离:避免角色重叠
- 动态权限:支持运行时修改
- 审计日志:记录权限变更
Spring Security 授权模型提供了灵活的权限控制能力,掌握其原理和配置,能够构建出安全可控的应用系统。
六、思考与练习
思考题
-
基础题:URL 级别授权和方法级别授权有什么区别?它们分别在什么场景下使用?
-
进阶题:RBAC 和 ABAC 两种授权模型各有什么特点?如何根据业务需求选择合适的授权模型?
-
实战题:如何设计一个支持动态权限配置的系统,实现权限的在线修改和实时生效?
编程练习
练习:基于 RBAC 模型实现一个完整的权限管理系统,要求:
- 用户、角色、权限的增删改查接口
- URL 和方法级别的权限控制
- 动态权限配置(支持数据库配置)
- 数据权限控制(用户只能查看自己部门的数据)
- 权限变更的审计日志
章节关联
- 前置章节:《认证机制详解》- 授权基于认证,需要先理解认证流程
- 后续章节:《CSRF 与 CORS 详解》- 掌握授权后,学习跨域安全防护
- 扩展阅读:
- Spring Security 权限表达式:docs.spring.io/spring-secu…
- RBAC 设计模式:en.wikipedia.org/wiki/Role-b…
📝 下一章预告
下一章将讲解 Web 安全中的 CSRF 和 CORS 攻击防护机制,这是前后端分离架构中必须掌握的重要安全知识。
本章完