基于Angular SSR与Keycloak实现BFF的技术咨询
Keycloak + Angular SSR 实现BFF架构方案指南
核心流程回顾
前端完成Keycloak认证后,将PKCE授权码和验证器传递给BFF(SSR后端),由BFF代表用户向Keycloak换取访问令牌,之后所有API请求都通过BFF转发,由BFF自动附加Bearer令牌。
1. 通过keycloak-angular获取PKCE授权码与验证器
前置配置
- Keycloak端:为浏览器端创建Public类型客户端,开启「Standard Flow Enabled」和「Proof Key for Code Exchange (PKCE) Enabled」;为BFF(SSR后端)创建Confidential类型客户端,开启「Standard Flow Enabled」并记录客户端密钥。
- Angular端:在
app.module.ts中配置KeycloakAngularModule时,开启PKCE模式:
KeycloakAngularModule.forRoot({ keycloakConfig: { url: 'http://your-keycloak/auth', realm: 'your-realm', clientId: 'browser-client-id', pkceMethod: 'S256' // 启用PKCE }, initOptions: { onLoad: 'login-required', redirectUri: 'http://your-ssr-app/callback' // 与Keycloak客户端配置一致 } })
获取PKCE参数
登录回调完成后,通过KeycloakService获取授权码(code)和验证器(codeVerifier):
import { KeycloakService } from 'keycloak-angular'; async getPkceParams() { const keycloakInstance = await this.keycloakService.getKeycloakInstance(); return { code: keycloakInstance.code, codeVerifier: keycloakInstance.codeVerifier, state: keycloakInstance.state // 用于CSRF验证 }; }
2. 安全传递PKCE参数到BFF
通过HTTPS POST请求将参数发送至BFF的专用接口,避免参数暴露在URL中:
import { HttpClient } from '@angular/common/http'; constructor(private http: HttpClient) {} async sendPkceToBff() { const params = await this.getPkceParams(); return this.http.post('/api/exchange-token', params); }
3. BFF换取并管理访问令牌
换取令牌
BFF接收参数后,调用Keycloak令牌端点换取访问令牌:
const axios = require('axios'); app.post('/api/exchange-token', async (req, res) => { const { code, codeVerifier, state } = req.body; // 验证state(防止CSRF攻击) if (state !== req.session.keycloakState) { // 需提前存储state到session return res.status(403).send('Invalid state'); } try { const tokenResponse = await axios.post( 'http://your-keycloak/auth/realms/your-realm/protocol/openid-connect/token', new URLSearchParams({ grant_type: 'authorization_code', client_id: 'bff-client-id', client_secret: 'bff-client-secret', // Confidential客户端密钥 code: code, code_verifier: codeVerifier, redirect_uri: 'http://your-ssr-app/callback' }) ); // 将访问令牌存入HttpOnly、Secure的Cookie(禁止前端读取) res.cookie('access_token', tokenResponse.data.access_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: tokenResponse.data.expires_in * 1000 }); // 可选:存储刷新令牌用于自动续期 res.cookie('refresh_token', tokenResponse.data.refresh_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' }); res.sendStatus(200); } catch (err) { res.status(err.response.status).send(err.response.data); } });
转发API请求
BFF拦截前端的API请求,从Cookie中取出令牌,附加到Authorization头后转发给后端API:
app.use('/api/proxy', (req, res) => { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).send('Unauthorized'); } const apiUrl = `http://your-api-service${req.url}`; axios({ method: req.method, url: apiUrl, headers: { ...req.headers, Authorization: `Bearer ${accessToken}` }, data: req.body }) .then(apiRes => { res.status(apiRes.status).send(apiRes.data); }) .catch(err => { res.status(err.response?.status || 500).send(err.response?.data || 'Server Error'); }); });
关键注意事项
- 所有通信必须使用HTTPS,防止令牌或PKCE参数被窃取
- BFF的客户端密钥需存储在环境变量中,禁止硬编码或暴露给前端
- 实现令牌自动刷新逻辑:BFF检测到令牌过期时,使用刷新令牌向Keycloak换取新令牌,无需前端介入
- Angular SSR的服务器端代码需单独配置Keycloak客户端(使用Confidential类型),避免与浏览器端配置混淆
内容的提问来源于stack exchange,提问作者ChrisJ
相关产品推荐
相关产品推荐

