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 必须保持一致。
Cookie 的 SameSite 和 Secure
如果只有端口不同,且主机名都是 localhost,请求虽然跨 origin,但通常仍属于同站请求。此时主要问题是 Angular 没有设置 withCredentials。
如果前端和后端使用不同域名,浏览器可能将 Cookie 视为跨站 Cookie。此时通常需要:
server:
servlet:
session:
cookie:
same-site: none
secure: true
SameSite=None 通常要求使用 HTTPS,不能直接照搬到普通 HTTP 开发环境。对本地开发来说,webpack 代理一般比调整跨站 Cookie 更可靠。
建议的排查顺序
-
确认登录回调请求是否成功到达:
/login/oauth2/code/oidc -
检查回调响应是否包含:
Set-Cookie: JSESSIONID=...或:
Set-Cookie: SESSION=... -
检查随后的
/api/account请求是否包含对应的Cookie请求头。 -
检查 CORS 响应是否包含:
Access-Control-Allow-Origin: http://localhost:9000 Access-Control-Allow-Credentials: true -
查看
302响应的Location。如果它再次指向/oauth2/authorization/oidc,说明后端没有识别到会话。 -
检查 Keycloak 请求中的
redirect_uri是否与客户端配置完全一致,包括协议、主机名、端口和路径。
在这个场景中,应优先采用“相对 SERVER_API_URL + webpack 代理”。如果必须跨源直连,则需要同时满足“精确 CORS + withCredentials: true + 正确的 Keycloak 回调地址”。
备注:内容仅供参考。
