Spring+GraphQL端点可选授权及@PreAuthorize方法级权限问题
解决Spring Boot + GraphQL的「可选授权」问题
你的核心问题在于:当前的Security配置要么强制所有GraphQL请求必须携带JWT(导致公开的query/mutation无法访问),要么完全放行GraphQL端点(导致@PreAuthorize因为SecurityContext为空而失效)。要实现「端点开放但方法级权限可控」的目标,我们需要调整过滤器逻辑和Security配置,让GraphQL请求既能匿名通过,又能正确解析JWT并传递认证信息给方法级注解。
问题根源拆解
- 如果把GraphQL端点加入
permitAll,你的AuthorizationFilter不会处理该请求,导致携带JWT的请求也不会被解析,SecurityContext始终为空,@PreAuthorize自然无法生效。 - 如果不加入
permitAll,AuthorizationFilter会强制所有GraphQL请求必须带JWT,直接阻断了公开访问的需求。
解决方案步骤
1. 调整HttpSecurity配置,开放GraphQL端点但保留JWT解析
首先修改configure方法,把GraphQL端点(API_ROOT_URL)加入permitAll,同时让AuthorizationFilter处理所有可能携带JWT的请求(包括GraphQL和需要认证的REST端点):
@Override protected void configure(HttpSecurity http) throws Exception { log.debug("configureHttpSecurity"); http.csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 公开REST端点 + GraphQL端点允许匿名访问 .antMatchers(permitAllEndpointList.toArray(new String[0]), API_ROOT_URL) .permitAll() // 其他需要强制认证的REST端点(比如消息服务) .antMatchers(MESSAGING_ROOT_URL) .authenticated() .and() .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new AuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class) // 让AuthorizationFilter处理所有请求,尝试解析JWT .addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); }
2. 修改AuthorizationFilter,允许匿名请求通过并正确解析JWT
核心是把原来「无JWT就返回403」的逻辑改成「无JWT就放行(设置匿名上下文),有JWT就解析认证」:
@Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { try { String authorization = httpServletRequest.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Bearer ")) { // 解析JWT并返回已认证的Authentication对象 return getAuthentication(authorization.replace("Bearer ", "")); } } catch (ExecutionException e) { // JWT无效时才返回403 httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN,"The provided token was either not valid or is already expired!"); return null; } catch (IOException | InterruptedException e) { httpServletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"There was a problem verifying the supplied token!"); return null; } // 无JWT时返回匿名认证对象,让请求继续流转 return new AnonymousAuthenticationToken("graphql-filter", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); }
注意:
getAuthentication方法需要返回已认证的Authentication对象(比如UsernamePasswordAuthenticationToken,构造时传入true表示已认证),这样Spring Security才会把它存入SecurityContext。
3. 确保@PreAuthorize注解生效
首先在你的Spring Boot启动类或配置类上开启方法级安全:
@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { // ... 其他配置 }
然后在GraphQL Resolver方法上按需添加注解:
@Component public class BookResolver implements GraphQLQueryResolver, GraphQLMutationResolver { // 完全公开,匿名用户可访问 @PreAuthorize("permitAll()") public Book getBookById(Long id) { // 业务逻辑 } // 仅登录用户可访问 @PreAuthorize("isAuthenticated()") public List<Book> getMyBooks() { // 业务逻辑 } // 仅管理员可访问 @PreAuthorize("hasRole('ADMIN')") public Book createBook(BookInput input) { // 业务逻辑 } }
为什么这样可行?
- GraphQL端点被加入
permitAll,所以匿名请求能顺利到达Resolver层。 AuthorizationFilter会尝试解析所有请求的JWT:有有效JWT就设置SecurityContext为已认证用户,没有就设置为匿名用户。@PreAuthorize注解会根据SecurityContext中的认证状态,判断是否允许访问该方法,完美实现「可选授权」的需求。- 其他需要强制认证的REST端点(比如
MESSAGING_ROOT_URL),HttpSecurity的authenticated()规则会拦截匿名请求,符合原有安全要求。
内容的提问来源于stack exchange,提问作者puelo
相关产品推荐
相关产品推荐

