Spring 如何判断 URL 是否设置 security="none"
结论
不建议在 HandlerInterceptor.preHandle() 中判断某个 URL 是否配置了 security="none"。Spring MVC 拦截器和 Spring Security 过滤器链属于两套机制,拦截器没有稳定的公开 API,无法反向查询当前请求匹配了哪条安全配置。
也不要用 enabled、accountNonExpired 等账户状态字段表示“是否已经付款”。这些字段描述账户能否完成认证,付款状态则是业务授权条件。建议这样配置:
- 允许公开访问登录页、静态资源和错误页。
- payment page 只要求用户已登录。
- 其他付费功能要求用户拥有
SERVICE_PAID权限,或通过自定义AuthorizationManager实时检查付款状态。 - 已登录但未付款时,由
AccessDeniedHandler将用户重定向到 payment page。
为什么不应在拦截器里判断
security="none" 表示请求完全绕过对应的 Spring Security 过滤器链。它不是附加在 HttpServletRequest 上的属性,因此无法通过下面这种方式可靠判断:
request.getUserPrincipal() != null
这个判断只能说明当前请求是否存在认证主体,不能说明 URL 是否受到保护。例如:
- 已登录用户访问公开 URL 时,仍然可能带有
Principal。 - 未登录用户访问受保护 URL 时,可能使用匿名认证。
permitAllURL 仍会经过 Spring Security,只是不要求权限。security="none"URL 完全绕过该过滤器链。
如果在 MVC 拦截器中另行维护一套 URL 排除规则,安全配置和拦截器配置很容易逐渐失去同步。以后新增登录页、错误页或静态资源路径时,还可能引发重定向循环。
推荐方案:把付款状态作为授权条件
下面的示例采用 SecurityFilterChain 和 authorizeHttpRequests 配置 Spring Security。不同 Spring Security 版本的接口细节可能稍有不同,但授权模型一致。
如果付款状态只需在用户下次登录时更新,可以在认证成功后为用户添加 SERVICE_PAID 权限:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
"/login",
"/error",
"/css/**",
"/js/**",
"/images/**"
).permitAll()
.requestMatchers("/payment/**").authenticated()
.anyRequest().hasAuthority("SERVICE_PAID")
);
http.exceptionHandling(exceptions -> exceptions
.accessDeniedHandler(paymentAccessDeniedHandler())
);
return http.build();
}
匹配规则有先后顺序,所以 /payment/** 必须写在要求 SERVICE_PAID 的规则前面,否则未付款用户连付款页面也无法访问。
拒绝处理器可以这样写:
@Bean
AccessDeniedHandler paymentAccessDeniedHandler() {
return (request, response, exception) -> {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
boolean loggedIn =
authentication != null
&& authentication.isAuthenticated()
&& !(authentication instanceof AnonymousAuthenticationToken);
boolean paid = loggedIn
&& authentication.getAuthorities().stream()
.anyMatch(authority ->
"SERVICE_PAID".equals(authority.getAuthority()));
if (loggedIn && !paid) {
response.sendRedirect(
request.getContextPath() + "/payment"
);
return;
}
response.sendError(HttpServletResponse.SC_FORBIDDEN);
};
}
完成这些配置后:
- 未登录用户由 Spring Security 引导进入登录流程。
- 已登录但未付款的用户访问付费功能时,会被重定向到
/payment。 /payment本身只要求登录,不要求用户已经付款。- 已付款但缺少其他业务权限的用户仍会收到
403 Forbidden,不会被错误地重定向。
付款状态需要实时生效时
如果用户支付成功后必须立即获得访问权限,只在登录时写入 Authentication 会造成状态过期。当前会话中的权限不会自动更新,除非用户重新认证或系统主动刷新 SecurityContext。
这种情况下,可以使用自定义 AuthorizationManager 查询付款服务:
@Component
public class PaymentAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
private final PaymentService paymentService;
public PaymentAuthorizationManager(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext context) {
Authentication authentication = authenticationSupplier.get();
boolean loggedIn =
authentication != null
&& authentication.isAuthenticated()
&& !(authentication instanceof AnonymousAuthenticationToken);
if (!loggedIn) {
return new AuthorizationDecision(false);
}
boolean paid =
paymentService.hasPaid(authentication.getName());
return new AuthorizationDecision(paid);
}
}
然后将它应用到需要付款的 URL:
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
PaymentAuthorizationManager paymentAuthorizationManager)
throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
"/login",
"/error",
"/css/**",
"/js/**",
"/images/**"
).permitAll()
.requestMatchers("/payment/**").authenticated()
.requestMatchers("/service/**")
.access(paymentAuthorizationManager)
.anyRequest().authenticated()
);
http.exceptionHandling(exceptions -> exceptions
.accessDeniedHandler(paymentAccessDeniedHandler())
);
return http.build();
}
如果 hasPaid() 每次都直接查询数据库,需要评估实际请求量。通常可以使用短期缓存,并在支付成功后主动清除或更新缓存。
是否可以把标志放进 Principal
可以在自定义 UserDetails 或 Principal 中添加 servicePaid 字段,但不能把它当作 enabled、accountNonExpired 等认证状态。
未付款用户仍然应该能够:
- 正常登录;
- 查看付款页面;
- 发起支付;
- 查询支付结果;
- 退出登录。
如果把未付款映射为 enabled=false,用户可能无法登录,自然也无法进入付款流程。
自定义字段可以作为授权判断的数据来源,不过更符合 Spring Security 使用方式的做法,是将它转换为权限:
authorities.add(
new SimpleGrantedAuthority("SERVICE_PAID")
);
这类权限通常在登录时生成。如果付款状态会在会话期间发生变化,就需要刷新当前 Authentication,或者改用实时查询的 AuthorizationManager。
关于 security="none" 与 permitAll
旧式 XML 配置中的:
<http pattern="/resources/**" security="none"/>
表示该路径不经过 Spring Security。现代配置通常用 WebSecurityCustomizer 的 ignoring() 表达类似含义,但除了纯静态资源外,不宜大量使用。
登录页、错误页和公开业务接口通常更适合配置为:
.requestMatchers("/login", "/error").permitAll()
permitAll 允许任何人访问,但请求仍会经过 Spring Security,所以安全响应头、SecurityContext 和其他过滤器行为仍能正常生效。
这个需求不必判断 URL 是否配置了 security="none"。直接在安全配置中定义三种访问级别即可:公开访问、登录后访问和付款后访问。付款检查、例外页面以及拒绝后的跳转,都由同一套授权规则管理。
备注:内容仅供参考。
