【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(新版)、SecurityMetadataSource、AccessDecisionManager三大组件协同完成,三者的关系类似安检流程:
| 组件 | 核心职责 | 类比角色 |
|---|---|---|
| AuthorizationFilter | 拦截 HTTP 请求,触发授权流程,处理授权结果(放行 / 拒绝) | 安检门(触发者) |
| SecurityMetadataSource | 解析 URL 对应的权限规则(如 /admin/** 需 ADMIN 角色),提供给授权决策器 | 安检规则表 |
| AccessDecisionManager | 结合用户权限和 URL 权限规则,判断是否允许访问 | 安检员(决策者) |
- AuthorizationFilter:授权流程的「触发者」
- 作用:作为 Spring Security 过滤器链中的关键过滤器,拦截所有进入应用的 HTTP 请求,启动授权流程:
- 拦截请求后,从
SecurityContext中获取当前用户的认证信息(Authentication对象,包含用户角色 / 权限); - 调用
SecurityMetadataSource获取当前请求 URL 对应的权限规则(ConfigAttribute,如ROLE_ADMIN); - 调用
AccessDecisionManager进行授权判断; - 根据判断结果处理:放行请求(通过)或抛出
AccessDeniedException(拒绝)。
- 拦截请求后,从
- 新版 vs 旧版:
- 旧版:
FilterSecurityInterceptor(同步拦截,功能单一); - 新版:
AuthorizationFilter(支持异步请求,性能更优,是 Spring Security 推荐的新版本实现)。
- 旧版:
- 作用:作为 Spring Security 过滤器链中的关键过滤器,拦截所有进入应用的 HTTP 请求,启动授权流程:
- 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'))。
- 核心功能:将我们配置的 URL 权限规则(如
- AccessDecisionManager:授权决策的「决策者」
- 核心功能:作为授权的核心决策组件,接收两个输入参数,最终判断是否允许访问:
- 用户权限:从
Authentication对象中获取(authentication.getAuthorities()); - URL 权限规则:从
SecurityMetadataSource中获取(Collection<ConfigAttribute>)。
- 用户权限:从
- 默认实现:
AffirmativeBased(一票通过制),即只要有一个投票器同意,就允许访问。 - 投票器机制:
AccessDecisionManager本身不直接判断,而是委托给多个AccessDecisionVoter(投票器)进行投票,再根据投票结果决策。Web 层授权默认包含 3 个投票器:WebExpressionVoter:解析 SpEL 表达式(如hasRole、hasIpAddress)并投票;RoleVoter:判断用户是否拥有规则要求的角色(基于ROLE_前缀);AuthenticatedVoter:判断用户的认证状态(如是否登录、是否通过 Remember-Me 登录)。
- 核心功能:作为授权的核心决策组件,接收两个输入参数,最终判断是否允许访问:
Web 层授权的完整工作流程
结合前后端分离场景,以用户访问 /admin/user/list 接口为例,梳理完整授权流程:

前后端分离场景适配:当抛出 AccessDeniedException 时,Spring Security 会通过 AuthenticationEntryPoint 或全局异常处理器,返回 JSON 格式的 403 响应(而非跳转页面),示例响应:
{"code": 403,"message": "权限不足,无法访问该资源","data": null
}
注意事项
- 规则匹配顺序:
requestMatchers按声明顺序匹配,第一个命中的规则生效(例如:先配置/admin/**规则,再配置/**规则,避免管理员路径被全局放行规则覆盖)。 - 角色与权限的区别:
hasRole会自动添加ROLE_前缀,而hasAuthority直接匹配权限字符串(前后端分离项目中,建议使用hasAuthority更灵活,避免前缀干扰)。 - 匿名用户的处理:未登录用户默认是「匿名用户」(
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 中的核心配置:
.requestMatchers("/admin/**").hasRole("ADMIN")requestMatchers("/admin/\**"):定义 URL 匹配模式,/admin/**表示「以/admin/开头的所有路径」(Ant 风格通配符,**匹配任意层级的路径);
例如:/admin/user、/admin/order/list都会被匹配;.hasRole("ADMIN"):指定访问该路径的权限要求 —— 用户必须具备ADMIN角色(底层会自动拼接ROLE_前缀,实际校验ROLE_ADMIN权限)。
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")hasAnyRole("USER", "ADMIN"):表示用户只要具备USER或ADMIN中的任意一个角色即可访问(底层对应ROLE_USER或ROLE_ADMIN);
适用场景:普通用户和管理员都能访问的资源(如用户个人中心/user/profile)。
.requestMatchers("/public/**", "/login", "/css/**").permitAll()permitAll():允许所有用户访问,包括匿名用户(未登录状态);
典型场景:- 公开资源(
/public/**,如首页、公告列表); - 登录页(
/login,必须允许匿名访问,否则无法登录); - 静态资源(
/css/**、/js/**、/images/**,避免样式 / 脚本无法加载)。
- 公开资源(
.anyRequest().authenticated()anyRequest():匹配所有未被前面规则匹配的请求(兜底规则);authenticated():要求用户必须登录(即不能是匿名用户);
作用:避免遗漏未配置的 URL,确保「未明确放行的资源必须登录才能访问」,是安全配置的最佳实践。
.formLogin(withDefaults())- 配置登录方式为「表单登录」,
withDefaults()表示使用 Spring Security 的默认配置:- 默认登录页路径:
/login(GET 请求,用户输入账号密码); - 默认登录提交路径:
/login(POST 请求,携带username和password参数); - 登录成功后默认跳转:之前访问的受保护页面(如访问
/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 层授权的基础。
.authorizeHttpRequests():开启 URL 授权的总开关- 核心作用:启动 Spring Security 的 URL 层级授权控制,是所有 URL 权限规则的入口。
- 底层逻辑:
- 新版(Spring Security 6+):会自动注册
AuthorizationFilter过滤器,替代旧版的FilterSecurityInterceptor,负责拦截 HTTP 请求并执行授权逻辑; - 旧版:依赖
FilterSecurityInterceptor,功能类似但不支持异步请求。
- 新版(Spring Security 6+):会自动注册
- 使用示例:
http.authorizeHttpRequests(auth -> auth // 此处配置具体的 URL 权限规则.requestMatchers("/public/**").permitAll().anyRequest().authenticated() ); - 注意:必须先调用此方法,才能链式配置后续的
requestMatchers等规则。
.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")
.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")。
.hasAnyRole(String... roles):多角色任意匹配- 核心作用:用户只需具备指定角色中的任意一个即可访问(逻辑「或」)。
- 底层处理:同样自动添加
ROLE_前缀,例如:.hasAnyRole("ADMIN", "MANAGER")等价于.hasAnyAuthority("ROLE_ADMIN", "ROLE_MANAGER")。
- 使用场景:多角色共享的资源(如仪表盘允许管理员、经理访问)。
- 示例:
// 产品列表允许 ADMIN、MANAGER、USER 角色访问 .requestMatchers("/products/**").hasAnyRole("ADMIN", "MANAGER", "USER")
.hasAuthority(String authority):单个权限校验- 核心作用:直接校验用户是否具备指定的权限字符串(无
ROLE_前缀自动添加,更灵活)。 - 与
hasRole的区别:方法 是否自动添加 ROLE_前缀适用场景 .hasRole()是 仅用于角色校验(约定前缀) .hasAuthority()否 任意权限字符串(如 API 权限) - 使用场景:细粒度权限控制(如 API 接口的
READ、WRITE权限)。 - 示例:
// 仅允许具备 API_READ 权限的用户访问接口 .requestMatchers("/api/data").hasAuthority("API_READ")
- 核心作用:直接校验用户是否具备指定的权限字符串(无
.hasAnyAuthority(String... authorities):多权限任意匹配- 核心作用:用户只需具备指定权限中的任意一个即可访问(逻辑「或」)。
- 使用场景:多权限共享的资源(如编辑功能允许
WRITE或ADMIN权限)。 - 示例:
// 允许具备 API_WRITE 或 API_ADMIN 权限的用户修改数据 .requestMatchers("/api/data").hasAnyAuthority("API_WRITE", "API_ADMIN")
- .
permitAll():允许所有用户访问- 核心作用:对匹配的 URL 放行所有请求,包括匿名用户(未登录)和已登录用户。
- 典型场景:
- 公开资源(首页、公告、注册页);
- 登录页(
/login)和登录接口(/doLogin); - 静态资源(CSS、JS、图片等,避免样式加载失败)。
- 示例:
.requestMatchers("/", "/login", "/css/**", "/images/**").permitAll() - 注意:若将
permitAll()用于/**(所有路径),会导致所有权限控制失效,需谨慎使用。
.denyAll():拒绝所有访问- 核心作用:无论用户是否登录、具备何种权限,均拒绝访问匹配的 URL。
- 使用场景:完全禁止访问的资源(如废弃接口、敏感路径)。
- 示例:
// 禁止访问 /secret 路径(无论任何用户) .requestMatchers("/secret").denyAll() - 效果:访问时会直接抛出
AccessDeniedException,返回 403 Forbidden。
.authenticated():要求用户已登录- 核心作用:仅允许已认证用户(登录用户)访问,拒绝匿名用户。
- 与
permitAll()的区别:permitAll():允许匿名 + 已登录用户;authenticated():仅允许已登录用户。
- 典型用法:作为兜底规则,确保未明确配置的 URL 必须登录才能访问:
.anyRequest().authenticated() // 所有未匹配的路径均需登录
.anonymous():仅允许匿名用户访问- 核心作用:仅允许未登录用户(匿名用户)访问,已登录用户反而被拒绝。
- 使用场景:登录页、注册页、忘记密码页等(已登录用户无需访问这些页面)。
- 示例:
// 注册和忘记密码页面仅允许未登录用户访问 .requestMatchers("/register", "/forgot-password").anonymous() - 注意:匿名用户在 Spring Security 中被封装为
AnonymousAuthenticationToken,其权限默认包含ROLE_ANONYMOUS。
.fullyAuthenticated():要求非 Remember-Me 登录- 核心作用:仅允许通过「真实登录」(输入账号密码)的用户访问,拒绝通过 Remember-Me 功能自动登录的用户。
- 背景:Remember-Me 登录是一种「弱认证」(依赖 Cookie),适合访问非敏感资源;而修改密码、支付等敏感操作需强制用户重新输入密码(强认证)。
- 示例:
// 修改密码页面必须通过真实登录访问,拒绝 Remember-Me 自动登录 .requestMatchers("/security/change-password").fullyAuthenticated()
.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() | ADMIN 或 USER 访问 /user/** |
| 细粒度权限(无角色前缀) | .hasAuthority() | API_READ 权限访问 /api/** |
| 多权限任意一个 | .hasAnyAuthority() | API_READ 或 API_WRITE |
| 禁止所有访问 | .denyAll() | 废弃接口 /old/** |
| 敏感操作(需真实登录) | .fullyAuthenticated() | 修改密码 /change-password |
| 弱认证用户(Remember-Me) | .rememberMe() | 浏览历史 /history |
授权匹配顺序
在 Spring Security 的 Web 层授权配置中,规则的声明顺序是决定权限控制是否有效的关键。即使配置了正确的权限表达式,若顺序不当,也会导致权限失效或安全漏洞。
匹配顺序的底层原理:「-first match wins」(先匹配者生效)
Spring Security 对 URL 权限规则的匹配遵循「顺序遍历、首次命中即终止」的原则,底层流程如下:
- 当一个 HTTP 请求到达时,
AuthorizationFilter会按配置的requestMatchers顺序依次检查 URL 是否匹配; - 一旦找到第一个匹配的
requestMatchers规则,就立即应用该规则对应的权限要求(如hasRole、permitAll等); - 后续的
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角色即可访问,权限控制降级。
正确的配置顺序原则:「从具体到通用,兜底规则最后」
为避免顺序问题,需严格遵循以下配置顺序(优先级从高到低):
- 最精确的路径(如
/admin/user/delete); - 较具体的路径(如
/admin/**); - 通用路径(如
/user/**、/public/**); - 兜底规则(
.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 种:
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 匹配(简单、直观,覆盖大多场景)。
-
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 路径(而非部分匹配)。
MvcRequestMatcher:与 Spring MVC 路由一致的匹配- 核心特点:基于 Spring MVC 的
HandlerMapping匹配 URL,与@RequestMapping的路径匹配规则完全一致(支持路径变量、矩阵变量等 MVC 特性)。 - 解决的问题:传统
AntPathRequestMatcher可能与 Spring MVC 的路由规则不一致,例如:- Spring MVC 中
/user/{id}和/user/{id}/视为同一路径(自动忽略尾部斜杠); - 而
AntPathRequestMatcher会严格区分/user/1和/user/1/。
- Spring MVC 中
- 使用方式:需要通过
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 的项目,确保权限匹配规则与控制器路由完全一致。
- 核心特点:基于 Spring MVC 的
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") );
- 核心特点:需同时满足多个
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 | 覆盖多个路径或规则 |
注意事项
AntPathRequestMatcher的尾部斜杠问题:- 严格区分
/user和/user/(前者不包含斜杠,后者包含); - 若需忽略尾部斜杠,建议使用
MvcRequestMatcher(与 Spring MVC 行为一致)。
- 严格区分
- 正则表达式的性能问题:
RegexRequestMatcher的匹配效率低于AntPathRequestMatcher,复杂正则可能影响性能;- 非必要场景(如简单路径匹配)优先使用 Ant 风格。
- 请求方法的匹配:
- 所有匹配器默认匹配任意 HTTP 方法,若需限制方法(如仅允许 POST),需显式指定(如
AntPathRequestMatcher的第二个参数)。
- 所有匹配器默认匹配任意 HTTP 方法,若需限制方法(如仅允许 POST),需显式指定(如
- 组合匹配器的可读性:
- 复杂的
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 层授权默认注册了三个投票器,分别负责不同维度的校验,覆盖绝大多数常见场景:
- 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 登录 - 工作流程:
- 接收
ConfigAttribute(权限规则,如hasRole('ADMIN'))和用户认证信息Authentication; - 将 SpEL 表达式编译为可执行代码,结合当前请求上下文(
HttpServletRequest)评估结果; - 若表达式返回
true,投「同意票」;否则投「反对票」;若无法解析表达式,投「弃权票」。
- 接收
- 示例:当配置
.requestMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")时,WebExpressionVoter会同时校验用户角色和 IP 地址,两者都满足才投同意票。
- 核心作用:解析并评估 URL 权限规则中配置的 SpEL 表达式(如
- RoleVoter:角色权限专用投票器
- 核心作用:专门校验用户是否具备规则要求的「角色权限」(基于
ROLE_前缀的权限字符串)。 - 工作原理:
- 仅处理
ConfigAttribute中以ROLE_开头的权限规则(如ROLE_ADMIN); - 检查用户的权限集合(
Authentication.getAuthorities())中是否包含该角色; - 若包含,投「同意票」;否则投「反对票」;若规则不以
ROLE_开头,投「弃权票」。
- 仅处理
- 与 WebExpressionVoter 的关系:当使用
.hasRole("ADMIN")时,底层会被WebExpressionVoter解析为hasRole('ADMIN')表达式,而RoleVoter会同时检查ROLE_ADMIN角色 —— 两者都可能参与投票,但最终由决策管理器统一判断。 - 注意:
RoleVoter仅识别ROLE_前缀的权限,若你的角色权限没有该前缀(如ADMIN),需通过RoleVoter.setRolePrefix("")移除前缀限制,或改用WebExpressionVoter的hasAuthority表达式。
- 核心作用:专门校验用户是否具备规则要求的「角色权限」(基于
- AuthenticatedVoter:认证状态投票器
- 核心作用:根据用户的认证状态(匿名、已认证、Remember-Me 登录等)投票,专门处理与认证状态相关的规则(如
.authenticated()、.fullyAuthenticated())。 - 支持的特殊权限规则:
规则常量 对应配置方法 含义 IS_AUTHENTICATED_FULLY.fullyAuthenticated()仅允许真实登录用户(非 Remember-Me) IS_AUTHENTICATED_REMEMBERED.rememberMe()仅允许 Remember-Me 登录用户 IS_AUTHENTICATED_ANONYMOUSLY.anonymous()仅允许匿名用户 - 工作流程:
- 识别
ConfigAttribute中是否包含上述特殊规则; - 检查用户的认证状态(从
Authentication对象获取); - 若状态匹配规则,投「同意票」;否则投「反对票」;若规则不匹配,投「弃权票」。
- 识别
- 示例:当配置
.requestMatchers("/change-password").fullyAuthenticated()时,AuthenticatedVoter会检查用户是否通过真实登录(非 Remember-Me),是则投同意票,否则投反对票。
- 核心作用:根据用户的认证状态(匿名、已认证、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 投票器完成(对应第六模块的默认投票器),其核心流程如下:
- 当配置
.access("SpEL 表达式")时,该表达式会被封装为ConfigAttribute对象; - 授权流程触发时,
WebExpressionVoter会从SecurityContext中获取当前用户的Authentication对象,同时获取当前 HTTP 请求上下文(HttpServletRequest); - 将用户信息、请求信息注入 SpEL 表达式上下文,编译并执行表达式;
- 根据表达式执行结果投票:返回
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' |
注意:hasRole 与 hasAuthority 的区别在于前缀处理,前后端分离项目中建议优先使用 hasAuthority,避免前缀干扰。
SpEL 表达式的典型用法
结合前后端分离项目的实际需求,以下是 SpEL 表达式的高频应用场景:
- 多条件组合校验
需求:管理员角色(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() ); - 限制请求方法
需求:仅允许 POST 方法访问/api/user接口,且用户需具备API_WRITE权限。http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/user").access("hasAuthority('API_WRITE') and request.method == 'POST'").anyRequest().authenticated() ); - 基于用户属性的动态校验
需求:用户只能访问自己的订单(订单 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获取当前用户详情,通过字符串分割判断订单归属。 - 拒绝 Remember-Me 用户访问敏感接口
需求:修改密码接口仅允许真实登录用户访问,拒绝 Remember-Me 自动登录用户。http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/security/change-password").access("isFullyAuthenticated()").anyRequest().authenticated() ); - 结合请求头信息校验
需求:仅允许携带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 的表达式上下文。
- 实现自定义校验逻辑
// 自定义工具类:校验用户是否为指定部门 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());} } - 扩展 SpEL 表达式上下文
通过WebSecurityConfigurerAdapter或SecurityFilterChain配置,将自定义函数注册到 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 表达式中使用,实现基于部门的权限校验。
注意事项
- 表达式性能:复杂的 SpEL 表达式(如循环、大量字符串操作)可能影响接口响应速度,建议简化逻辑或缓存计算结果;
- 权限泄露风险:避免在表达式中硬编码敏感信息(如 IP 地址、密钥),建议通过配置文件注入;
- 用户详情类型转换:使用
authentication.principal时,需确保其类型为自定义用户详情类(如CustomUserDetails),避免类型转换异常; - 路径变量引用:引用路径变量时(如
#orderId),需确保requestMatchers中声明了对应的变量名,且与表达式中的变量名一致。
