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

Quarkus集成OIDC存AccessToken到Cookie后请求遇403问题

Quarkus OIDC Cookie认证403问题排查与解决

问题描述

在Quarkus中使用OIDC,通过后端API完成登录流程:登录接口重定向到Keycloak认证,回调接口获取授权码并交换为access token,将token存入Cookie。但前端携带该Cookie发起认证请求时,始终返回403 Forbidden错误。

相关代码与配置

登录API

@GET
@Path("/login")
public Response login(@jakarta.ws.rs.QueryParam(STATE) String state) {
    // 对于'web-app'类型,通常会重定向到Keycloak
    // 对于'service'类型,需要手动处理令牌交换
    // 本示例为简化初始登录流程,假设使用'web-app'类型
    String scope = URLEncoder.encode("openid profile email", StandardCharsets.UTF_8);
    String stateParam = (state != null && !state.isEmpty()) ? STATE_PARAM + state : "";

    String authUrl = authServerUrl + PROTOCOL_OPENID_CONNECT_AUTH +
            RESPONSE_TYPE_CODE_PARAM +
            CLIENT_ID_PARAM + clientId +
            REDIRECT_URI_PARAM + URLEncoder.encode(callbackUrl, StandardCharsets.UTF_8) + // 配置你的回调URI
            SCOPE_PARAM + scope +
            stateParam;

    log.info("重定向到Keycloak认证URL: {}", authUrl);

    return Response.seeOther(java.net.URI.create(authUrl)).build();
}

回调API

@GET
@Path("/callback")
public Response callback(@jakarta.ws.rs.QueryParam(CODE) String code, @jakarta.ws.rs.QueryParam(STATE) String state) {
    if (code == null || code.isEmpty()) {
        return Response.status(Response.Status.BAD_REQUEST).entity("缺少授权码.").build();
    }
    log.info("收到授权码: {}", code);
    log.info("收到state: {}", state);
    try {
        Form form = new Form()
                .param(GRANT_TYPE, AUTHORIZATION_CODE)
                .param(CODE, code)
                .param(REDIRECT_URI, callbackUrl)
                .param(CLIENT_ID, clientId)
                .param(CLIENT_SECRET, clientSecret); // 仅当客户端为机密类型时需要

        Response tokenResponse = ClientBuilder.newClient()
                .target(authServerUrl + PROTOCOL_OPENID_CONNECT_TOKEN)
                .request(MediaType.APPLICATION_FORM_URLENCODED)
                .post(Entity.form(form));
        log.info("令牌响应状态: " + tokenResponse);
        log.info("令牌响应状态码: {}", tokenResponse.getStatus());
        if (tokenResponse.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
            Map<String, Object> tokens = tokenResponse.readEntity(Map.class);
            String accessToken = (String) tokens.get(ACCESS_TOKEN);
            String idToken = (String) tokens.get(ID_TOKEN);
            String refreshToken = (String) tokens.get(REFRESH_TOKEN);

            // 重定向用户到存储的路径或默认仪表板
            String redirectUrl = (state != null ? state : "");
            return Response.seeOther(java.net.URI.create(redirectUrl))
                    .cookie(
                            new NewCookie("access_token", accessToken, "/", "localhost", null, 300, false,false)
                    ).build();
        } else {
            String error = tokenResponse.readEntity(String.class);
            log.error("令牌交换错误: {} - {}", tokenResponse.getStatus(), error);
            return Response.status(Response.Status.BAD_REQUEST).entity("令牌交换失败: " + error).build();
        }
    } catch (Exception e) {
        log.error("回调处理异常: {}", e.getMessage());
        return Response.serverError().entity("回调处理错误.").build();
    } finally {
        // 确保tokenResponse被关闭
    }
}

Application.yaml配置

mp:
 jwt:
  token:
   header: Cookie
   cookie: access_token
   issuer: http://localhost:8080/realms/blog-realm
 verify:
  publickey:
    location: http://localhost:8080/realms/blog-realm/protocol/openid-connect/certs
 roles-claim: realm_access/roles 

