PAYATHON 2026

JHipster 前后端分离后 OAuth2 登录失败

数据小许

结论

问题不在 AccountResource、Keycloak 用户或 PostgreSQL,而在于 OAuth2 登录创建的服务端会话没有随跨源 /api/account 请求一起发送。

JHipster 6.2.0 的 OAuth2 登录流程依赖 Spring Security 的会话 Cookie。前端从 http://localhost:9000 请求 http://localhost:8080 属于跨源请求,而 Angular HttpClient 默认不会在跨源请求中携带 Cookie。后端会将请求视为未登录,返回 302 并重定向到 OAuth2 登录入口,因此 getAccount() 不会执行。

开发环境中,较稳妥的做法是使用 webpack dev-server 反向代理,让浏览器始终访问 localhost:9000,不直接跨源访问 localhost:8080。如果必须直连后端,则需要同时正确配置:

  • Angular 请求的 withCredentials: true
  • 后端精确的 CORS origin
  • allow-credentials: true
  • Keycloak 的 redirect URI 和 Web Origins
  • OAuth2 登录成功后的前端跳转地址

为什么会出现两个 302

安全配置要求 /api/account 必须经过认证:

.antMatchers("/api/**").authenticated()

请求未携带有效的 JSESSIONID 或 SESSION Cookie 时,Spring Security 会将其识别为匿名请求。由于配置中启用了:

.oauth2Login()

未认证请求可能被重定向到 OAuth2 authorization endpoint。浏览器因此收到 302,请求也不会进入:

public UserDTO getAccount(Principal principal)

题目中的 curl 命令同样没有携带 Cookie,所以收到重定向是正常的。Referer 不能证明用户已经登录。

可以在浏览器中查看 /api/account 的 Request Headers。如果其中没有 Cookie: JSESSIONID=... 或 Cookie: SESSION=...,基本可以确认是会话 Cookie 未随请求发送。

推荐方案:开发环境使用 webpack 代理

不要让浏览器直接跨源访问后端。将 API URL 保持为相对地址:

SERVER_API_URL: `''`

然后通过 webpack dev-server,将后端相关路径代理到 http://localhost:8080。具体配置文件名可能因生成模板而异,常见写法如下:

devServer: {
  proxy: [
    {
      context: [
        '/api',
        '/management',
        '/oauth2',
        '/login',
        '/logout'
      ],
      target: 'http://localhost:8080',
      secure: false,
      changeOrigin: false
    }
  ]
}

需要代理的路径不只有 /api。OAuth2 登录入口和回调通常还会用到:

/oauth2/authorization/oidc
/login/oauth2/code/oidc
/logout

启用代理后,浏览器访问的地址都以:

http://localhost:9000

为源,例如:

http://localhost:9000/api/account
http://localhost:9000/oauth2/authorization/oidc
http://localhost:9000/login/oauth2/code/oidc

webpack 会将这些请求转发到 8080。这样一来,Cookie、CSRF Cookie 和登录回调都位于同一个浏览器 origin 下,可以避开大部分跨源会话问题。

Keycloak 客户端需要允许代理后的回调地址,例如:

Valid Redirect URIs:
http://localhost:9000/login/oauth2/code/oidc

Web Origins:
http://localhost:9000

如果 Spring Boot 根据转发头生成回调地址,还要确保代理正确传递 Host、X-Forwarded-Host、X-Forwarded-Port 和 X-Forwarded-Proto。可以在浏览器 Network 面板或 Keycloak 登录请求中直接检查实际生成的 redirect_uri。

备选方案:前端直接访问 8080

如果继续使用:

SERVER_API_URL: `'http://localhost:8080/'`

Angular 的跨源 API 请求就必须携带凭据。可以在负责重写 API URL 的 interceptor 中设置 withCredentials:

@Injectable()
export class ApiRequestInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const apiRequest = request.clone({
      url: `${SERVER_API_URL}${request.url.replace(/^\//, '')}`,
      withCredentials: true
    });

    return next.handle(apiRequest);
  }
}

如果其他 interceptor 已经完成 URL 处理,只需补充:

request = request.clone({
  withCredentials: true
});

不要给所有第三方 URL 无条件添加凭据,最好只处理后端 API:

if (request.url.startsWith(SERVER_API_URL)) {
  request = request.clone({
    withCredentials: true
  });
}

