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

Docker Compose环境下Keycloak令牌Issuer无效错误的通用解决方案咨询

解决Docker Compose中Keycloak Issuer URI前后端不匹配问题

问题核心

前端React从localhost:8080获取的JWT令牌,其iss(签发者)字段是http://localhost:8080/realms/mrtapp;但后端Spring Boot在Docker内部网络中,用keycloak:8080作为Issuer URI验证令牌,两者不匹配导致JWT验证失败,触发错误:

spring_1    | 2023-05-15T13:02:57.448Z DEBUG 1 --- [nio-8081-exec-2] o.s.s.oauth2.jwt.JwtClaimValidator       : The iss claim is not valid
spring_1    | 2023-05-15T13:02:57.449Z DEBUG 1 --- [nio-8081-exec-2] o.s.s.o.s.r.a.JwtAuthenticationProvider  : Failed to authenticate since the JWT was invalid

可行通用解决方案

方案1:统一Keycloak的Issuer URI(推荐,符合OAuth2最佳实践)

让Keycloak生成的令牌iss字段使用一个既可以被外部前端访问,又能被Docker内部后端解析的地址,从根源上解决匹配问题。

步骤:

  1. 配置Keycloak容器:在Docker Compose的Keycloak服务中添加环境变量,指定统一的主机名:
    services:
      keycloak:
        image: quay.io/keycloak/keycloak:21.1.1
        environment:
          KC_HOSTNAME: keycloak.local
          KC_HOSTNAME_URL: http://keycloak.local:8080
          KC_HOSTNAME_ADMIN_URL: http://keycloak.local:8080
          KC_DB: postgres
          # 其他数据库/管理员账号配置...
        ports:
          - "8080:8080"
    
  2. 前端配置修改:keycloak-js的url改为统一的主机名:
    const keycloak = new Keycloak({
        url: "http://keycloak.local:8080",
        realm: "mrtapp",
        clientId: "mrtwebapp"
      })
    
  3. 后端配置修改:Spring Boot的Issuer URI也改为这个统一主机名:
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak.local:8080/realms/mrtapp
    
  4. 域名解析配置:
    • 本地开发:在/etc/hosts(Linux/macOS)或C:\Windows\System32\drivers\etc\hosts(Windows)中添加 127.0.0.1 keycloak.local,让前端能解析到本地Keycloak服务。
    • 部署环境:在DNS服务器添加keycloak.local解析到Keycloak服务的公网IP;或者在Docker Compose中给Spring Boot服务添加extra_hosts,让后端容器能解析这个域名到Keycloak容器IP:
      spring-boot-app:
        # 其他配置...
        extra_hosts:
          - "keycloak.local:172.17.0.2" # 替换为Keycloak容器的实际内网IP,或者用服务名映射
      

方案2:让Spring Boot接受多个Issuer

如果不想修改Keycloak和前端的配置,可以自定义JWT验证逻辑,允许后端同时接受localhost:8080和keycloak:8080作为合法Issuer。

步骤:
在Spring Boot中自定义JWT验证器:

import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtValidators;

import java.util.Arrays;
import java.util.List;

@Bean
public OAuth2TokenValidator<Jwt> jwtIssuerValidator() {
    List<String> allowedIssuers = Arrays.asList(
            "http://localhost:8080/realms/mrtapp",
            "http://keycloak:8080/realms/mrtapp"
    );

    return jwt -> {
        String issuer = jwt.getIssuer();
        if (!allowedIssuers.contains(issuer)) {
            OAuth2Error error = new OAuth2Error("invalid_issuer", "Issuer not allowed: " + issuer, null);
            return OAuth2TokenValidatorResult.failure(error);
        }
        // 保留其他默认验证逻辑
        return JwtValidators.createDefault().validate(jwt);
    };
}

注意:此方法仅适合临时过渡,长期来看还是统一Issuer更规范,避免安全风险。

方案3:用反向代理统一服务入口

使用Traefik、Nginx等反向代理,让所有服务通过同一个域名的不同路径访问,消除内外网地址差异。

步骤示例(以Traefik为例):

  1. 在Docker Compose中添加Traefik服务,配置端口监听:
    traefik:
      image: traefik:v2.10
      command:
        - "--api.insecure=true"
        - "--providers.docker=true"
        - "--entrypoints.web.address=:80"
      ports:
        - "80:80"
        - "8080:8080" # Traefik控制台端口
      volumes:
        - "/var/run/docker.sock:/var/run/docker.sock"
    
  2. 给Keycloak、React、Spring Boot服务添加Traefik路由标签:
    keycloak:
      image: quay.io/keycloak/keycloak:21.1.1
      environment:
        KC_PATH: /auth
        KC_HOSTNAME: localhost
        # 其他配置...
      labels:
        - "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)"
        - "traefik.http.services.keycloak.loadbalancer.server.port=8080"
    
    react-app:
      # 其他配置...
      labels:
        - "traefik.http.routers.react.rule=PathPrefix(`/app`)"
        - "traefik.http.services.react.loadbalancer.server.port=3000"
    
    spring-boot-app:
      # 其他配置...
      labels:
        - "traefik.http.routers.spring.rule=PathPrefix(`/api`)"
        - "traefik.http.services.spring.loadbalancer.server.port=8081"
    
  3. 前端keycloak-js配置改为反向代理的Keycloak路径:
    const keycloak = new Keycloak({
        url: "http://localhost/auth",
        realm: "mrtapp",
        clientId: "mrtwebapp"
      })
    
  4. 后端Spring Boot的Issuer URI可以用Docker内部服务名访问Keycloak:
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak:8080/auth/realms/mrtapp
    

关于PKCE的疑问纠正

你担心手动修改令牌中的iss会破坏PKCE流程——这个认知是对的,而且更关键的是:JWT是经过签名的,任何字段修改都会导致签名验证失败,后端根本无法通过合法验证。所以手动修改令牌的方法完全不可行,直接放弃即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:22:58