quarkus:
 http:
  port: 8081
  cors:
    enabled: true
    origins: /.*/
    access-control-allow-credentials: true

   oidc:
     auth-server-url: http://localhost:8080/realms/blog-realm 
     client-id: blog-auth 
    credentials:
      secret: G6xabvJp7G6YIAHez1HvBhICT0AzPK0L 
    application-type: service 
    authentication:
      scopes: openid, profile, email
    token:
      cookie: true
  rest-client:
    backend-api:
      url: http://localhost:8081/api 
    keycloak-admin-api:
      url: http://localhost:8080/auth/admin/realms/blog-realm 

JS请求代码

const fetchToken = async () => {
    try {
      const response = await fetch("http://localhost:8081/auth/token", {
        method: "GET",
        credentials: 'include', // 🔥 发送Cookie
        headers: {
          "Accept": "application/json",
        },
      });

      if (!response.ok) throw new Error("认证失败");

      setIsLogin(true);
    } catch (error) {
      console.error("认证失败", error);
      setIsLogin(false);
    }
   };

问题根源分析

  1. 配置冲突:同时启用mp.jwt和quarkus.oidc两个认证模块,令牌解析逻辑相互干扰。Quarkus OIDC本身支持从Cookie读取令牌,无需额外配置MP JWT。
  2. OIDC应用类型错误:quarkus.oidc.application-type设为service,该类型适用于服务间认证,不适用于处理前端Cookie的Web应用场景。
  3. Cookie属性不规范:未启用HttpOnly属性,且Secure属性关闭,既存在安全风险,也可能导致Quarkus OIDC无法正确识别Cookie。
  4. 令牌验证逻辑不一致:MP JWT与OIDC的issuer、公钥等参数可能不匹配,导致令牌验证失败。

解决方案

1. 移除MP JWT相关配置

删除mp.jwt下所有配置,让Quarkus OIDC全权处理认证:

# 移除以下配置块
# mp:
#  jwt:
#   token:
#    header: Cookie
#    cookie: access_token
#    issuer: http://localhost:8080/realms/blog-realm
#  verify:
#   publickey:
#     location: http://localhost:8080/realms/blog-realm/protocol/openid-connect/certs
#  roles-claim: realm_access/roles 

2. 调整Quarkus OIDC配置

将应用类型改为web-app,明确Cookie参数:

quarkus:
 oidc:
   auth-server-url: http://localhost:8080/realms/blog-realm 
   client-id: blog-auth 
   credentials:
     secret: G6xabvJp7G6YIAHez1HvBhICT0AzPK0L 
   application-type: web-app  # 改为web-app类型
   authentication:
     scopes: openid, profile, email
   token:
     cookie: true
     cookie-path: "/"  # 指定Cookie路径
     cookie-name: access_token  # 与回调中设置的Cookie名称一致
   roles:
     role-claim-path: realm_access/roles  # 配置角色声明路径

3. 修正回调中的Cookie设置

添加HttpOnly属性增强安全性,根据环境设置Secure:

return Response.seeOther(java.net.URI.create(redirectUrl))
        .cookie(
                new NewCookie(
                        "access_token", 
                        accessToken, 
                        "/", 
                        "localhost", 
                        null, 
                        300, 
                        true,  // 启用HttpOnly
                        false  // 开发环境关闭Secure,生产环境建议开启
                )
        ).build();

4. 确保受保护API使用正确注解

在需要认证的API上添加Quarkus OIDC支持的注解:

@GET
@Path("/token")
@Authenticated  // 要求用户已认证
public Response getTokenInfo() {
    // 业务逻辑
}

5. 验证令牌有效性

通过Keycloak端点验证获取的access token是否有效:

curl -X POST http://localhost:8080/realms/blog-realm/protocol/openid-connect/token/introspect \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=blog-auth" \
  -d "client_secret=G6xabvJp7G6YIAHez1HvBhICT0AzPK0L" \
  -d "token=YOUR_ACCESS_TOKEN"

验证步骤

  1. 重启Quarkus应用
  2. 完成登录流程,确认Cookie已正确设置
  3. 前端调用受保护API,检查返回状态是否为200 OK

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:15:56