深入解析:# 指南:从零开始为 GKE 应用启用 Google 账号登录 (IAP)

指南:从零开始为 GKE 应用启用 Google 账号登录 (IAP)

本文档将非常详细地描述我们如何从零开始,为部署在 GKE 上的 Web 应用(一个前端 chat-ui 和一个后端 chat-api-svc)启用通过 Google 账号登录的 IAP(Identity-Aware Proxy)保护。

1. 最终架构

我们的目标是实现以下请求流程:当用户访问前端并调用后端 API 时,Google 的负载均衡器会拦截请求,强制用户通过 Google 账号认证,然后将认证信息安全地传递给我们的后端服务。

User's BrowserGoogle Cloud Load Balancer (GKE Gateway)Google IAPFrontend Pod (chat-ui)Backend Pod (chat-api-svc)访问前端 UI请求受 IAP 保护的前端服务重定向到 Google 登录页面登录成功后返回alt[用户未登录]注入认证 Header 并转发请求返回前端静态文件 (HTML/JS/CSS)返回前端静态文件渲染前端页面前端 App 加载并执行 JSJS 发起 API 请求 (fetch('/chat-api-svc/api/v1/me'))拦截发往后端的请求验证用户之前的 IAP session验证通过,注入 X-Goog-* Header 并转发请求后端 /me 接口读取 Header,返回 JSON返回 JSON 响应浏览器收到用户信息前端 UI 在左下角显示用户 EmailUser's BrowserGoogle Cloud Load Balancer (GKE Gateway)Google IAPFrontend Pod (chat-ui)Backend Pod (chat-api-svc)

2. 前提条件

  • 一个正在运行的 GKE 集群。
  • 集群中已安装并配置好 GKE Gateway Controller。
  • 您的前端 (chat-ui) 和后端 (chat-api-svc) 应用已通过 Deployment 部署到集群。
  • 已有一个 GatewayHTTPRoute 资源,将公网流量路由到您的服务。

3. 操作步骤

第 1 步:在 Google Cloud Console 中创建 OAuth 凭证

IAP 需要一个 OAuth 2.0 客户端 ID 和密钥来识别您的应用并展示统一的登录界面。

  1. 配置 OAuth 同意屏幕

    • 导航到 Google Cloud Console -> “API 和服务” -> “OAuth 同意屏幕”。
    • 选择 “内部” 用户类型(如果您组织内的所有用户都可以访问)或“外部”(如果您想允许任何 Google 用户)。
    • 填写应用名称(例如 “Chat App”)、用户支持电子邮件等信息。
    • 保存并继续。在范围页面,您不需要添加任何范围,直接点击“保存并继续”。
      在这里插入图片描述在这里插入图片描述
  2. 创建 OAuth 2.0 客户端 ID

    • 导航到 “API 和服务” -> “凭据”。

    • 点击 “+ 创建凭据” -> “OAuth 客户端 ID”

    • 在这里插入图片描述

    • 在“应用类型”中选择 “Web 应用”

    • 为它命名,例如 “chat-ui-iap-client”。

    • 关键步骤:添加已获授权的重定向 URI

      • 点击 “+ 添加 URI”。
      • 您需要添加一个特殊的 URI,IAP 会用它来处理登录回调。这个 URI 的格式是:https://iap.googleapis.com/v1/oauth2/clientServiceAccounts/[PROJECT_NUMBER]:handleRedirect
      • 您可以在 Cloud Console -> “IAM 和管理” -> “设置” 中找到您的 项目编号 (Project Number)
      • 最终的 URI 类似于:https://iap.googleapis.com/v1/oauth2/clientServiceAccounts/912156613264:handleRedirect

在这里插入图片描述

*   点击“创建”。弹出的窗口会显示您的 **客户端 ID** 和 **客户端密钥**。请**务必**将这两个值复制下来,下一步会用到。
理解“已获授权的重定向 URI”

问:这个 URI 的作用是什么?为什么需要手动配置?

答:简单来说,配置这个 URI 就是一个授权步骤。它明确地告诉 Google 的认证系统:“我允许我刚刚创建的这个客户端 ID (Client ID) 被我当前 GCP 项目中的 IAP 服务使用。”

这是一个核心的安全机制,确保只有您明确授权的应用和服务才能使用您的 OAuth 凭证。Google 要求手动配置此项,是为了让您作为项目所有者进行一次明确的安全确认。


