Spring Security上下文感知授权实现方案合理性咨询
关于方案有效性的答复
你构思的实现方式可以正常运行,但不属于Spring Security场景下的可靠最佳实践,存在3个明确隐患:
- 职责耦合:
checkAccess方法本身的定位是返回布尔值做访问准入判断,在里面混入SecurityContext修改逻辑,会导致权限校验和上下文构建两个逻辑强绑定,后续维护调整校验规则时,很容易出现上下文漏更新、脏数据残留的问题。 - 执行顺序不可控:URL级的
access()规则执行时机、方法级@PreAuthorize的执行时机和项目配置强相关,部分配置下方法级校验会先于URL授权逻辑执行,此时还未将资源域权限塞进SecurityContext,会直接返回403。 - 上下文清理风险:在
checkAccess里直接替换Authentication对象,没有配套的请求结束后还原逻辑,如果容器用线程池处理请求,线程复用时会把上一个请求的资源域权限带到下一个请求,出现越权漏洞。
Spring Security原生的简便实现方案
Spring Security没有直接提供“资源域权限自动注入”的开箱即用组件,但框架本身预留了标准扩展点,实现起来比修改checkAccess的方案更稳定,核心思路是把「资源准入校验」和「上下文权限填充」拆成两个独立环节,具体实现步骤如下:
- 第一步:保留原有
objAChecker/objBChecker的纯校验逻辑,只做用户是否有资源基础访问权的判断,不要在里面修改SecurityContext,保证方法职责单一。 - 第二步:新增一个继承
OncePerRequestFilter的自定义资源上下文过滤器,注册到FilterSecurityInterceptor之前的过滤器位置,核心逻辑参考:
其中public class ResourceScopeAuthFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final ObjAService objAService; private final ObjBService objBService; public ResourceScopeAuthFilter(ObjAService objAService, ObjBService objBService) { this.objAService = objAService; this.objBService = objBService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 先获取原始认证信息 Authentication originalAuth = SecurityContextHolder.getContext().getAuthentication(); String requestPath = request.getRequestURI(); Authentication scopedAuth = originalAuth; // 匹配objA路径 Map<String, String> objAVars = pathMatcher.extractUriTemplateVariables("/api/objA/{id}/**", requestPath); if (!objAVars.isEmpty() && originalAuth != null && !(originalAuth instanceof AnonymousAuthenticationToken)) { Long objAId = Long.valueOf(objAVars.get("id")); // 加载当前用户在该objA资源下的专属权限 Collection<GrantedAuthority> scopedAuthorities = objAService.getScopedAuthorities(originalAuth.getName(), objAId); // 构建带资源域权限的认证对象,保留原始认证的用户信息 scopedAuth = new ResourceScopedAuthentication(originalAuth, objAId, scopedAuthorities); } // 同理匹配objB路径,生成对应scopedAuth Map<String, String> objBVars = pathMatcher.extractUriTemplateVariables("/api/objB/{id}/**", requestPath); if (!objBVars.isEmpty() && originalAuth != null && !(originalAuth instanceof AnonymousAuthenticationToken)) { Long objBId = Long.valueOf(objBVars.get("id")); Collection<GrantedAuthority> scopedAuthorities = objBService.getScopedAuthorities(originalAuth.getName(), objBId); scopedAuth = new ResourceScopedAuthentication(originalAuth, objBId, scopedAuthorities); } try { // 替换成资源域认证信息 SecurityContextHolder.getContext().setAuthentication(scopedAuth); filterChain.doFilter(request, response); } finally { // 请求结束后强制还原原始认证信息,避免线程复用导致的上下文串扰 SecurityContextHolder.getContext().setAuthentication(originalAuth); } } }ResourceScopedAuthentication是自定义的Authentication实现类,仅需将getAuthorities()方法返回资源域专属权限集合,其余接口方法全部委托给原始Authentication对象即可。 - 第三步:把自定义过滤器注册到Spring Security过滤器链中,配置参考:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(new ResourceScopeAuthFilter(objAService, objBService), FilterSecurityInterceptor.class) .authorizeRequests() .antMatchers("/api/objA/{id}/**") .access("@objAChecker.checkAccess(authentication.principal, #id)") .antMatchers("/api/objB/{id}/**") .access("@objBChecker.checkAccess(authentication.principal, #id)"); }
完成上述配置后,你在Controller、Service层添加的@PreAuthorize("hasAuthority('read_data')")注解就会自动读取当前请求对应资源的专属权限做校验,不需要额外修改业务逻辑。如果使用Spring Security 6.x版本,还可以直接用新的AuthorizationManagerAPI替代原有SpEL配置,在授权决策时直接附加资源级权限元数据,适配成本更低。
内容的提问来源于stack exchange,提问作者Andrew Lalis
相关产品推荐
相关产品推荐

