You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    }
}

问题原因分析

方法安全注解失效核心原因

  1. Spring AOP代理未覆盖Jersey资源类:@EnableGlobalMethodSecurity依赖Spring AOP实现方法拦截,但Jersey资源类默认由Jersey自身实例化管理,不在Spring容器的AOP代理范围内,导致Spring Security的Pre/Post注解无法被拦截处理。
  2. 拦截链路不重叠: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 03:31:01