Quarkus集成OIDC存AccessToken到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); } };
问题根源分析
- 配置冲突:同时启用
mp.jwt和quarkus.oidc两个认证模块,令牌解析逻辑相互干扰。Quarkus OIDC本身支持从Cookie读取令牌,无需额外配置MP JWT。 - OIDC应用类型错误:
quarkus.oidc.application-type设为service,该类型适用于服务间认证,不适用于处理前端Cookie的Web应用场景。 - Cookie属性不规范:未启用
HttpOnly属性,且Secure属性关闭,既存在安全风险,也可能导致Quarkus OIDC无法正确识别Cookie。 - 令牌验证逻辑不一致: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"
验证步骤
- 重启Quarkus应用
- 完成登录流程,确认Cookie已正确设置
- 前端调用受保护API,检查返回状态是否为200 OK
内容的提问来源于stack exchange,提问作者Nalin Kumar
相关产品推荐
相关产品推荐

