跳到主要内容

授权控制

授权(Authorization)是确定用户能否访问特定资源的过程,回答"你能做什么"这个问题。在用户通过认证确认身份后,授权机制决定用户有权执行哪些操作。Spring Security 提供了强大而灵活的授权控制能力,支持请求级别和方法级别的细粒度权限控制。

授权架构

核心概念

Spring Security 的授权机制围绕以下核心概念展开:

  • Authority(权限):用户拥有的权限,通常表示为字符串,如 ROLE_USERPERMISSION_READ
  • Role(角色):一组权限的集合,实际也是一种 Authority,习惯上以 ROLE_ 前缀标识
  • Resource(资源):需要保护的对象,可以是 URL、方法、领域对象等
  • Permission(许可):对特定资源的操作权限,如读取、写入、删除

授权决策流程

当用户尝试访问受保护资源时,授权决策的流程如下:

  1. 用户请求到达 AuthorizationFilter
  2. 过滤器从 SecurityContext 获取当前用户的 Authentication
  3. 使用 AuthorizationManager 进行授权决策
  4. 根据决策结果:允许则继续处理请求,拒绝则抛出 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_MANAGERROLE_USERROLE_GUEST 的所有权限
  • ROLE_MANAGER 自动拥有 ROLE_USERROLE_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、安全响应头是保护应用的重要手段

下一章将学习方法级安全,在服务层方法上实现更细粒度的权限控制。