设置 SERVER_API_URL 只会改变请求地址,并不会让 Angular 自动携带跨源 Cookie。

正确配置 JHipster CORS

允许凭据时,origin 不能使用通配符:

allowed-origins: '*'
allow-credentials: true

这组配置不符合浏览器对 CORS 凭据请求的要求。服务器必须返回具体的 Access-Control-Allow-Origin,不能返回 *。

在 JHipster 6.2.0 的常见配置结构中,CORS 配置位于 jhipster.cors 下:

jhipster:
  cors:
    allowed-origins:
      - http://localhost:9000
    allowed-methods: "*"
    allowed-headers: "*"
    exposed-headers: Authorization,Link,X-Total-Count
    allow-credentials: true
    max-age: 1800

这里应填写发起浏览器请求的 origin:

http://localhost:9000

后端虽然监听 8080,但 http://localhost:8080 是请求目标,不是请求来源,因此无须因为端口是 8080 就将它加入允许列表。Keycloak 地址通常也不用加入后端 /api/** 的 CORS 列表,因为 OAuth2 页面跳转不是针对后端 API 的跨源 XHR。

同时要检查 YAML 缩进,并确认配置位于当前启用的 profile 中。例如使用 dev profile 时,这段配置应由 application-dev.yml 加载。

Keycloak 客户端配置

Keycloak 中的 web_app 客户端至少要允许实际使用的回调地址。前端直连后端时,通常是:

Valid Redirect URIs:
http://localhost:8080/login/oauth2/code/oidc

如果登录完成后需要返回 Angular 页面,还要确保应用最终重定向到:

http://localhost:9000/

前端 origin 可以配置为:

Web Origins:
http://localhost:9000

Web Origins、Valid Redirect URIs 和后端 CORS 分别控制不同的环节:

  • Valid Redirect URIs 控制 Keycloak 可以将授权响应回调到哪些地址。
  • Web Origins 控制 Keycloak 允许哪些浏览器 origin 进行相关跨源交互。
  • jhipster.cors 控制浏览器能否从 9000 调用后端 8080。
  • withCredentials 决定 Angular 是否会把后端会话 Cookie 发送给 8080。

主机名必须保持一致

不要混用以下地址:

localhost
127.0.0.1
192.168.0.159

Cookie 按域名管理。即便这些地址指向同一台机器,浏览器仍会将它们视为不同站点。

可以统一使用:

Angular:  http://localhost:9000
Backend:  http://localhost:8080
Keycloak: http://localhost:9080

也可以全部使用同一个局域网 IP。浏览器访问地址、Keycloak redirect URI 和 Spring Security 生成的 callback URI 必须保持一致。

如果只有端口不同,且主机名都是 localhost,请求虽然跨 origin,但通常仍属于同站请求。此时主要问题是 Angular 没有设置 withCredentials。

如果前端和后端使用不同域名,浏览器可能将 Cookie 视为跨站 Cookie。此时通常需要:

server:
  servlet:
    session:
      cookie:
        same-site: none
        secure: true

SameSite=None 通常要求使用 HTTPS,不能直接照搬到普通 HTTP 开发环境。对本地开发来说,webpack 代理一般比调整跨站 Cookie 更可靠。

建议的排查顺序

  1. 确认登录回调请求是否成功到达:

    /login/oauth2/code/oidc
    
  2. 检查回调响应是否包含:

    Set-Cookie: JSESSIONID=...
    

    或:

    Set-Cookie: SESSION=...
    
  3. 检查随后的 /api/account 请求是否包含对应的 Cookie 请求头。

  4. 检查 CORS 响应是否包含:

    Access-Control-Allow-Origin: http://localhost:9000
    Access-Control-Allow-Credentials: true
    
  5. 查看 302 响应的 Location。如果它再次指向 /oauth2/authorization/oidc,说明后端没有识别到会话。

  6. 检查 Keycloak 请求中的 redirect_uri 是否与客户端配置完全一致,包括协议、主机名、端口和路径。

在这个场景中,应优先采用“相对 SERVER_API_URL + webpack 代理”。如果必须跨源直连,则需要同时满足“精确 CORS + withCredentials: true + 正确的 Keycloak 回调地址”。

备注:内容仅供参考。