Spring Boot中Jersey整合Spring Security时方法安全注解失效求助
Spring Boot整合Jersey与Spring Security的问题排查与解决建议
问题现状
- 全局认证正常:
WebSecurityConfigurerAdapter中配置的全局认证规则可正常生效。 - 方法安全注解部分失效:注册
RolesAllowedDynamicFeature后,JSR-250规范的@DenyAll、@RolesAllowed注解可正常工作,但Spring Security的@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter注解完全被忽略。 - 已解决的Cookie问题:原Jersey编写的
/login端点无法返回XSRF-TOKEN Cookie(改用Spring MVC端点则正常),现已通过直接向HttpServletResponse添加ACCESS-TOKEN Cookie替代JAX-RS的Response设置解决。
环境说明
当前使用Spring Boot 2.3.10.RELEASE,暂无法升级版本。
相关配置与代码
application.yaml配置
spring: jersey: application-path: /resources servlet: load-on-startup: 1 type: filter
JerseyConfig配置类
@Configuration public class JerseyConfig { @Bean public ResourceConfig resourceConfig(ObjectMapper objectMapper) { return new ResourceConfig() .property(ServletProperties.FILTER_FORWARD_ON_404, true) .register((ContextResolver<ObjectMapper>) aClass -> objectMapper) .register(JacksonFeature.class) .register(HttpMethodOverrideFilter.class) .register(AuthResource.class) .register(UserResource.class); } }
SecurityConfigurationAdapter配置类
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { private final JwtTokenAuthFilter jwtTokenAuthFilter; private final JwtTokenAuthEntryPoint unauthorizedHandler; @Autowired public SecurityConfigurationAdapter(final JwtTokenAuthFilter jwtTokenAuthFilter, final JwtTokenAuthEntryPoint unauthorizedHandler) { this.jwtTokenAuthFilter = jwtTokenAuthFilter; this.unauthorizedHandler = unauthorizedHandler; } @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .configurationSource(corsConfig()).and() .csrf() .ignoringAntMatchers("/login", "/login2") .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler).and() .authorizeRequests() .antMatchers("/login", "/login2").permitAll() .anyRequest().authenticated().and() .addFilterBefore(jwtTokenAuthFilter, UsernamePasswordAuthenticationFilter.class) .headers() .xssProtection().and() .contentSecurityPolicy("script-src 'self';require-trusted-types-for 'script';object-src 'none';"); } private CorsConfigurationSource corsConfig() { return request -> { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(singletonList("http://localhost:9000")); config.setAllowedMethods(singletonList("*")); config.setAllowCredentials(true); config.setAllowedHeaders(singletonList("*")); config.setExposedHeaders(singletonList("Authorization")); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return config; }; } }
JwtTokenAuthFilter过滤器
@Priority(Priorities.AUTHENTICATION) public class JwtTokenAuthFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenUtils jwtTokenUtils; public JwtTokenAuthFilter(final UserDetailsService userDetailsService, final JwtTokenUtils jwtTokenUtils) { this.userDetailsService = userDetailsService; this.jwtTokenUtils = jwtTokenUtils; } @Override protected void doFilterInternal(final HttpServletRequest req, final HttpServletResponse resp, final FilterChain chain) throws ServletException, IOException { if (req.getCookies() == null) { chain.doFilter(req, resp); return; } String token = Arrays.stream(req.getCookies()) .filter(c -> "ACCESS-TOKEN".equals(c.getName())) .findFirst() .map(Cookie::getValue) .orElse(null); if (isEmpty(token) || !jwtTokenUtils.validateJwtToken(token)) { chain.doFilter(req, resp); return; } String username = jwtTokenUtils.getUserNameFromJwtToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(req, resp); } }
更新后的AuthResource登录端点
@Singleton @Path("") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class AuthResource { private final AuthenticationManager authenticationManager; private final JwtTokenUtils jwtTokenUtils; @Inject public AuthResource(AuthenticationManager authenticationManager, JwtTokenUtils jwtTokenUtils) { this.authenticationManager = authenticationManager; this.jwtTokenUtils = jwtTokenUtils; } @POST @Path("/login") public Response authenticateUser(@Context HttpServletResponse response, @RequestBody LoginRequest request) { try { Authentication authentication = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getEmail(), request.getPassword() ) ); UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal(); Cookie cookie = new Cookie("ACCESS-TOKEN", jwtTokenUtils.generateJwtToken(user)); cookie.setPath("/"); cookie.setSecure(true); cookie.setHttpOnly(true); cookie.setMaxAge(-1); response.addCookie(cookie); return Response.ok().build(); } catch (BadCredentialsException ex) { return Response.status(Response.Status.UNAUTHORIZED).build(); } } }
问题原因分析
方法安全注解失效核心原因
- Spring AOP代理未覆盖Jersey资源类:
@EnableGlobalMethodSecurity依赖Spring AOP实现方法拦截,但Jersey资源类默认由Jersey自身实例化管理,不在Spring容器的AOP代理范围内,导致Spring Security的Pre/Post注解无法被拦截处理。 - 拦截链路不重叠:Jersey的
RolesAllowedDynamicFeature是JAX-RS规范的实现,直接在Jersey的请求处理链中生效;而Spring的方法安全注解需要Spring的代理或拦截器介入,两者的执行链路完全独立。
解决方案与改进建议
针对@PreAuthorize等注解失效问题
方案1:将Jersey资源类交由Spring管理
- 在资源类上添加
@Component或@Service注解,让Spring负责实例化,确保Spring AOP能生成代理类。 - 修改JerseyConfig,注册Spring管理的Bean而非类:
@Configuration public class JerseyConfig { @Autowired private AuthResource authResource; @Autowired private UserResource userResource; @Bean public ResourceConfig resourceConfig(ObjectMapper objectMapper) { return new ResourceConfig() .property(ServletProperties.FILTER_FORWARD_ON_404, true) .register((ContextResolver<ObjectMapper>) aClass -> objectMapper) .register(JacksonFeature.class) .register(HttpMethodOverrideFilter.class) .register(authResource) // 注册Spring管理的Bean .register(userResource); } }
同时给AuthResource添加Spring组件注解:
@Singleton @Path("") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Component // 添加Spring组件注解 public class AuthResource { // ... 原有代码 }
方案2:自定义Jersey扩展处理Spring Security注解
编写自定义DynamicFeature,在Jersey请求链中手动调用Spring Security的权限校验逻辑,处理@PreAuthorize等注解:
@Provider @Priority(Priorities.AUTHORIZATION) public class SpringMethodSecurityFeature implements DynamicFeature { private final MethodSecurityExpressionHandler expressionHandler; public SpringMethodSecurityFeature(MethodSecurityExpressionHandler expressionHandler) { this.expressionHandler = expressionHandler; } @Override public void configure(ResourceInfo resourceInfo, FeatureContext context) { Method method = resourceInfo.getResourceMethod(); PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); if (preAuthorize != null) { context.register(new PreAuthorizeRequestFilter(preAuthorize.value(), expressionHandler)); } // 同理处理@PostAuthorize等注解 } private static class PreAuthorizeRequestFilter implements ContainerRequestFilter { private final String expression; private final MethodSecurityExpressionHandler expressionHandler; public PreAuthorizeRequestFilter(String expression, MethodSecurityExpressionHandler expressionHandler) { this.expression = expression; this.expressionHandler = expressionHandler; } @Override public void filter(ContainerRequestContext requestContext) throws IOException { SecurityContext securityContext = SecurityContextHolder.getContext(); if (securityContext.getAuthentication() == null) { throw new ForbiddenException("未认证"); } MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(securityContext.getAuthentication()); root.setPermissionEvaluator(expressionHandler.getPermissionEvaluator()); if (!(Boolean) expressionHandler.getExpressionParser().parseExpression(expression).getValue(root)) { throw new ForbiddenException("权限不足"); } } } }
然后在JerseyConfig中注册该Feature。
针对Cookie设置的优化
- 当前采用的
HttpServletResponse.addCookie()方式是合理的,因为Jersey的Response.cookie()在Jersey响应阶段处理,与Spring Security的CSRF Token处理链路独立,直接操作Servlet Response能确保Cookie被正确写入。 - 可将Cookie配置逻辑提取到工具类,避免重复代码。
内容的提问来源于stack exchange,提问作者ChambreNoire
相关产品推荐
相关产品推荐

