当前位置: 首页 > news >正文

【Spring Security】授权(二)

Spring Security

  • 授权(Authorization)
    • Web 层授权配置
      • 基本概念
      • 基本使用结构
      • 核心 API
      • 授权匹配顺序
      • 匹配机制
      • 默认权限评估器
      • SpEL 表达式支持

授权(Authorization)

Web 层授权配置

基本概念

什么是 Web 层授权?

Web 层授权是 Spring Security 中针对 HTTP 请求链路 的权限控制机制,核心作用是:根据请求的 URL 路径、请求方法(GET/POST 等)、客户端信息(IP、Cookie 等),结合当前用户的身份和权限,判断是否允许该请求访问目标资源

  • 应用场景:几乎所有 Web 应用的权限控制核心,比如:
    • 博客系统中,仅管理员可访问 /admin/** 路径(文章审核、用户管理);
    • 电商系统中,仅登录用户可访问 /order/** 路径(查看订单、提交支付);
    • 前后端分离项目中,API 接口的访问权限控制(如 /api/v1/user/info 需登录认证,/api/v1/admin/user/delete 需 ADMIN 角色)。
  • 与其他授权层的区别
    授权层级控制对象典型场景
    Web 层授权HTTP 请求(URL/Method)接口访问控制、静态资源保护
    方法层授权方法调用(@PreAuthorize)服务层方法权限校验
    数据层授权数据库数据行 / 列多租户数据隔离、数据脱敏

核心配置入口:http.authorizeHttpRequests()
在 Spring Security 6+ 版本中,http.authorizeHttpRequests() 是开启 Web 层授权的核心配置方法,替代了旧版本的 authorizeRequests()

  • 配置位置:通过 @Bean 声明 SecurityFilterChain 时进行配置,示例框架如下:
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 开启 Web 层授权配置.authorizeHttpRequests(auth -> auth// 具体的 URL 权限规则.requestMatchers("/public/**").permitAll().requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated());return http.build();}
    }
    
  • 底层作用:该方法会为 Spring Security 过滤器链注册 AuthorizationFilter(新版核心过滤器),替代旧版的 FilterSecurityInterceptor,负责拦截所有 HTTP 请求并执行授权逻辑。
    底层核心组件:三大组件协同工作
    Web 层授权的核心逻辑由 FilterSecurityInterceptor(旧版)/AuthorizationFilter(新版)、SecurityMetadataSourceAccessDecisionManager 三大组件协同完成,三者的关系类似安检流程:
组件核心职责类比角色
AuthorizationFilter拦截 HTTP 请求,触发授权流程,处理授权结果(放行 / 拒绝)安检门(触发者)
SecurityMetadataSource解析 URL 对应的权限规则(如 /admin/** 需 ADMIN 角色),提供给授权决策器安检规则表
AccessDecisionManager结合用户权限和 URL 权限规则,判断是否允许访问安检员(决策者)
  1. AuthorizationFilter:授权流程的「触发者」
    • 作用:作为 Spring Security 过滤器链中的关键过滤器,拦截所有进入应用的 HTTP 请求,启动授权流程:
      1. 拦截请求后,从 SecurityContext 中获取当前用户的认证信息(Authentication对象,包含用户角色 / 权限);
      2. 调用 SecurityMetadataSource 获取当前请求 URL 对应的权限规则(ConfigAttribute,如 ROLE_ADMIN);
      3. 调用 AccessDecisionManager 进行授权判断;
      4. 根据判断结果处理:放行请求(通过)或抛出 AccessDeniedException(拒绝)。
    • 新版 vs 旧版
      • 旧版:FilterSecurityInterceptor(同步拦截,功能单一);
      • 新版:AuthorizationFilter(支持异步请求,性能更优,是 Spring Security 推荐的新版本实现)。
  2. SecurityMetadataSource:权限规则的「提供者」
    • 核心功能:将我们配置的 URL 权限规则(如 .requestMatchers("/admin/**").hasRole("ADMIN"))解析为 ConfigAttribute 集合(即权限需求)。
      • 例如:配置 .hasRole("ADMIN") 会被解析为 ConfigAttribute 对象,其值为 ROLE_ADMIN
      • 配置 .permitAll() 会被解析为特殊的 ConfigAttribute(表示无需权限)。
    • 关键实现类ExpressionBasedFilterInvocationSecurityMetadataSource(默认实现),支持解析 SpEL 表达式(如 hasRole('ADMIN') and hasIpAddress('192.168.1.0/24'))。
  3. AccessDecisionManager:授权决策的「决策者」
    • 核心功能:作为授权的核心决策组件,接收两个输入参数,最终判断是否允许访问:
      1. 用户权限:从 Authentication 对象中获取(authentication.getAuthorities());
      2. URL 权限规则:从 SecurityMetadataSource 中获取(Collection<ConfigAttribute>)。
    • 默认实现AffirmativeBased(一票通过制),即只要有一个投票器同意,就允许访问。
    • 投票器机制AccessDecisionManager 本身不直接判断,而是委托给多个 AccessDecisionVoter(投票器)进行投票,再根据投票结果决策。Web 层授权默认包含 3 个投票器:
      • WebExpressionVoter:解析 SpEL 表达式(如 hasRolehasIpAddress)并投票;
      • RoleVoter:判断用户是否拥有规则要求的角色(基于 ROLE_ 前缀);
      • AuthenticatedVoter:判断用户的认证状态(如是否登录、是否通过 Remember-Me 登录)。

Web 层授权的完整工作流程

结合前后端分离场景,以用户访问 /admin/user/list 接口为例,梳理完整授权流程:

在这里插入图片描述

前后端分离场景适配:当抛出 AccessDeniedException 时,Spring Security 会通过 AuthenticationEntryPoint 或全局异常处理器,返回 JSON 格式的 403 响应(而非跳转页面),示例响应:

{"code": 403,"message": "权限不足,无法访问该资源","data": null
}

注意事项

  1. 规则匹配顺序requestMatchers 按声明顺序匹配,第一个命中的规则生效(例如:先配置 /admin/** 规则,再配置 /** 规则,避免管理员路径被全局放行规则覆盖)。
  2. 角色与权限的区别hasRole 会自动添加 ROLE_ 前缀,而 hasAuthority 直接匹配权限字符串(前后端分离项目中,建议使用 hasAuthority 更灵活,避免前缀干扰)。
  3. 匿名用户的处理:未登录用户默认是「匿名用户」(AnonymousAuthenticationToken),permitAll() 允许匿名用户访问,而 authenticated() 会拒绝匿名用户。

基本使用结构

核心配置结构:SecurityFilterChain 的角色

Web 层授权的所有规则都围绕 SecurityFilterChain 这个核心 Bean 配置,它相当于 Spring Security 的「权限控制总开关」,定义了:

  • 哪些 URL 需要什么权限才能访问;
  • 采用哪种登录方式(表单登录、JWT 等);
  • 是否开启 CSRF 防护、会话管理等附加功能。

基本骨架如下:

@Configuration
@EnableWebSecurity // 开启 Spring Security 功能
public class SecurityConfig {// 核心配置:定义安全过滤链@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 1. 配置 URL 授权规则(核心).authorizeHttpRequests(auth -> auth// 具体的 URL 匹配与权限要求.requestMatchers("/admin/**").hasRole("ADMIN").requestMatchers("/user/**").hasAnyRole("USER", "ADMIN").requestMatchers("/public/**", "/login", "/css/**").permitAll().anyRequest().authenticated())// 2. 配置登录方式(此处用默认表单登录).formLogin(withDefaults());return http.build(); // 构建过滤链}
}
  • 必须添加 @EnableWebSecurity 注解,否则 Spring Security 不会加载该配置;
  • HttpSecurity 是配置的核心入口,通过链式调用添加各种规则。

以示例代码为例,逐行拆解 authorizeHttpRequests 中的核心配置:

  1. .requestMatchers("/admin/**").hasRole("ADMIN")
    • requestMatchers("/admin/\**"):定义 URL 匹配模式,/admin/** 表示「以 /admin/ 开头的所有路径」(Ant 风格通配符,** 匹配任意层级的路径);
      例如:/admin/user/admin/order/list 都会被匹配;
    • .hasRole("ADMIN"):指定访问该路径的权限要求 —— 用户必须具备 ADMIN 角色(底层会自动拼接 ROLE_ 前缀,实际校验 ROLE_ADMIN 权限)。
  2. .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
    • hasAnyRole("USER", "ADMIN"):表示用户只要具备 USERADMIN 中的任意一个角色即可访问(底层对应 ROLE_USERROLE_ADMIN);
      适用场景:普通用户和管理员都能访问的资源(如用户个人中心 /user/profile)。
  3. .requestMatchers("/public/**", "/login", "/css/**").permitAll()
    • permitAll():允许所有用户访问,包括匿名用户(未登录状态);
      典型场景:
      • 公开资源(/public/**,如首页、公告列表);
      • 登录页(/login,必须允许匿名访问,否则无法登录);
      • 静态资源(/css/**/js/**/images/**,避免样式 / 脚本无法加载)。
  4. .anyRequest().authenticated()
    • anyRequest():匹配所有未被前面规则匹配的请求(兜底规则);
    • authenticated():要求用户必须登录(即不能是匿名用户);
      作用:避免遗漏未配置的 URL,确保「未明确放行的资源必须登录才能访问」,是安全配置的最佳实践。
  5. .formLogin(withDefaults())
    • 配置登录方式为「表单登录」,withDefaults() 表示使用 Spring Security 的默认配置:
      • 默认登录页路径:/login(GET 请求,用户输入账号密码);
      • 默认登录提交路径:/login(POST 请求,携带 usernamepassword 参数);
      • 登录成功后默认跳转:之前访问的受保护页面(如访问 /user/info 未登录,登录后自动跳转至此);
      • 登录失败默认跳转:/login?error

执行顺序:规则匹配的优先级原则

Spring Security 对 URL 的匹配严格遵循「先声明先匹配」原则,整个流程可分为 4 步:

在这里插入图片描述

关键细节

  • 一旦找到第一个匹配的 requestMatchers,后续规则不再生效;
  • 兜底规则 .anyRequest() 必须放在最后,否则会覆盖前面的具体规则(例如:如果先写 .anyRequest().authenticated(),后面的 /public/** 规则会失效)。

反例(错误写法)

.authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll() // 先匹配所有路径,直接放行.requestMatchers("/admin/**").hasRole("ADMIN") // 永远不会生效
)

正例(正确写法)

.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").hasRole("ADMIN") // 具体规则放前面.requestMatchers("/public/**").permitAll().anyRequest().authenticated() // 兜底规则放最后
)

异常处理:授权失败的表现

当授权失败(用户不满足权限要求)时,Spring Security 会抛出 AccessDeniedException,具体表现取决于场景:

  • 未登录用户访问受保护资源(如 /user/**):
    • 若配置了表单登录,会自动跳转到登录页(/login);
    • 前后端分离场景(无表单登录),会返回 401 Unauthorized。
  • 已登录用户但权限不足(如普通用户访问 /admin/**):
    • 无论哪种场景,都会返回 403 Forbidden(禁止访问);
    • 可通过全局异常处理器自定义响应格式(如 JSON 提示「权限不足」)。

扩展场景:适配前后端分离与 RESTful API

示例中的配置是基于传统 Web 应用(表单登录),若用于前后端分离的 RESTful API,需调整登录方式和响应处理:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/public/**").permitAll().requestMatchers("/api/admin/**").hasRole("ADMIN").requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN").anyRequest().authenticated()).formLogin(form -> form.disable() // 关闭默认表单登录).httpBasic(withDefaults()) // 启用 HTTP Basic 认证(适合 API 场景).exceptionHandling(ex -> ex// 自定义 401/403 响应.authenticationEntryPoint((request, response, ex) -> {response.setContentType("application/json");response.setStatus(401);response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}");}).accessDeniedHandler((request, response, ex) -> {response.setContentType("application/json");response.setStatus(403);response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}");}));return http.build();
}

核心 API

Spring Security 提供了一系列 API 用于配置 URL 权限规则,这些 API 是实现 Web 层授权的基础。

  1. .authorizeHttpRequests():开启 URL 授权的总开关
    • 核心作用:启动 Spring Security 的 URL 层级授权控制,是所有 URL 权限规则的入口。
    • 底层逻辑
      • 新版(Spring Security 6+):会自动注册 AuthorizationFilter 过滤器,替代旧版的 FilterSecurityInterceptor,负责拦截 HTTP 请求并执行授权逻辑;
      • 旧版:依赖 FilterSecurityInterceptor,功能类似但不支持异步请求。
    • 使用示例
      http.authorizeHttpRequests(auth -> auth // 此处配置具体的 URL 权限规则.requestMatchers("/public/**").permitAll().anyRequest().authenticated()
      );
      
    • 注意:必须先调用此方法,才能链式配置后续的 requestMatchers 等规则。
  2. .requestMatchers(String...):定义 URL 匹配模式
    • 核心作用:指定需要匹配的 URL 路径(支持 Ant 风格通配符),是权限规则的「作用范围」。
    • 匹配规则(Ant 风格,默认实现):
      • ?:匹配单个字符(如 /user/? 匹配 /user/1/user/a,但不匹配 /user/12);
      • *:匹配单层路径的任意字符(如 /user/* 匹配 /user/1/user/list,但不匹配 /user/1/detail);
      • **:匹配多层路径的任意字符(如 /user/** 匹配 /user/1/user/1/detail/user/list/2)。
    • 关键特性顺序优先—— 第一个匹配的规则会直接生效,后续规则不再执行(务必将具体路径放在前面,通用路径放在后面)。
    • 扩展用法:除了字符串路径,还支持传入 RequestMatcher 接口实现类(如正则匹配):
      // 正则匹配:仅允许 /admin/ 后接数字的路径(如 /admin/123)
      .requestMatchers(new RegexRequestMatcher("/admin/\\d+", null)) .hasRole("ADMIN")
      
  3. .hasRole(String role):单个角色校验
    • 核心作用:要求访问该 URL 的用户必须具备指定角色。
    • 底层处理:Spring Security 会自动为角色添加 ROLE_ 前缀,因此:
      • .hasRole("ADMIN") 等价于 .hasAuthority("ROLE_ADMIN")
      • 若用户的权限集合(Authentication.getAuthorities())中包含 ROLE_ADMIN,则校验通过。
    • 使用场景:单一角色控制的资源(如管理员后台仅允许 ADMIN 角色访问)。
    • 注意:若数据库中存储的角色已包含 ROLE_ 前缀(如 ROLE_ADMIN),使用 .hasRole("ROLE_ADMIN") 会导致最终匹配 ROLE_ROLE_ADMIN,引发权限失效,此时应改用 .hasAuthority("ROLE_ADMIN")
  4. .hasAnyRole(String... roles):多角色任意匹配
    • 核心作用:用户只需具备指定角色中的任意一个即可访问(逻辑「或」)。
    • 底层处理:同样自动添加 ROLE_ 前缀,例如:
      • .hasAnyRole("ADMIN", "MANAGER") 等价于 .hasAnyAuthority("ROLE_ADMIN", "ROLE_MANAGER")
    • 使用场景:多角色共享的资源(如仪表盘允许管理员、经理访问)。
    • 示例
      // 产品列表允许 ADMIN、MANAGER、USER 角色访问
      .requestMatchers("/products/**").hasAnyRole("ADMIN", "MANAGER", "USER")
      
  5. .hasAuthority(String authority):单个权限校验
    • 核心作用:直接校验用户是否具备指定的权限字符串(无 ROLE_ 前缀自动添加,更灵活)。
    • hasRole 的区别
      方法是否自动添加 ROLE_ 前缀适用场景
      .hasRole()仅用于角色校验(约定前缀)
      .hasAuthority()任意权限字符串(如 API 权限)
    • 使用场景:细粒度权限控制(如 API 接口的 READWRITE 权限)。
    • 示例
      // 仅允许具备 API_READ 权限的用户访问接口
      .requestMatchers("/api/data").hasAuthority("API_READ")
      
  6. .hasAnyAuthority(String... authorities):多权限任意匹配
    • 核心作用:用户只需具备指定权限中的任意一个即可访问(逻辑「或」)。
    • 使用场景:多权限共享的资源(如编辑功能允许 WRITEADMIN 权限)。
    • 示例
      // 允许具备 API_WRITE 或 API_ADMIN 权限的用户修改数据
      .requestMatchers("/api/data").hasAnyAuthority("API_WRITE", "API_ADMIN")
      
  7. .permitAll():允许所有用户访问
    • 核心作用:对匹配的 URL 放行所有请求,包括匿名用户(未登录)和已登录用户。
    • 典型场景
      • 公开资源(首页、公告、注册页);
      • 登录页(/login)和登录接口(/doLogin);
      • 静态资源(CSS、JS、图片等,避免样式加载失败)。
    • 示例
      .requestMatchers("/", "/login", "/css/**", "/images/**").permitAll()
      
    • 注意:若将 permitAll() 用于 /**(所有路径),会导致所有权限控制失效,需谨慎使用。
  8. .denyAll():拒绝所有访问
    • 核心作用:无论用户是否登录、具备何种权限,均拒绝访问匹配的 URL。
    • 使用场景:完全禁止访问的资源(如废弃接口、敏感路径)。
    • 示例
      // 禁止访问 /secret 路径(无论任何用户)
      .requestMatchers("/secret").denyAll()
      
    • 效果:访问时会直接抛出 AccessDeniedException,返回 403 Forbidden。
  9. .authenticated():要求用户已登录
    • 核心作用:仅允许已认证用户(登录用户)访问,拒绝匿名用户。
    • permitAll() 的区别
      • permitAll():允许匿名 + 已登录用户;
      • authenticated():仅允许已登录用户。
    • 典型用法:作为兜底规则,确保未明确配置的 URL 必须登录才能访问:
      .anyRequest().authenticated() // 所有未匹配的路径均需登录
      
  10. .anonymous():仅允许匿名用户访问
    • 核心作用:仅允许未登录用户(匿名用户)访问,已登录用户反而被拒绝。
    • 使用场景:登录页、注册页、忘记密码页等(已登录用户无需访问这些页面)。
    • 示例
      // 注册和忘记密码页面仅允许未登录用户访问
      .requestMatchers("/register", "/forgot-password").anonymous()
      
    • 注意:匿名用户在 Spring Security 中被封装为 AnonymousAuthenticationToken,其权限默认包含 ROLE_ANONYMOUS
  11. .fullyAuthenticated():要求非 Remember-Me 登录
    • 核心作用:仅允许通过「真实登录」(输入账号密码)的用户访问,拒绝通过 Remember-Me 功能自动登录的用户。
    • 背景:Remember-Me 登录是一种「弱认证」(依赖 Cookie),适合访问非敏感资源;而修改密码、支付等敏感操作需强制用户重新输入密码(强认证)。
    • 示例
      // 修改密码页面必须通过真实登录访问,拒绝 Remember-Me 自动登录
      .requestMatchers("/security/change-password").fullyAuthenticated()
      
  12. .rememberMe():仅允许 Remember-Me 登录用户访问
    • 核心作用:与 fullyAuthenticated() 相反,仅允许通过 Remember-Me 功能自动登录的用户访问。
    • 使用场景:限制某些低敏感资源仅对 Remember-Me 用户开放(实际应用中较少见,更多用于特殊业务场景)。
    • 示例
      // 浏览历史仅允许 Remember-Me 登录用户访问
      .requestMatchers("/history").rememberMe()
      

使用场景对照表:

需求场景推荐 API示例
公开资源(允许匿名).permitAll()/login, /public/**
必须登录才能访问.authenticated()兜底规则 .anyRequest()
仅未登录用户可访问.anonymous()/register, /forgot-password
单一角色控制.hasRole()ADMIN 角色访问 /admin/**
多角色任意一个.hasAnyRole()ADMINUSER 访问 /user/**
细粒度权限(无角色前缀).hasAuthority()API_READ 权限访问 /api/**
多权限任意一个.hasAnyAuthority()API_READAPI_WRITE
禁止所有访问.denyAll()废弃接口 /old/**
敏感操作(需真实登录).fullyAuthenticated()修改密码 /change-password
弱认证用户(Remember-Me).rememberMe()浏览历史 /history

授权匹配顺序

在 Spring Security 的 Web 层授权配置中,规则的声明顺序是决定权限控制是否有效的关键。即使配置了正确的权限表达式,若顺序不当,也会导致权限失效或安全漏洞。

匹配顺序的底层原理:「-first match wins」(先匹配者生效)

Spring Security 对 URL 权限规则的匹配遵循「顺序遍历、首次命中即终止」的原则,底层流程如下:

  1. 当一个 HTTP 请求到达时,AuthorizationFilter 会按配置的 requestMatchers 顺序依次检查 URL 是否匹配;
  2. 一旦找到第一个匹配的 requestMatchers 规则,就立即应用该规则对应的权限要求(如 hasRolepermitAll 等);
  3. 后续的 requestMatchers 规则不再参与匹配,无论其是否更精确。

这种机制类似 Java 中的 if-else if 语句 —— 第一个满足条件的分支会被执行,其他分支被忽略。

顺序不当导致的权限失效

最常见的错误是「通用规则前置,覆盖具体规则」,以下结合实例分析问题及后果:

案例 1:全局放行规则在前,管理员规则失效

// 错误配置
.authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll() // 规则1:所有路径放行.requestMatchers("/admin/**").hasRole("ADMIN") // 规则2:管理员路径需ADMIN角色.anyRequest().authenticated()
)
  • 问题/** 匹配所有 URL(包括 /admin/user/admin/order 等),规则 1 会被首先命中,导致规则 2 永远不会执行;
  • 后果/admin/** 路径被意外放行,匿名用户或普通用户可直接访问管理员资源,造成严重安全漏洞。

案例 2:较宽泛的路径规则在前,精确路径规则失效

// 错误配置
.authorizeHttpRequests(auth -> auth.requestMatchers("/user/*").hasRole("USER") // 规则1:匹配/user/1、/user/list(单层路径).requestMatchers("/user/admin").hasRole("ADMIN") // 规则2:精确匹配/user/admin
)
  • 问题/user/admin 同时匹配规则 1(/user/*)和规则 2,但规则 1 在前,导致规则 2 失效;
  • 后果:本应仅允许 ADMIN 访问的 /user/admin,实际只需 USER 角色即可访问,权限控制降级。

正确的配置顺序原则:「从具体到通用,兜底规则最后」

为避免顺序问题,需严格遵循以下配置顺序(优先级从高到低):

  1. 最精确的路径(如 /admin/user/delete);
  2. 较具体的路径(如 /admin/**);
  3. 通用路径(如 /user/**/public/**);
  4. 兜底规则.anyRequest())。

正确案例 1:管理员路径优先于全局放行

// 正确配置
.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").hasRole("ADMIN") // 步骤1:先匹配管理员路径.requestMatchers("/public/**").permitAll() // 步骤2:再匹配公开路径.requestMatchers("/user/**").hasRole("USER") // 步骤3:再匹配用户路径.anyRequest().authenticated() // 步骤4:最后兜底(未匹配的路径需登录)
)
  • 解析:
    • /admin/user 会匹配步骤 1,要求 ADMIN 角色;
    • /public/home 会匹配步骤 2,直接放行;
    • /user/profile 会匹配步骤 3,要求 USER 角色;
    • 未被上述规则匹配的路径(如 /order)会触发步骤 4,要求登录。

正确案例 2:精确路径优先于宽泛路径

// 正确配置
.authorizeHttpRequests(auth -> auth.requestMatchers("/user/admin").hasRole("ADMIN") // 步骤1:精确匹配/user/admin.requestMatchers("/user/*").hasRole("USER") // 步骤2:宽泛匹配/user/*
)
  • 解析:
    • /user/admin 会优先匹配步骤 1,要求 ADMIN 角色;
    • 其他 /user/1/user/list 等路径会匹配步骤 2,要求 USER 角色。

兜底规则 .anyRequest() 的特殊地位

.anyRequest() 是匹配所有未被前面规则覆盖的请求的「终极兜底」,必须放在所有 requestMatchers 之后,否则会覆盖后续规则。

错误用法(.anyRequest() 前置):

// 错误配置
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated() // 兜底规则前置.requestMatchers("/public/**").permitAll() // 此规则永远无效
)
  • 后果/public/** 会被 .anyRequest().authenticated() 匹配,导致公开资源需要登录才能访问。

正确用法(.anyRequest() 后置):

// 正确配置
.authorizeHttpRequests(auth -> auth.requestMatchers("/public/**").permitAll().requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated() // 放在最后,兜底未匹配的路径
)
  • 作用:确保所有未被明确配置的 URL 都需要登录(避免遗漏导致的安全风险),是生产环境的最佳实践。

匹配机制

Spring Security 中,.requestMatchers() 对 URL 的匹配并非简单的字符串比对,而是通过 RequestMatcher 接口的实现类完成的。不同的实现类支持不同的匹配规则(如 Ant 风格、正则表达式等)。

核心接口:RequestMatcher 的作用

RequestMatcher 是 Spring Security 用于匹配 HTTP 请求的核心接口,定义了一个抽象方法:

boolean matches(HttpServletRequest request);
  • 作用:判断当前 HTTP 请求(包含 URL、请求方法、参数等信息)是否符合匹配规则;
  • 扩展:通过实现该接口,可自定义任意复杂的匹配逻辑(如结合请求头、IP 地址、参数等)。

.requestMatchers() 方法本质上是接收一个或多个 RequestMatcher 实例,用于定义 URL 的匹配范围。

常用 RequestMatcher 实现类详解

Spring Security 提供了多种内置实现类,覆盖绝大多数常见场景,以下是最常用的 5 种:

  1. AntPathRequestMatcher:Ant 风格通配符匹配(默认)
    • 核心特点:使用 Ant 风格的通配符匹配 URL,是 .requestMatchers(String...) 方法的默认实现(传入字符串路径时,自动转换为 AntPathRequestMatcher)。

    • 通配符规则

      通配符含义示例
      ?匹配单个字符(不含 //user/? 匹配 /user/1/user/a
      *匹配单层路径的任意字符(不含子路径)/user/* 匹配 /user/list/user/1
      **匹配多层路径的任意字符(含子路径)/user/** 匹配 /user/1/detail/user/list/2
    • 请求方法匹配:默认匹配所有 HTTP 方法(GET、POST 等),也可指定特定方法:

      // 仅匹配 POST 请求的 /admin/user
      .requestMatchers(new AntPathRequestMatcher("/admin/user", "POST")).hasRole("ADMIN")
      
    • 适用场景:绝大多数 Web 应用的 URL 匹配(简单、直观,覆盖大多场景)。

  2. RegexRequestMatcher:正则表达式匹配
    • 核心特点:通过正则表达式匹配 URL,支持更复杂的模式匹配(如特定格式的路径参数)。
    • 构造方法
      // pattern:正则表达式;httpMethod:允许的请求方法(null 表示任意方法)
      new RegexRequestMatcher(String pattern, String httpMethod)
      
    • 示例场景
      • 匹配 /admin/ 后接数字的路径(如 /admin/123/admin/456):
        .requestMatchers(new RegexRequestMatcher("/admin/\\d+", null)).hasRole("ADMIN")
        
      • 匹配以 .html 结尾的静态页面:
        .requestMatchers(new RegexRequestMatcher(".+\\.html$", "GET")).permitAll()
        
    • 注意:正则表达式中的特殊字符(如 ./)需要转义(用 \\),且匹配范围是整个 URL 路径(而非部分匹配)。
  3. MvcRequestMatcher:与 Spring MVC 路由一致的匹配
    • 核心特点:基于 Spring MVC 的 HandlerMapping 匹配 URL,与 @RequestMapping 的路径匹配规则完全一致(支持路径变量、矩阵变量等 MVC 特性)。
    • 解决的问题:传统 AntPathRequestMatcher 可能与 Spring MVC 的路由规则不一致,例如:
      • Spring MVC 中 /user/{id}/user/{id}/ 视为同一路径(自动忽略尾部斜杠);
      • AntPathRequestMatcher 会严格区分 /user/1/user/1/
    • 使用方式:需要通过 MvcRequestMatcher.Builder 构建,依赖 Spring MVC 的 HandlerMapping
      @Configuration
      public class SecurityConfig {// 注入 MvcRequestMatcher 构建器@BeanMvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {return new MvcRequestMatcher.Builder(introspector);}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {http.authorizeHttpRequests(auth -> auth// 匹配 Spring MVC 路由中的 /user/{id}(忽略尾部斜杠).requestMatchers(mvc.pattern("/user/{id}")).hasRole("USER"));return http.build();}
      }
      
    • 适用场景:使用 Spring MVC 的项目,确保权限匹配规则与控制器路由完全一致。
  4. AndRequestMatcher:逻辑「与」匹配
    • 核心特点:需同时满足多个 RequestMatcher 的条件才视为匹配(逻辑「与」)。
    • 示例:匹配「POST 方法且路径为 /admin/user」的请求:
      RequestMatcher postMethod = new AntPathRequestMatcher("/admin/user", "POST");
      RequestMatcher adminPath = new AntPathRequestMatcher("/admin/**");
      // 同时满足两个条件才匹配
      RequestMatcher andMatcher = new AndRequestMatcher(postMethod, adminPath);http.authorizeHttpRequests(auth -> auth.requestMatchers(andMatcher).hasRole("ADMIN")
      );
      
  5. OrRequestMatcher:逻辑「或」匹配
    • 核心特点:满足多个 RequestMatcher 中的任意一个即视为匹配(逻辑「或」)。
    • 示例:匹配「/admin/**/manager/**」的路径:
      RequestMatcher adminMatcher = new AntPathRequestMatcher("/admin/**");
      RequestMatcher managerMatcher = new AntPathRequestMatcher("/manager/**");
      // 满足任意一个条件即匹配
      RequestMatcher orMatcher = new OrRequestMatcher(adminMatcher, managerMatcher);http.authorizeHttpRequests(auth -> auth.requestMatchers(orMatcher).hasAnyRole("ADMIN", "MANAGER")
      );
      

不同匹配器适用于不同场景,选择原则如下:

场景需求推荐匹配器优势
简单 URL 匹配(如 /admin/**AntPathRequestMatcher简单直观,默认支持
复杂格式匹配(如数字 ID 路径)RegexRequestMatcher正则表达式灵活,支持格式校验
与 Spring MVC 路由一致MvcRequestMatcher避免路径匹配不一致问题
多条件同时满足AndRequestMatcher组合多个匹配规则(如路径 + 方法)
多条件任意满足OrRequestMatcher覆盖多个路径或规则

注意事项

  1. AntPathRequestMatcher 的尾部斜杠问题
    • 严格区分 /user/user/(前者不包含斜杠,后者包含);
    • 若需忽略尾部斜杠,建议使用 MvcRequestMatcher(与 Spring MVC 行为一致)。
  2. 正则表达式的性能问题
    • RegexRequestMatcher 的匹配效率低于 AntPathRequestMatcher,复杂正则可能影响性能;
    • 非必要场景(如简单路径匹配)优先使用 Ant 风格。
  3. 请求方法的匹配
    • 所有匹配器默认匹配任意 HTTP 方法,若需限制方法(如仅允许 POST),需显式指定(如 AntPathRequestMatcher 的第二个参数)。
  4. 组合匹配器的可读性
    • 复杂的 And/Or 组合可能降低配置可读性,建议提取为单独的 RequestMatcher 变量,或使用 SpEL 表达式替代(如 .access("hasRole('ADMIN') and request.method == 'POST'"))。

默认权限评估器

Spring Security 的 Web 层授权最终能否通过,并非由单一组件决定,而是由一组「投票器(AccessDecisionVoter)」共同参与决策。这些投票器各自负责不同维度的校验,最终通过「投票结果」决定是否允许访问。

核心原理:投票器与决策管理器的协作模式

Web 层授权的决策流程本质是「多人投票,少数服从多数(或一票通过)」的机制:

  • 投票器(AccessDecisionVoter):负责从特定维度判断用户是否有权限(如角色是否匹配、是否已认证),并给出「同意(ACCESS_GRANTED)」「反对(ACCESS_DENIED)」或「弃权(ACCESS_ABSTAIN)」的投票结果;
  • 决策管理器(AccessDecisionManager):接收所有投票器的结果,按预设规则(如一票通过、全票通过)最终决定是否允许访问。

在 Web 层授权中,Spring Security 默认使用 AffirmativeBased 决策管理器(一票通过制),只要有一个投票器同意,就允许访问。

三个默认投票器:分工与协作

Spring Security 为 Web 层授权默认注册了三个投票器,分别负责不同维度的校验,覆盖绝大多数常见场景:

  1. WebExpressionVoter:SpEL 表达式解析器(核心投票器)
    • 核心作用:解析并评估 URL 权限规则中配置的 SpEL 表达式(如 .hasRole("ADMIN").hasIpAddress("192.168.1.0/24")),根据表达式结果投票。
    • 支持的表达式函数(部分常用):
      函数含义
      hasRole('ROLE_X')用户是否具备指定角色(自动加 ROLE_ 前缀)
      hasAuthority('X')用户是否具备指定权限(无前缀)
      hasIpAddress('IP')请求来源 IP 是否匹配
      isAuthenticated()用户是否已认证(非匿名)
      isRememberMe()用户是否通过 Remember-Me 登录
    • 工作流程
      1. 接收 ConfigAttribute(权限规则,如 hasRole('ADMIN'))和用户认证信息 Authentication
      2. 将 SpEL 表达式编译为可执行代码,结合当前请求上下文(HttpServletRequest)评估结果;
      3. 若表达式返回 true,投「同意票」;否则投「反对票」;若无法解析表达式,投「弃权票」。
    • 示例:当配置 .requestMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')") 时,WebExpressionVoter 会同时校验用户角色和 IP 地址,两者都满足才投同意票。
  2. RoleVoter:角色权限专用投票器
    • 核心作用:专门校验用户是否具备规则要求的「角色权限」(基于 ROLE_ 前缀的权限字符串)。
    • 工作原理
      1. 仅处理 ConfigAttribute 中以 ROLE_ 开头的权限规则(如 ROLE_ADMIN);
      2. 检查用户的权限集合(Authentication.getAuthorities())中是否包含该角色;
      3. 若包含,投「同意票」;否则投「反对票」;若规则不以 ROLE_ 开头,投「弃权票」。
    • 与 WebExpressionVoter 的关系:当使用 .hasRole("ADMIN") 时,底层会被 WebExpressionVoter 解析为 hasRole('ADMIN') 表达式,而 RoleVoter 会同时检查 ROLE_ADMIN 角色 —— 两者都可能参与投票,但最终由决策管理器统一判断。
    • 注意RoleVoter 仅识别 ROLE_ 前缀的权限,若你的角色权限没有该前缀(如 ADMIN),需通过 RoleVoter.setRolePrefix("") 移除前缀限制,或改用 WebExpressionVoterhasAuthority 表达式。
  3. AuthenticatedVoter:认证状态投票器
    • 核心作用:根据用户的认证状态(匿名、已认证、Remember-Me 登录等)投票,专门处理与认证状态相关的规则(如 .authenticated().fullyAuthenticated())。
    • 支持的特殊权限规则
      规则常量对应配置方法含义
      IS_AUTHENTICATED_FULLY.fullyAuthenticated()仅允许真实登录用户(非 Remember-Me)
      IS_AUTHENTICATED_REMEMBERED.rememberMe()仅允许 Remember-Me 登录用户
      IS_AUTHENTICATED_ANONYMOUSLY.anonymous()仅允许匿名用户
    • 工作流程
      1. 识别 ConfigAttribute 中是否包含上述特殊规则;
      2. 检查用户的认证状态(从 Authentication 对象获取);
      3. 若状态匹配规则,投「同意票」;否则投「反对票」;若规则不匹配,投「弃权票」。
    • 示例:当配置 .requestMatchers("/change-password").fullyAuthenticated() 时,AuthenticatedVoter 会检查用户是否通过真实登录(非 Remember-Me),是则投同意票,否则投反对票。

扩展场景:自定义投票器与决策机制

若默认投票器无法满足需求(如需要结合用户部门、数据权限等复杂逻辑),可通过以下方式扩展:
自定义 AccessDecisionVoter
实现 AccessDecisionVoter 接口,重写 supports(支持的规则类型)和 vote(投票逻辑)方法:

// 示例:仅允许指定部门用户访问的投票器
public class DepartmentVoter implements AccessDecisionVoter<Object> {@Overridepublic boolean supports(ConfigAttribute attribute) {// 仅处理以 DEPARTMENT_ 开头的规则(如 DEPARTMENT_IT)return attribute.getAttribute() != null && attribute.getAttribute().startsWith("DEPARTMENT_");}@Overridepublic int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {// 获取用户部门(假设用户详情中包含部门信息)String userDept = ((CustomUserDetails) authentication.getPrincipal()).getDepartment();for (ConfigAttribute attr : attributes) {String requiredDept = attr.getAttribute().replace("DEPARTMENT_", "");if (requiredDept.equals(userDept)) {return ACCESS_GRANTED; // 部门匹配,投同意票}}return ACCESS_DENIED; // 部门不匹配,投反对票}
}

替换决策管理器
默认的 AffirmativeBased(一票通过)可能不满足严格场景,可替换为:

  • ConsensusBased:多数票通过(同意票 > 反对票);
  • UnanimousBased:全票通过(所有投票器都同意或弃权)。

配置方式:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).exceptionHandling(ex -> ex.accessDeniedHandler(new AccessDeniedHandler() { ... }))// 替换决策管理器为全票通过制.with(new Customizer<AuthorizationManagerRequestMatcherRegistry>() {@Overridepublic void customize(AuthorizationManagerRequestMatcherRegistry registry) {registry.setAccessDecisionManager(new UnanimousBased(Arrays.asList(new WebExpressionVoter(),new RoleVoter(),new AuthenticatedVoter(),new DepartmentVoter() // 加入自定义投票器)));}});return http.build();
}

SpEL 表达式支持

Spring Security 对 Spring 表达式语言(SpEL)的支持,为 URL 层授权提供了极强的灵活性。通过 SpEL 表达式,你可以突破单一权限 / 角色的校验限制,实现多条件组合(如角色 + IP 地址)、动态权限判断等复杂场景。

核心原理:SpEL 表达式的执行机制

在 Spring Security 中,SpEL 表达式的解析和评估主要由 WebExpressionVoter 投票器完成(对应第六模块的默认投票器),其核心流程如下:

  1. 当配置 .access("SpEL 表达式") 时,该表达式会被封装为 ConfigAttribute 对象;
  2. 授权流程触发时,WebExpressionVoter 会从 SecurityContext 中获取当前用户的 Authentication 对象,同时获取当前 HTTP 请求上下文(HttpServletRequest);
  3. 将用户信息、请求信息注入 SpEL 表达式上下文,编译并执行表达式;
  4. 根据表达式执行结果投票:返回 true 投「同意票」,返回 false 投「反对票」。

关键优势:SpEL 表达式支持逻辑运算(and/or/not)、函数调用、变量引用等特性,可实现远超基础 API 的复杂权限逻辑。

常用 SpEL 表达式函数详解

Spring Security 内置了一系列专用的 SpEL 函数,覆盖角色、权限、认证状态、请求信息等核心场景,以下是最常用的函数:

函数名含义示例用法
hasRole('ROLE_X')验证用户是否具备指定角色(自动拼接 ROLE_ 前缀)hasRole('ADMIN') 等价于校验 ROLE_ADMIN
hasAnyRole('ROLE_X', 'ROLE_Y')验证用户是否具备任意一个指定角色(自动拼接 ROLE_前缀)hasAnyRole('ADMIN', 'USER')
hasAuthority('X')验证用户是否具备指定权限(无前缀,直接匹配权限字符串)hasAuthority('API_WRITE')
hasAnyAuthority('X', 'Y')验证用户是否具备任意一个指定权限(无前缀)hasAnyAuthority('API_READ', 'API_WRITE')
hasIpAddress('IP')验证请求来源 IP 是否匹配(支持 IP 地址、子网掩码)hasIpAddress('192.168.1.0/24')
isAnonymous()验证当前用户是否为匿名用户(未登录)isAnonymous()
isAuthenticated()验证当前用户是否已认证(登录用户,包括 Remember-Me 自动登录)isAuthenticated()
isRememberMe()验证当前用户是否通过 Remember-Me 功能自动登录isRememberMe()
isFullyAuthenticated()验证当前用户是否为「完全认证」(真实登录,非 Remember-Me)isFullyAuthenticated()
permitAll()直接允许访问(等价于 .permitAll() 方法)permitAll()
denyAll()直接拒绝访问(等价于 .denyAll() 方法)denyAll()
authentication引用当前用户的 Authentication 对象(可获取用户详情、权限等)authentication.principal.username
request引用当前 HTTP 请求对象(可获取请求方法、参数、头信息等)request.method == 'POST'

注意hasRolehasAuthority 的区别在于前缀处理,前后端分离项目中建议优先使用 hasAuthority,避免前缀干扰。

SpEL 表达式的典型用法

结合前后端分离项目的实际需求,以下是 SpEL 表达式的高频应用场景:

  1. 多条件组合校验
    需求:管理员角色(ADMIN)且请求来源 IP 为内网(192.168.1.0/24)才能访问管理员接口。
    http.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')").anyRequest().authenticated()
    );
    
  2. 限制请求方法
    需求:仅允许 POST 方法访问 /api/user 接口,且用户需具备 API_WRITE 权限。
    http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/user").access("hasAuthority('API_WRITE') and request.method == 'POST'").anyRequest().authenticated()
    );
    
  3. 基于用户属性的动态校验
    需求:用户只能访问自己的订单(订单 ID 与用户名关联,假设用户名为 admin,订单 ID 为 admin_123)。
    // 假设请求路径为 /api/orders/{orderId},用户详情存储在 CustomUserDetails 中
    http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/orders/{orderId}").access("authentication.principal.username == #orderId.split('_')[0]").anyRequest().authenticated()
    );
    
    解析:#orderId 引用路径变量,authentication.principal 获取当前用户详情,通过字符串分割判断订单归属。
  4. 拒绝 Remember-Me 用户访问敏感接口
    需求:修改密码接口仅允许真实登录用户访问,拒绝 Remember-Me 自动登录用户。
    http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/security/change-password").access("isFullyAuthenticated()").anyRequest().authenticated()
    );
    
  5. 结合请求头信息校验
    需求:仅允许携带 X-API-Key 请求头且值为 123456 的请求访问公开接口。
    http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/public/data").access("request.getHeader('X-API-Key') == '123456'").anyRequest().authenticated()
    );
    

自定义 SpEL 表达式函数

若内置函数无法满足需求(如结合业务逻辑的复杂校验),可自定义 SpEL 函数并注入 Spring Security 的表达式上下文。

  1. 实现自定义校验逻辑
    // 自定义工具类:校验用户是否为指定部门
    public class CustomSecurityExpressionUtil {// 假设用户详情中包含部门信息,返回 true 表示有权限public static boolean hasDepartment(Authentication authentication, String department) {if (!(authentication.getPrincipal() instanceof CustomUserDetails userDetails)) {return false;}return department.equals(userDetails.getDepartment());}
    }
    
  2. 扩展 SpEL 表达式上下文
    通过 WebSecurityConfigurerAdapterSecurityFilterChain 配置,将自定义函数注册到 SpEL 表达式解析器:
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {// 扩展 SpEL 表达式,添加自定义函数@Beanpublic DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();// 注册自定义函数:函数名 hasDepartment,对应工具类方法handler.getExpressionParser().getConfiguration().addMethodResolver(new StaticMethodResolver(CustomSecurityExpressionUtil.class));return handler;}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth// 使用自定义 SpEL 函数:仅 IT 部门的 ADMIN 可访问.requestMatchers("/admin/it/**").access("hasRole('ADMIN') and hasDepartment(authentication, 'IT')").anyRequest().authenticated()).formLogin(withDefaults());return http.build();}
    }
    
    效果hasDepartment(authentication, 'IT') 可直接在 SpEL 表达式中使用,实现基于部门的权限校验。

注意事项

  1. 表达式性能:复杂的 SpEL 表达式(如循环、大量字符串操作)可能影响接口响应速度,建议简化逻辑或缓存计算结果;
  2. 权限泄露风险:避免在表达式中硬编码敏感信息(如 IP 地址、密钥),建议通过配置文件注入;
  3. 用户详情类型转换:使用 authentication.principal 时,需确保其类型为自定义用户详情类(如 CustomUserDetails),避免类型转换异常;
  4. 路径变量引用:引用路径变量时(如 #orderId),需确保 requestMatchers 中声明了对应的变量名,且与表达式中的变量名一致。
http://www.dtcms.com/a/521228.html

相关文章:

  • 塘沽网站开发企业站网页制作实训步骤
  • jsp电影网站开发教程单位外部网站建设价格
  • 哈尔滨住房和城乡建设厅网站做网站原型现成的框架
  • 做网站实现发送信息功能号卡分销系统源码
  • 用Python Streamlit Sqlite3 写一个简单商品管理系统
  • LazyLLM 创新实践:LLM 与工具协同,构建智能客服问答与知识库检索助手
  • 网站主体负责人邮箱wordpress国外空间
  • 网站建设公制度网页设计与制作实训报告两千字
  • 算力赋能,智见未来 | 国鑫亮相ICG-20,共赴组学与AI新纪元
  • 阿里巴巴网站的功能win 无法卸载 wordpress
  • 慧园区:科技赋能下的城市空间新范式
  • 网站建设费一般是什么费用网页设计的背景代码大全
  • 现在网站开发语言有各大网站搜索引擎提交入口
  • 【MCU控制 初级手札】1.4 化合物 【化学基础】
  • 基于SpringBoot+Vue的DIY手工社预约管理系统(Echarts图形化、腾讯地图API)
  • 网站开发专业的建设设想慕课网站开发与实现
  • 【“具身智能”AI烹饪机器人系统 - 外委研发课题清单】
  • ELK运维之路(使用Logstatsh对日志进行处理综合案例)
  • 【开题答辩全过程】以 基于.NET的途乐旅游管理系统为例,包含答辩的问题和答案
  • 徐州机票网站开发短网址生成条形码
  • MySQL 的四种 Binlog 日志处理工具:Canal、Maxwell、Databus和 阿里云 DTS
  • 特效型网站asp网站幻灯片不显示
  • 基于RSSI修正的定位算法分析
  • p图做网站兼职手机网站建设规范
  • 如何将 ONLYOFFICE 文档社区版更新到 v9.1(Docker、Linux、Windows)
  • 网站架构和网络哈尔滨网站建设口碑好
  • CNN 模型搭建与训练:PyTorch 实战 CIFAR10 任务
  • 网站目录模板新月直播大全免费下载手机版官网
  • 常州建设工程监理员挂证网站网站网上商城建设方案
  • JavaEE知识点总结