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
相关产品推荐
相关产品推荐

