授权控制
授权(Authorization)是确定用户能否访问特定资源的过程,回答"你能做什么"这个问题。在用户通过认证确认身份后,授权机制决定用户有权执行哪些操作。Spring Security 提供了强大而灵活的授权控制能力,支持请求级别和方法级别的细粒度权限控制。
授权架构
核心概念
Spring Security 的授权机制围绕以下核心概念展开:
- Authority(权限):用户拥有的权限,通常表示为字符串,如
ROLE_USER、PERMISSION_READ - Role(角色):一组权限的集合,实际也是一种 Authority,习惯上以
ROLE_前缀标识 - Resource(资源):需要保护的对象,可以是 URL、方法、领域对象等
- Permission(许可):对特定资源的操作权限,如读取、写入、删除
授权决策流程
当用户尝试访问受保护资源时,授权决策的流程如下:
- 用户请求到达
AuthorizationFilter - 过滤器从
SecurityContext获取当前用户的Authentication - 使用
AuthorizationManager进行授权决策 - 根据决策结果:允许则继续处理请求,拒绝则抛出
AccessDeniedException
请求级授权
请求级授权是最常用的授权方式,基于 URL 模式控制访问权限。
基本配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 静态资源公开访问
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// 公开页面
.requestMatchers("/", "/home", "/register", "/login").permitAll()
// 用户页面需要 USER 角色
.requestMatchers("/user/**").hasRole("USER")
// 管理页面需要 ADMIN 角色
.requestMatchers("/admin/**").hasRole("ADMIN")
// API 端点
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasAuthority("API_USER")
// 其他所有请求都需要认证
.anyRequest().authenticated()
);
return http.build();
}
匹配器详解
Spring Security 提供了多种请求匹配器:
路径匹配
// 精确匹配
.requestMatchers("/login").permitAll()
// Ant 风格通配符
.requestMatchers("/api/**").authenticated() // 匹配 /api 及其子路径
.requestMatchers("/user/*").authenticated() // 匹配单层路径
.requestMatchers("/*.html").permitAll() // 匹配根目录下的 HTML 文件
// 正则表达式匹配
.requestMatchers(RegexRequestMatcher.regexMatch("^/api/v[0-9]+/.*"))
.authenticated()
// HTTP 方法匹配
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/posts/**").hasRole("AUTHOR")
.requestMatchers(HttpMethod.DELETE, "/api/posts/**").hasRole("ADMIN")
复合匹配
// AND 组合
.requestMatchers(
request -> request.getRequestURI().startsWith("/api")
&& "application/json".equals(request.getContentType())
).authenticated()
// OR 组合
.requestMatchers(
new OrRequestMatcher(
new AntPathRequestMatcher("/admin/**"),
new AntPathRequestMatcher("/manager/**")
)
).hasRole("ADMIN")
授权规则
Spring Security 提供了丰富的授权规则:
| 方法 | 说明 | 示例 |
|---|---|---|
permitAll() | 允许所有人访问 | .requestMatchers("/public/**").permitAll() |
denyAll() | 拒绝所有人访问 | .requestMatchers("/blocked/**").denyAll() |
authenticated() | 需要认证 | .anyRequest().authenticated() |
anonymous() | 允许匿名访问 | .requestMatchers("/guest/**").anonymous() |
hasRole(String) | 需要指定角色 | .requestMatchers("/admin/**").hasRole("ADMIN") |
hasAnyRole(String...) | 需要任一角色 | .requestMatchers("/manage/**").hasAnyRole("ADMIN", "MANAGER") |
hasAuthority(String) | 需要指定权限 | .requestMatchers("/api/**").hasAuthority("API_ACCESS") |
hasAnyAuthority(String...) | 需要任一权限 | .requestMatchers("/data/**").hasAnyAuthority("READ", "WRITE") |
access(String) | SpEL 表达式 | .requestMatchers("/special/**").access("hasRole('ADMIN') and T(java.time.LocalDate).now().getDayOfWeek() == T(java.time.DayOfWeek).MONDAY") |
使用 SpEL 表达式
Spring Expression Language (SpEL) 提供了更强大的表达能力:
.authorizeHttpRequests(auth -> auth
// 组合条件
.requestMatchers("/premium/**").access("hasRole('VIP') and authentication.name != 'test'")
// 基于时间
.requestMatchers("/maintenance/**").access("T(java.time.LocalTime).now().hour between 1 and 5")
// 基于IP
.requestMatchers("/internal/**").access("hasIpAddress('10.0.0.0/8')")
// 调用 Bean 方法
.requestMatchers("/special/**").access("@securityService.hasSpecialAccess(authentication)")
)
自定义授权管理器
当内置规则无法满足需求时,可以实现自定义 AuthorizationManager:
public class IpAddressAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
private final String allowedIpRange;
public IpAddressAuthorizationManager(String allowedIpRange) {
this.allowedIpRange = allowedIpRange;
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authentication,
RequestAuthorizationContext context) {
HttpServletRequest request = context.getRequest();
String remoteAddress = request.getRemoteAddr();
// 检查 IP 是否在允许范围内
boolean allowed = isInRange(remoteAddress, allowedIpRange);
return new AuthorizationDecision(allowed);
}
private boolean isInRange(String ip, String range) {
// IP 范围检查逻辑
return true;
}
}
// 使用自定义授权管理器
.authorizeHttpRequests(auth -> auth
.requestMatchers("/internal/**").access(new IpAddressAuthorizationManager("192.168.1.0/24"))
)
角色与权限
理解角色和权限的关系
在 Spring Security 中,角色和权限本质上都是 GrantedAuthority。角色是一种特殊的权限,习惯上以 ROLE_ 前缀标识。hasRole("ADMIN") 等价于 hasAuthority("ROLE_ADMIN")。
// 两种写法等价
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
角色继承
当存在角色层级关系时(如管理员拥有普通用户的所有权限),可以使用角色继承:
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
ROLE_USER > ROLE_GUEST
""");
}
// 配置方法安全表达式处理器
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(
RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
这个配置意味着:
ROLE_ADMIN自动拥有ROLE_MANAGER、ROLE_USER、ROLE_GUEST的所有权限ROLE_MANAGER自动拥有ROLE_USER、ROLE_GUEST的所有权限ROLE_USER自动拥有ROLE_GUEST的所有权限
动态权限加载
当权限需要从数据库动态加载时,可以在认证成功后加载用户的所有权限:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final PermissionRepository permissionRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 加载用户角色和权限
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 添加角色
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role.getName()));
// 添加角色下的权限
role.getPermissions().forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission.getName()))
);
});
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
}
授权事件处理
监听授权事件
Spring Security 在授权检查时会发布事件,可以监听这些事件进行日志记录或审计:
@Component
public class AuthorizationEventListener {
private final AuditLogService auditLogService;
@EventListener
public void onAuthorizationSuccess(AuthorizationGrantedEvent event) {
Authentication auth = event.getAuthentication();
Object resource = event.getObject();
auditLogService.logSuccess(
auth.getName(),
"ACCESS_GRANTED",
resource.toString()
);
}
@EventListener
public void onAuthorizationDenied(AuthorizationDeniedEvent event) {
Authentication auth = event.getAuthentication();
Object resource = event.getObject();
auditLogService.logFailure(
auth.getName(),
"ACCESS_DENIED",
resource.toString()
);
}
}
自定义访问拒绝处理
当授权失败时,默认返回 403 错误页面。可以自定义处理逻辑:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(ex -> ex
.accessDeniedHandler(new CustomAccessDeniedHandler())
.accessDeniedPage("/access-denied")
);
return http.build();
}
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException ex) throws IOException {
// 判断是否是 AJAX 请求
String requestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWith)) {
// JSON 响应
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"权限不足\"}");
} else {
// 重定向到错误页面
response.sendRedirect("/access-denied");
}
}
}
HTTPS 安全
强制 HTTPS
在生产环境中,敏感请求应该使用 HTTPS:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.requiresChannel(channel -> channel
.anyRequest().requiresSecure() // 强制 HTTPS
);
return http.build();
}
混合 HTTP/HTTPS
某些场景下可能需要混合使用:
.requiresChannel(channel -> channel
.requestMatchers("/secure/**").requiresSecure() // HTTPS
.requestMatchers("/public/**").requiresInsecure() // HTTP
)
CORS 配置
跨域资源共享(CORS)是前后端分离架构中常见的需求。
基本 CORS 配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("https://example.com"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
使用 Spring Boot 配置
Spring Boot 提供了简化的 CORS 配置方式:
spring:
web:
cors:
allowed-origins: "https://example.com"
allowed-methods: GET,POST,PUT,DELETE
allowed-headers: "*"
allow-credentials: true
max-age: 3600
安全响应头
Spring Security 默认添加多个安全响应头,可以自定义配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// 内容类型嗅探防护
.contentTypeOptions(Customizer.withDefaults())
// XSS 防护
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
// 点击劫持防护
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
// HSTS(HTTP Strict Transport Security)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
// CSP(内容安全策略)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'")
)
// Referrer Policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
);
return http.build();
}
小结
本章详细介绍了 Spring Security 的授权控制机制:
- 授权是在认证之后确定用户能否访问特定资源的过程
- 请求级授权基于 URL 模式控制访问权限,支持多种匹配器和授权规则
- SpEL 表达式提供更强大的条件表达能力
- 角色和权限本质上都是
GrantedAuthority,角色继承简化权限管理 - 自定义授权管理器实现复杂授权逻辑
- HTTPS、CORS、安全响应头是保护应用的重要手段
下一章将学习方法级安全,在服务层方法上实现更细粒度的权限控制。