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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 02:23:16