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

基于请求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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 03:58:14