第 2 步:将 OAuth 凭证存储为 Kubernetes Secret

为了让 GKE 中的 IAP 配置能安全地使用这些凭证,我们需要将它们存储为 Kubernetes Secret。

  1. 创建 iap-secret.yaml 文件
    k8s/ 目录下创建一个名为 iap-secret.yaml 的文件。

  2. 填充 Secret 内容
    将您上一步获取的客户端 ID 和密钥填入以下模板。注意: 值需要经过 Base64 编码。
    您可以使用命令 echo -n 'YOUR_CLIENT_ID' | base64echo -n 'YOUR_CLIENT_SECRET' | base64 来生成编码后的字符串。

    # k8s/iap-secret.yaml
    apiVersion: v1
    kind: Secret
    metadata:
    name: iap-oauth-credentials
    type: Opaque
    data:
    client_id: "YOUR_CLIENT_ID_BASE64"
    client_secret: "YOUR_CLIENT_SECRET_BASE64"
  3. 应用 Secret
    执行 kubectl apply -f k8s/iap-secret.yaml 将这个 Secret 创建在您的集群中。

第 3 步:为后端 API 创建专用的 IAP Service 和策略

为了将 IAP 仅应用于后端 API,同时保持内部服务间的通信不受影响,最佳实践是创建一个专用的 Service 来接收 IAP 流量。

  1. 创建新的 Service (service-chat-api-iap.yaml)
    这个 Service 会和现有的 clusterip-chat-api-svc 指向相同的 Pods(通过 selector: app: chat-api-svc),但它有自己独立的名字。

    # k8s/service-chat-api-iap.yaml
    # This service is a dedicated entrypoint for IAP traffic to the chat-api backend.
    apiVersion: v1
    kind: Service
    metadata:
    name: clusterip-chat-api-svc-iap
    spec:
    type: ClusterIP
    selector:
    app: chat-api-svc  # 确保这与后端 Pod 的标签匹配
    ports:
    - protocol: TCP
    port: 8000
    targetPort: 8000
  2. 创建 GCPBackendPolicy (gcp-backend-policy-chat-api-svc.yaml)
    这个策略会将 IAP 规则附加到我们刚刚创建的新 Service 上。

    # k8s/gcp-backend-policy-chat-api-svc.yaml
    # This policy applies IAP to the dedicated backend IAP service.
    apiVersion: networking.gke.io/v1
    kind: GCPBackendPolicy
    metadata:
    name: iap-policy-chat-api-svc
    spec:
    default:
    iap:
    enabled: true
    oauth2ClientSecret:
    name: iap-oauth-credentials # 引用我们创建的 K8s Secret
    clientID: "912156613264-eaphf3lhji9jbp402e2qac050oiq1cdm.apps.googleusercontent.com" # 填入您的客户端 ID
    targetRef:
    group: ""
    kind: Service
    name: clusterip-chat-api-svc-iap # 将 IAP 策略指向新的 Service

第 3 步:为前端和后端服务启用 IAP

为了实现完整的端到端认证,我们不仅需要保护后端 API,通常也需要保护前端应用本身。这意味着当用户第一次访问网站时,就需要登录。

3.1 为前端服务启用 IAP

项目中已经存在一个为前端启用 IAP 的策略。它的原理和我们即将为后端创建的策略完全相同:

  • 目标服务: chat-ui-iap-service (一个专门用于 IAP 流量的前端 Service)。
  • 策略文件: k8s/gcp-backend-policy.yaml
# k8s/gcp-backend-policy.yaml
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
name: iap-backend-policy
spec:
default:
iap:
enabled: true
oauth2ClientSecret:
name: iap-oauth-credentials
clientID: "912156613264-eaphf3lhji9jbp402e2qac050oiq1cdm.apps.googleusercontent.com"
targetRef:
group: ""
kind: Service
name: chat-ui-iap-service # 策略指向前端的 IAP Service

当您部署 k8s/ 目录下的所有文件时,这个策略会被应用,从而保护前端的访问。

当做完上面步骤, 我们再次打开前端后会直接到google的登录窗口

在这里插入图片描述

3.2 在 IAP 中授权用户访问权限 (关键)

