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

Spring Boot过滤器链外验证Keycloak JWT:GraphQL订阅鉴权实现

问题描述

我有一个基于Spring Boot + Angular的Web应用,后端采用Keycloak(JWT)实现认证。需要创建带鉴权的Apollo GraphQL订阅,使用WebSocket,但无法在初始HTTP请求中添加授权头。WebSocket鉴权有两种方式:1)将JWT令牌放入请求URI;2)将令牌添加到初始化消息中。我倾向于第二种方式,避免令牌被存入日志。

前端请求代码:

const auth = this.injector.get(AuthenticationService);
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:9090/web-service/graphql_ws',
  connectionParams: {
      Authorization: 'Bearer ' + auth.getLoginDataFromStorage().access_token
  }
}))

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: ApolloLink.from([
    middleware,
    errorLink,
    split(
        // split based on operation type
      ({ query }) => {
        const def = getMainDefinition(query)
        return def.kind === 'OperationDefinition' && def.operation === 'subscription'
      },
      wsLink,
      httpLink
    )
  ]),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'no-cache'
    },
    query: {
      fetchPolicy: 'no-cache'
    }
  },
});

已创建WebSocketGraphQlInterceptor获取初始化消息中的令牌,但不知如何完成会话认证:

@Configuration
public class SubscriptionInterceptor implements WebSocketGraphQlInterceptor
{
    @Override
    public Mono<Object> handleConnectionInitialization(
            WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload)
    {
        var authToken = connectionInitPayload.get("Authorization").toString();
        return Mono.just(connectionInitPayload);
    }

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain)
    {
        List<String> token = request.getHeaders().getOrEmpty("Authorization");
        return chain.next(request)
                .contextWrite(context -> context.put("Authorization", token.isEmpty() ? "" : token.get(0)));
    }
}

安全配置代码:

@Configuration
@EnableWebSecurity
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

   @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
 
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
          .csrf().disable()
          .authorizeRequests()
          .antMatchers("/api/**").authenticated()
          .anyRequest().permitAll();
    }
}

请问如何在拦截器中使用JWT令牌完成用户认证?


解决方案

1. 注入Keycloak令牌验证依赖

确保项目已引入Keycloak starter依赖,在拦截器中注入KeycloakAuthenticationProvider和KeycloakDeployment,用于解析、验证JWT令牌。

2. 修改拦截器实现认证逻辑

在handleConnectionInitialization方法中解析初始化消息里的JWT令牌,验证有效性后生成Authentication对象并存入WebSocket会话属性;后续请求拦截时,从会话属性取出认证信息并设置到Spring Security上下文。

修改后的拦截器代码:

@Configuration
public class SubscriptionInterceptor implements WebSocketGraphQlInterceptor {

    private final KeycloakAuthenticationProvider keycloakAuthenticationProvider;
    private final KeycloakDeployment keycloakDeployment;

    // 构造注入依赖
    public SubscriptionInterceptor(KeycloakAuthenticationProvider keycloakAuthenticationProvider,
                                  KeycloakDeployment keycloakDeployment) {
        this.keycloakAuthenticationProvider = keycloakAuthenticationProvider;
        this.keycloakDeployment = keycloakDeployment;
    }

    @Override
    public Mono<Object> handleConnectionInitialization(WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload) {
        // 从初始化参数中获取Authorization令牌
        String authHeader = (String) connectionInitPayload.get("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            // 令牌无效,返回错误终止连接
            return Mono.error(new AuthenticationCredentialsNotFoundException("Invalid or missing authorization token"));
        }

        String jwtToken = authHeader.substring(7); // 去掉"Bearer "前缀
        try {
            // 验证并解析JWT令牌
            AccessToken token = AdapterTokenVerifier.verifyToken(jwtToken, keycloakDeployment);
            // 生成Keycloak认证对象
            KeycloakAuthenticationToken authentication = new KeycloakAuthenticationToken(token, false);
            // 将认证对象存入WebSocket会话属性,供后续请求使用
            sessionInfo.getAttributes().put("AUTHENTICATION", authentication);
            return Mono.just(connectionInitPayload);
        } catch (VerificationException | OAuthErrorException e) {
            return Mono.error(new AuthenticationServiceException("Invalid JWT token", e));
        }
    }

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        // 从WebSocket会话属性中取出认证对象
        KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) request.getSessionAttributes().get("AUTHENTICATION");
        if (authentication != null) {
            // 将认证信息设置到SecurityContext,供Spring Security鉴权使用
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        return chain.next(request)
                .doFinally(signalType -> SecurityContextHolder.clearContext()); // 请求结束后清理上下文
    }
}

3. 调整安全配置,控制WebSocket端点权限

在KeycloakSecurityConfig的configure(HttpSecurity http)方法中,添加WebSocket GraphQL端点的认证要求:

@Override
protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http
      .csrf().disable()
      .authorizeRequests()
      .antMatchers("/api/**").authenticated()
      .antMatchers("/web-service/graphql_ws").authenticated() // 添加WebSocket端点的认证控制
      .anyRequest().permitAll();
}

4. 响应式场景下的上下文传递(可选)

若项目使用Spring WebFlux,可将认证信息存入Reactor Context,更贴合响应式编程规范:

@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
    KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) request.getSessionAttributes().get("AUTHENTICATION");
    if (authentication != null) {
        return chain.next(request)
                .contextWrite(SecurityContextHolder.withAuthentication(authentication));
    }
    return chain.next(request);
}

内容的提问来源于stack exchange,提问作者Achaad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 18:30:49