基于请求URL动态配置JWT issuer-uri的技术实现问询
动态根据请求URL匹配JWT Issuer的优化方案
需求
根据请求URL动态设置jwt.issuer-uri:
- 请求
http://hostname/realm1/request时,对应Issuer地址为http://keycloak-host/auth/realms/realm1 - 请求
https://hostname/realm2/request时,对应Issuer地址为http://keycloak-host/auth/realms/realm2
当前实现
通过自定义SecurityFilterChain和MyJwtDecoder循环遍历Realm列表验证JWT,代码如下:
SecurityFilterChain 配置
@Bean @Order(100) public SecurityFilterChain myJwtSecFilterChain (HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> { auth.requestMatchers("/api/**").authenticated(); }); http.oauth2ResourceServer(oauth -> { oauth.jwt(jwt -> { jwt.decoder(new MyJwtDecoder()); }); }); return http.build(); }
自定义JwtDecoder实现
public class MyJwtDecoder implements JwtDecoder { private static final Logger LOGGER = LoggerFactory.getLogger(MyJwtDecoder.class); public MyJwtDecoder() { } @Override public Jwt decode(String token) throws JwtException { LOGGER.debug("*********** MyJwtDecoder - decode token {}", token); return getJwt(token); } private Jwt getJwt(String token) { Jwt result = null; List<String> realmList = List.of("realm1", "realm2"); for (String realm : realmList) { JwtDecoder jdLocal = JwtDecoders.fromIssuerLocation("http://keycloak-host/auth/realms/" + realm); try { result = jdLocal.decode(token); LOGGER.info("TOKEN VALIDATED !!!!!"); break; } catch (JwtValidationException vex) { LOGGER.info("Jwt validation exception - Stop Validation - Exception type {} - Message: {}", vex.getClass().getSimpleName(), vex.getMessage()); throw vex; } catch (Exception ex) { // Continue with validation LOGGER.info("Jwt validation exception - Continue with other realms validation - Exception type {} - Message: {}", ex.getClass().getSimpleName(), ex.getMessage()); } } return result; } }
问题
如何修改解码器以直接利用请求URL获取Realm,替代循环验证逻辑?或通过在BearerTokenAuthenticationFilter前添加Filter实现时,如何绑定解码器?请勿提供已弃用适配器的解决方案。
解决方案
方案一:修改自定义JwtDecoder,从请求上下文提取Realm
利用Spring的RequestContextHolder获取当前请求,解析URL中的Realm名称,直接匹配对应Issuer的解码器,避免循环验证。
修改后的MyJwtDecoder代码
public class MyJwtDecoder implements JwtDecoder { private static final Logger LOGGER = LoggerFactory.getLogger(MyJwtDecoder.class); private static final String KEYCLOAK_ISSUER_PREFIX = "http://keycloak-host/auth/realms/"; // 可根据实际URL格式调整正则表达式 private static final Pattern REALM_PATTERN = Pattern.compile("/(realm1|realm2)/"); @Override public Jwt decode(String token) throws JwtException { LOGGER.debug("*********** MyJwtDecoder - decode token {}", token); // 获取当前HTTP请求上下文 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new JwtValidationException("无法获取请求上下文"); } HttpServletRequest request = attributes.getRequest(); String requestUri = request.getRequestURI(); // 从URL中提取Realm名称 Matcher matcher = REALM_PATTERN.matcher(requestUri); if (!matcher.find()) { throw new JwtValidationException("请求URL中未包含合法Realm"); } String realm = matcher.group(1); // 构建对应Realm的解码器并验证Token JwtDecoder decoder = JwtDecoders.fromIssuerLocation(KEYCLOAK_ISSUER_PREFIX + realm); try { Jwt jwt = decoder.decode(token); LOGGER.info("TOKEN VALIDATED FOR REALM: {}", realm); return jwt; } catch (JwtValidationException vex) { LOGGER.info("Jwt验证失败 - Realm: {}, 异常类型: {}, 消息: {}", realm, vex.getClass().getSimpleName(), vex.getMessage()); throw vex; } catch (Exception ex) { LOGGER.info("Jwt处理异常 - Realm: {}, 异常类型: {}, 消息: {}", realm, ex.getClass().getSimpleName(), ex.getMessage()); throw new JwtException("Token验证失败", ex); } } }
注意事项
- 正则表达式需根据实际业务的URL结构调整,确保能精准匹配Realm
- Spring Web环境下
RequestContextHolder默认已启用,无需额外配置 - 可添加Realm白名单校验,防止非法Realm请求
方案二:前置Filter提取Realm,动态绑定JwtDecoder
在BearerTokenAuthenticationFilter之前添加自定义Filter,提前从URL提取Realm并构建对应解码器,存入请求属性,后续认证流程直接使用该解码器。
步骤1:自定义Realm解析Filter
@Component public class RealmJwtDecoderFilter extends OncePerRequestFilter { private static final String KEYCLOAK_ISSUER_PREFIX = "http://keycloak-host/auth/realms/"; private static final String REQUEST_JWT_DECODER_ATTR = "REQUEST_JWT_DECODER"; private static final Pattern REALM_PATTERN = Pattern.compile("/(realm1|realm2)/"); // 缓存解码器,避免重复创建 private final ConcurrentHashMap<String, JwtDecoder> decoderCache = new ConcurrentHashMap<>(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestUri = request.getRequestURI(); Matcher matcher = REALM_PATTERN.matcher(requestUri); if (!matcher.find()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求URL中未包含合法Realm"); return; } String realm = matcher.group(1); // 从缓存获取或创建解码器 JwtDecoder decoder = decoderCache.computeIfAbsent(realm, key -> JwtDecoders.fromIssuerLocation(KEYCLOAK_ISSUER_PREFIX + key)); request.setAttribute(REQUEST_JWT_DECODER_ATTR, decoder); filterChain.doFilter(request, response); } }
步骤2:修改SecurityFilterChain配置
@Bean @Order(100) public SecurityFilterChain myJwtSecFilterChain(HttpSecurity http, RealmJwtDecoderFilter realmJwtDecoderFilter) throws Exception { // 将自定义Filter添加到BearerTokenAuthenticationFilter之前 http.addFilterBefore(realmJwtDecoderFilter, BearerTokenAuthenticationFilter.class); http.authorizeHttpRequests(auth -> { auth.requestMatchers("/api/**").authenticated(); }); http.oauth2ResourceServer(oauth -> { oauth.jwt(jwt -> { // 从请求属性中获取预先设置的解码器 jwt.decoder(request -> { JwtDecoder decoder = (JwtDecoder) request.getAttribute("REQUEST_JWT_DECODER"); if (decoder == null) { throw new JwtValidationException("未找到对应Realm的JWT解码器"); } return decoder; }); }); }); return http.build(); }
优化点
- 使用
ConcurrentHashMap缓存解码器,减少重复创建开销 - Filter中提前拦截非法Realm请求,避免后续无效认证流程
内容的提问来源于stack exchange,提问作者saavedrah
相关产品推荐
相关产品推荐