仅仅启用 IAP 是不够的,您还必须明确指定哪些用户或用户组有权访问您的应用。否则,所有用户(包括您自己)都会看到 “You don’t have access” (403 Forbidden) 的错误。

  1. 导航到 IAP 页面

    • 在 Google Cloud Console 中,前往 “安全” -> “Identity-Aware Proxy”。
      在这里插入图片描述
  2. 找到您的后端服务

    • 在列表中,找到与您的 GKE 负载均衡器关联的后端服务。您应该能看到对应 chat-ui-iap-serviceclusterip-chat-api-svc-iap 的条目。
  3. 添加主账号

    • 选中您需要授权的后端服务(例如 chat-ui-iap-service)。
    • 在右侧打开的信息面板中,点击 “添加主账号”
  4. 分配角色

    • 新的主账号: 输入您希望授予访问权限的 Google 账号(例如 your-email@gmail.com)、Google 群组 (my-group@example.com),或者 allAuthenticatedUsers (允许所有登录了 Google 的用户)。
    • 选择角色: 在下拉菜单中,找到并选择 Cloud IAP -> IAP-secured Web App User

在这里插入图片描述

  1. 保存
    • 点击“保存”。对所有需要保护的服务(包括前端和后端)重复此操作。

现在,只有被授权的用户才能在通过 Google 登录后访问您的前端应用。

3.3 为后端 API 创建专用的 IAP Service 和策略

为了将 IAP 仅应用于后端 API,同时保持内部服务间的通信不受影响,最佳实践是创建一个专用的 Service 来接收 IAP 流量。

  1. 创建新的 Service (service-chat-api-iap.yaml)
    这个 Service 会和现有的 clusterip-chat-api-svc 指向相同的 Pods(通过 selector: app: chat-api-svc),但它有自己独立的名字。

    # k8s/service-chat-api-iap.yaml
    # This service is a dedicated entrypoint for IAP traffic to the chat-api backend.
    apiVersion: v1
    kind: Service
    metadata:
    name: clusterip-chat-api-svc-iap
    spec:
    type: ClusterIP
    selector:
    app: chat-api-svc  # 确保这与后端 Pod 的标签匹配
    ports:
    - protocol: TCP
    port: 8000
    targetPort: 8000
  2. 创建 GCPBackendPolicy (gcp-backend-policy-chat-api-svc.yaml)
    这个策略会将 IAP 规则附加到我们刚刚创建的新 Service 上。

    # k8s/gcp-backend-policy-chat-api-svc.yaml
    # This policy applies IAP to the dedicated backend IAP service.
    apiVersion: networking.gke.io/v1
    kind: GCPBackendPolicy
    metadata:
    name: iap-policy-chat-api-svc
    spec:
    default:
    iap:
    enabled: true
    oauth2ClientSecret:
    name: iap-oauth-credentials # 引用我们创建的 K8s Secret
    clientID: "912156613264-eaphf3lhji9jbp402e2qac050oiq1cdm.apps.googleusercontent.com" # 填入您的客户端 ID
    targetRef:
    group: ""
    kind: Service
    name: clusterip-chat-api-svc-iap # 将 IAP 策略指向新的 Service
3.4 在 IAP 中授权用户访问权限 for 后端, 同3.2


现在,只有被授权的用户才能在通过 Google 登录后访问您的后端应用。


第 4 步:更新网关路由 (HTTPRoute)

现在,我们需要告诉 GKE Gateway,将发往 /chat-api-svc 的外部流量路由到受 IAP 保护的新 Service 上。

  1. 修改 HTTPRoute 配置
    我们基于线上的 py-api-route 配置,只修改 /chat-api-svc 规则的 backendRefs 部分。

    变更前:

    - backendRefs:
    - name: clusterip-chat-api-svc
    port: 8000

    变更后:

    - backendRefs:
    - name: clusterip-chat-api-svc-iap # 指向新的 IAP Service
    port: 8000
  2. 应用变更
    由于直接 apply 可能会遇到 resourceVersion 冲突,我们使用 replace 命令来确保更新成功。

    kubectl replace -f k8s/httproute-iap-fix.yaml

第 5 步:后端代码修改 (/me 接口)

后端需要一个接口来读取 IAP 注入的 Header 并返回给前端。

  1. 修改 src/routers/user_router.py
    我们添加了一个新的 /me 接口。

    from fastapi import APIRouter, Depends, HTTPException, Request
    from src.schemas.user import IAPUser # 需要先定义 IAPUser schema
    # ... (router 定义) ...
    @router.get("/me", response_model=IAPUser)
    async def get_current_user_from_iap(request: Request):
    """
    从 IAP 头中获取当前认证用户的信息。
    """
    email = request.headers.get("X-Goog-Authenticated-User-Email")
    user_id = request.headers.get("X-Goog-Authenticated-User-Id")
    # IAP email header 的格式通常是 "accounts.google.com:user@example.com"
    if email and ":" in email:
    email = email.split(":")[-1]
    return {"email": email, "user_id": user_id}

第 6 步:前端代码修改 (获取并显示用户信息)

最后,我们需要让前端应用调用 /me 接口,并在 UI 上展示出来。

  1. 更新 API 服务 (src/services/api.ts)
    添加一个新函数来调用 /me 接口,并为本地开发提供模拟数据。

    export interface UserInfo {
    email: string | null;
    user_id: string | null;
    }
    export async function fetchUserInfo(): Promise<UserInfo> {
      const ME_API_URL = '/chat-api-svc/api/v1/me';
      try {
      const response = await fetch(ME_API_URL);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const userInfo: UserInfo = await response.json();
      // 为本地开发提供模拟数据
      if (!userInfo.email && import.meta.env.DEV) {
      return { email: 'local-dev@example.com', user_id: 'dev-user-123' };
      }
      return userInfo;
      } catch (error) {
      console.error('Error fetching user info:', error);
      if (import.meta.env.DEV) {
      return { email: 'local-dev-error@example.com', user_id: 'dev-user-error-123' };
      }
      return { email: null, user_id: null };
      }
      }
  2. 更新侧边栏组件 (src/components/conversation-list.ts)
    我们修改这个组件,让它自己获取并显示用户信息。

    // ... imports ...
    import { fetchUserInfo, type UserInfo } from '../services/api';
    @customElement('conversation-list')
    export class ConversationList extends LitElement {
    // ...
    @state()
    private _userInfo: UserInfo | null = null;
    async connectedCallback() {
    super.connectedCallback();
    // 组件加载时调用 API
    this._userInfo = await fetchUserInfo();
    // 派发事件,通知父组件用户已加载
    if (this._userInfo?.user_id) {
    this.dispatchEvent(new CustomEvent('user-loaded', {
    detail: { user: this._userInfo },
    bubbles: true,
    composed: true
    }));
    }
    }
    // ...
    render() {
    return html`
    <!-- ... 其他模板 ... -->
    <sl-divider></sl-divider>
      <div class="profile-section">
        ${this._userInfo?.email
        ? html`
      <div class="user-avatar">${this._userInfo.email.charAt(0)}</div>
        <div class="profile-info">
        <span class="profile-email">${this._userInfo.email}</span>
        </div>
        `
      : html`<sl-spinner></sl-spinner><span>Loading user...</span>`}
      </div>
      <!-- ... -->
        `;
        }
        }
  3. 更新主应用组件 (src/chat-app.ts)
    最后,修改主应用,移除硬编码的用户,并监听 user-loaded 事件来动态加载对话。

    @customElement('chat-app')
    export class ChatApp extends LitElement {
    // ...
    // 移除硬编码的 _currentUser
    @state()
    private _currentUser: UserInfo | null = null;
    // 移除 firstUpdated
    // 添加事件处理器
    private _handleUserLoaded(e: CustomEvent) {
    console.log('User loaded:', e.detail.user);
    this._currentUser = e.detail.user;
    this._loadConversations(); // 在获取到用户信息后再加载对话
    }
    // ...
    render() {
    const conversationList = html`
    <conversation-list
    .conversations=${this.conversations...}
    .selectedConversationId=${this._selectedConversationId}
    @conversation-selected=${this._handleConversationSelected}
    @conversation-created=${this._handleConversationCreated}
    @user-loaded=${this._handleUserLoaded}
    ></conversation-list>
    `;
    // ...
    }
    }

效果
在这里插入图片描述

4. 结论

通过以上步骤,我们成功地为 GKE 上的应用配置了 IAP,实现了无需修改应用核心代码的安全登录。整个流程涉及了云平台配置、Kubernetes 资源编排、后端接口实现和前端 UI 展示,是一个完整的端到端实践。

posted @ 2026-01-21 14:18  gccbuaa  阅读(1)  评论(0)    收藏  举报