From 3e87ee856715f2908f38ef544cdbf9ad73c658ea Mon Sep 17 00:00:00 2001 From: pqcqaq <905739777@qq.com> Date: Sun, 26 Oct 2025 11:03:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0oauth=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backups/oauth.md | 1620 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1620 insertions(+) create mode 100644 backups/oauth.md diff --git a/backups/oauth.md b/backups/oauth.md new file mode 100644 index 0000000..2d30a3d --- /dev/null +++ b/backups/oauth.md @@ -0,0 +1,1620 @@ + +## 下一步优化方案 + +### 1. 实现 PKCE 完整支持 + +#### 1.1 什么是 PKCE? + +**PKCE**(Proof Key for Code Exchange,RFC 7636)是 OAuth 2.0 的安全扩展,主要用于**公开客户端**(如移动应用、单页应用)防止授权码拦截攻击。 + +#### 1.2 为什么需要 PKCE? + +**传统授权码流程的安全隐患**: + +```mermaid +sequenceDiagram + participant App as 移动应用 + participant Browser as 系统浏览器 + participant Attacker as 攻击者 + participant AS as 授权服务器 + + App->>Browser: 1. 打开授权页面 + Browser->>AS: 2. 用户授权 + AS->>Browser: 3. 重定向 + 授权码 + Browser->>Attacker: 4. 恶意应用拦截回调 + Attacker->>AS: 5. 使用授权码换取令牌 + Note over Attacker: 攻击成功! +``` + +**PKCE 解决方案**: + +```mermaid +sequenceDiagram + participant App as 移动应用 + participant AS as 授权服务器 + participant Attacker as 攻击者 + + Note over App: 生成 code_verifier + App->>App: code_verifier = random(43-128) + App->>App: code_challenge = SHA256(code_verifier) + + App->>AS: 1. 授权请求
(code_challenge, method=S256) + AS->>App: 2. 返回授权码 + + alt 正常流程 + App->>AS: 3. 换取令牌
(code, code_verifier) + AS->>AS: 验证: SHA256(code_verifier) == code_challenge + AS->>App: 4. 返回令牌 + else 攻击者拦截 + Attacker->>AS: 3. 换取令牌
(code, 无code_verifier) + AS->>Attacker: 4. 拒绝请求 + Note over Attacker: 攻击失败! + end +``` + +#### 1.3 实施步骤 + +**步骤 1:更新数据库结构** + +实体 `OauthAuthorizationCode` 已经预留了字段: + +```typescript +// src/entities/OauthAuthorizationCode.ts +export interface Schema extends EntityShape { + // ...现有字段... + + // PKCE 扩展 (RFC 7636) - 已预留 + codeChallenge?: String<128>; + codeChallengeMethod?: String<10>; // "plain" or "S256" +} +``` + +**步骤 2:修改授权端点** + +```typescript +// src/aspects/oauth.ts - authorize() + +export async function authorize(params: { + response_type: string; + client_id: string; + redirect_uri: string; + scope?: string; + state?: string; + action: 'grant' | 'deny'; + + // 新增 PKCE 参数 + code_challenge?: string; + code_challenge_method?: 'plain' | 'S256'; +}, context: BRC) { + const { + response_type, + client_id, + redirect_uri, + scope, + state, + action, + code_challenge, + code_challenge_method + } = params; + + // ... 现有验证逻辑 ... + + if (action === 'grant') { + const code = randomUUID(); + const codeId = await generateNewIdAsync(); + + // 存储授权码(包含 PKCE 信息) + await context.operate("oauthAuthorizationCode", { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: codeId, + code, + redirectUri: redirect_uri, + oauthAppId: oauthApp.id, + applicationId: context.getApplicationId()!, + userId: context.getCurrentUserId()!, + scope: scope === undefined ? [] : [scope], + expiresAt: Date.now() + 10 * 60 * 1000, + + // 存储 PKCE 参数 + codeChallenge: code_challenge, + codeChallengeMethod: code_challenge_method || 'plain', + } + }, {}); + + // ... 后续逻辑 ... + } +} +``` + +**步骤 3:修改令牌端点验证** + +```typescript +// src/endpoints/oauth.ts - oauthTokenEndpoint + +const oauthTokenEndpoint: Endpoint> = { + name: "获取OAuth Token", + params: [], + method: 'post', + type: "free", + fn: async (contextBuilder, params, header, req, body) => { + const context = await contextBuilder(); + const { + client_id, + client_secret, + grant_type, + code, + redirect_uri, + code_verifier // 新增:PKCE 验证器 + } = body as { + client_id: string, + client_secret: string, + grant_type: string, + code?: string, + redirect_uri?: string, + code_verifier?: string + }; + + // ... 现有验证逻辑 ... + + // 找code的记录 + const [authCodeRecord] = await context.select("oauthAuthorizationCode", { + data: { + id: 1, + code: 1, + redirectUri: 1, + userId: 1, + expiresAt: 1, + usedAt: 1, + codeChallenge: 1, + codeChallengeMethod: 1, + }, + filter: { code } + }, {}); + + if (!authCodeRecord) { + await context.commit(); + return { + statusCode: 400, + data: { + error: "invalid_grant", + error_description: "Invalid authorization code", + success: false + } + }; + } + + // PKCE 验证 + if (authCodeRecord.codeChallenge) { + if (!code_verifier) { + await context.commit(); + return { + statusCode: 400, + data: { + error: "invalid_request", + error_description: "code_verifier is required", + success: false + } + }; + } + + // 验证 code_verifier + const isValid = await verifyPKCE( + code_verifier, + authCodeRecord.codeChallenge as string, + authCodeRecord.codeChallengeMethod as 'plain' | 'S256' + ); + + if (!isValid) { + await context.commit(); + return { + statusCode: 400, + data: { + error: "invalid_grant", + error_description: "Invalid code_verifier", + success: false + } + }; + } + } + + // ... 后续令牌颁发逻辑 ... + } +}; + +// PKCE 验证函数 +async function verifyPKCE( + verifier: string, + challenge: string, + method: 'plain' | 'S256' +): Promise { + if (method === 'plain') { + return verifier === challenge; + } else if (method === 'S256') { + const crypto = require('crypto'); + const hash = crypto.createHash('sha256').update(verifier).digest(); + const computed = hash.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return computed === challenge; + } + return false; +} +``` + +**步骤 4:更新应用配置** + +```typescript +// src/entities/OauthApplication.ts + +export interface Schema extends EntityShape { + // ... 现有字段 ... + + // 新增:是否强制使用 PKCE + requirePKCE?: Boolean; +} + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + // ... + attr: { + // ... + requirePKCE: '强制使用 PKCE', + }, + }, + }, +}; +``` + +**步骤 5:客户端集成示例** + +```javascript +// 移动应用或 SPA 客户端 +import crypto from 'crypto'; + +// 1. 生成 code_verifier 和 code_challenge +function generatePKCE() { + // 生成随机字符串 (43-128 字符) + const verifier = crypto.randomBytes(32).toString('base64url'); + + // 计算 challenge + const challenge = crypto + .createHash('sha256') + .update(verifier) + .digest('base64url'); + + return { verifier, challenge }; +} + +// 2. 发起授权请求 +const pkce = generatePKCE(); +sessionStorage.setItem('code_verifier', pkce.verifier); + +const authUrl = new URL('https://your-oak-system.com/authorize'); +authUrl.searchParams.set('response_type', 'code'); +authUrl.searchParams.set('client_id', 'your_client_id'); +authUrl.searchParams.set('redirect_uri', 'your://app/callback'); +authUrl.searchParams.set('code_challenge', pkce.challenge); +authUrl.searchParams.set('code_challenge_method', 'S256'); +authUrl.searchParams.set('state', generateState()); + +window.location.href = authUrl.toString(); + +// 3. 处理回调并换取令牌 +const code = getCodeFromCallback(); +const verifier = sessionStorage.getItem('code_verifier'); + +const tokenResponse = await fetch('https://your-oak-system.com/oauth/access_token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + grant_type: 'authorization_code', + code: code, + client_id: 'your_client_id', + redirect_uri: 'your://app/callback', + code_verifier: verifier // 提供验证器 + }) +}); +``` + +#### 1.4 测试清单 + +- [ ] 公开客户端使用 PKCE 成功获取令牌 +- [ ] 保密客户端可选使用 PKCE +- [ ] `code_verifier` 不匹配时拒绝请求 +- [ ] 支持 `plain` 和 `S256` 两种方法 +- [ ] `requirePKCE=true` 时强制验证 + +--- + +### 2. 添加更多内置提供商 + +#### 2.1 提供商支持矩阵 + +| 提供商 | 状态 | 优先级 | 用户信息端点 | 特殊说明 | +|-------|------|-------|------------|---------| +| Oak | ✅ 已实现 | - | `/oauth/userinfo` | 自建系统 | +| Gitea | ✅ 已实现 | - | `/api/v1/user` | 开源 Git 服务 | +| GitHub | 🔲 待实现 | ⭐⭐⭐ | `/user` | 最常用 | +| GitLab | 🔲 待实现 | ⭐⭐⭐ | `/api/v4/user` | 开源替代 | +| Google | 🔲 待实现 | ⭐⭐⭐ | `/oauth2/v2/userinfo` | 通用登录 | +| Microsoft | 🔲 待实现 | ⭐⭐ | `/v1.0/me` | 企业场景 | +| 微信开放平台 | 🔲 待实现 | ⭐⭐ | `/sns/userinfo` | 移动应用 | +| 企业微信 | 🔲 待实现 | ⭐⭐ | `/cgi-bin/user/getuserinfo` | 企业内部 | +| 钉钉 | 🔲 待实现 | ⭐⭐ | `/user/getuserinfo` | 企业场景 | +| 飞书 | 🔲 待实现 | ⭐ | `/open-apis/authen/v1/user_info` | 企业协作 | +| Apple | 🔲 待实现 | ⭐ | N/A (ID Token) | iOS 应用 | + +#### 2.2 实现步骤 + +**步骤 1:创建处理器文件** + +```typescript +// src/utils/oauth/handler/github.ts +import { UserInfoHandler } from '../index'; + +export const githubHandler: UserInfoHandler = (data: any) => { + // GitHub 用户信息格式 + // { + // "id": 12345678, + // "login": "octocat", + // "name": "The Octocat", + // "email": "octocat@github.com", + // "avatar_url": "https://avatars.githubusercontent.com/u/12345678", + // "bio": "...", + // "created_at": "2011-01-25T18:44:36Z" + // } + + return { + id: String(data.id), + name: data.login, + nickname: data.name || data.login, + gender: undefined, // GitHub 不提供性别信息 + birth: undefined, + avatarUrl: data.avatar_url, + }; +}; +``` + +```typescript +// src/utils/oauth/handler/gitlab.ts +import { UserInfoHandler } from '../index'; + +export const gitlabHandler: UserInfoHandler = (data: any) => { + // GitLab 用户信息格式 + // { + // "id": 123, + // "username": "user", + // "name": "User Name", + // "email": "user@example.com", + // "avatar_url": "https://...", + // "state": "active" + // } + + return { + id: String(data.id), + name: data.username, + nickname: data.name || data.username, + gender: undefined, + birth: undefined, + avatarUrl: data.avatar_url, + }; +}; +``` + +```typescript +// src/utils/oauth/handler/google.ts +import { UserInfoHandler } from '../index'; + +export const googleHandler: UserInfoHandler = (data: any) => { + // Google 用户信息格式 + // { + // "id": "123456789", + // "email": "user@gmail.com", + // "verified_email": true, + // "name": "User Name", + // "given_name": "User", + // "family_name": "Name", + // "picture": "https://...", + // "locale": "zh-CN" + // } + + return { + id: data.id, + name: data.email.split('@')[0], + nickname: data.name, + gender: undefined, // 需额外权限 + birth: undefined, // 需额外权限 + avatarUrl: data.picture, + }; +}; +``` + +```typescript +// src/utils/oauth/handler/weixin.ts +import { UserInfoHandler } from '../index'; + +export const weixinHandler: UserInfoHandler = (data: any) => { + // 微信用户信息格式 + // { + // "openid": "OPENID", + // "nickname": "NICKNAME", + // "sex": 1, + // "province": "PROVINCE", + // "city": "CITY", + // "country": "COUNTRY", + // "headimgurl": "https://...", + // "unionid": "UNIONID" + // } + + return { + id: data.unionid || data.openid, // 优先使用 unionid + name: data.nickname, + nickname: data.nickname, + gender: data.sex === 1 ? 'male' : data.sex === 2 ? 'female' : undefined, + birth: undefined, + avatarUrl: data.headimgurl, + }; +}; +``` + +```typescript +// src/utils/oauth/handler/microsoft.ts +import { UserInfoHandler } from '../index'; + +export const microsoftHandler: UserInfoHandler = (data: any) => { + // Microsoft 用户信息格式 + // { + // "id": "12345678-1234-1234-1234-123456789012", + // "userPrincipalName": "user@example.com", + // "displayName": "User Name", + // "givenName": "User", + // "surname": "Name", + // "mail": "user@example.com" + // } + + return { + id: data.id, + name: data.userPrincipalName.split('@')[0], + nickname: data.displayName, + gender: undefined, + birth: undefined, + avatarUrl: null, // 需要额外的 Graph API 调用 + }; +}; +``` + +**步骤 2:注册所有处理器** + +```typescript +// src/utils/oauth/handler/index.ts +import { giteaHandler } from "./gitea"; +import { oakHandler } from "./oak"; +import { githubHandler } from "./github"; +import { gitlabHandler } from "./gitlab"; +import { googleHandler } from "./google"; +import { weixinHandler } from "./weixin"; +import { microsoftHandler } from "./microsoft"; + +export const getDefaultHandlers = () => { + return { + oak: oakHandler, + gitea: giteaHandler, + github: githubHandler, + gitlab: gitlabHandler, + google: googleHandler, + weixin: weixinHandler, + microsoft: microsoftHandler, + // 更多提供商... + } +} +``` + +**步骤 3:更新实体类型定义** + +```typescript +// src/entities/OauthProvider.ts +export interface Schema extends EntityShape { + // ... + type: + | "oak" + | "gitea" + | "github" + | "gitlab" + | "google" + | "microsoft" + | "apple" + | "weixin" // 微信开放平台 + | "wecom" // 企业微信 + | "dingtalk" // 钉钉 + | "feishu" // 飞书 + | "custom"; + // ... +} +``` + +**步骤 4:提供配置模板** + +```typescript +// src/config/oauthProviderTemplates.ts + +export const providerTemplates = { + github: { + name: 'GitHub', + type: 'github', + authorizationEndpoint: 'https://github.com/login/oauth/authorize', + tokenEndpoint: 'https://github.com/login/oauth/access_token', + userInfoEndpoint: 'https://api.github.com/user', + scopes: ['user:email'], + logo: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png', + }, + gitlab: { + name: 'GitLab', + type: 'gitlab', + authorizationEndpoint: 'https://gitlab.com/oauth/authorize', + tokenEndpoint: 'https://gitlab.com/oauth/token', + userInfoEndpoint: 'https://gitlab.com/api/v4/user', + scopes: ['read_user'], + logo: 'https://about.gitlab.com/images/press/logo/png/gitlab-logo-500.png', + }, + google: { + name: 'Google', + type: 'google', + authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + tokenEndpoint: 'https://oauth2.googleapis.com/token', + userInfoEndpoint: 'https://www.googleapis.com/oauth2/v2/userinfo', + scopes: ['openid', 'profile', 'email'], + logo: 'https://www.gstatic.com/images/branding/product/1x/googleg_512dp.png', + }, + // ... 更多模板 +}; +``` + +--- + +### 3. 实现 OpenID Connect + +#### 3.1 什么是 OpenID Connect? + +**OpenID Connect (OIDC)** 是基于 OAuth 2.0 的身份认证层,添加了标准化的用户信息获取方式。 + +**核心概念**: + +```mermaid +graph LR + OAuth[OAuth 2.0] -->|授权| Token[Access Token] + OIDC[OpenID Connect] -->|认证| IDToken[ID Token] + OIDC -->|继承| OAuth + + IDToken -->|包含| UserInfo[用户信息] + IDToken -->|格式| JWT[JWT 格式] +``` + +**与 OAuth 2.0 的区别**: + +| 特性 | OAuth 2.0 | OpenID Connect | +|-----|----------|---------------| +| 目的 | 授权(Authorization) | 认证(Authentication) | +| 获取用户信息 | 调用 `/userinfo` 端点 | 解析 ID Token(JWT) | +| 标准化 | 灵活,各家实现不同 | 标准化用户信息字段 | +| 使用场景 | 第三方应用访问资源 | 用户登录 | + +#### 3.2 实施步骤 + +**步骤 1:添加新实体** + +```typescript +// src/entities/OauthIdToken.ts +import { String, Text, Datetime } from 'oak-domain/lib/types/DataType'; +import { Schema as User } from './User'; +import { Schema as OauthApplication } from './OauthApplication'; +import { EntityShape } from 'oak-domain/lib/types/Entity'; +import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; + +// OpenID Connect ID Token +export interface Schema extends EntityShape { + // JWT 标准声明 (RFC 7519) + iss: String<512>; // Issuer (发行者) + sub: String<256>; // Subject (用户唯一标识) + aud: String<256>; // Audience (接收方,即 client_id) + exp: Datetime; // Expiration Time (过期时间) + iat: Datetime; // Issued At (签发时间) + + // OIDC 标准声明 + auth_time?: Datetime; // 认证时间 + nonce?: String<128>; // 防重放攻击 + + // 关联关系 + user: User; + application: OauthApplication; + + // JWT 原文 + rawToken: Text; +} + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + name: 'OpenID Connect ID Token', + attr: { + iss: '发行者', + sub: '用户标识', + aud: '接收方', + exp: '过期时间', + iat: '签发时间', + auth_time: '认证时间', + nonce: '随机数', + user: '用户', + application: '应用', + rawToken: 'Token 原文', + }, + }, + }, +}; +``` + +**步骤 2:修改令牌端点返回 ID Token** + +```typescript +// src/endpoints/oauth.ts + +import jwt from 'jsonwebtoken'; + +const oauthTokenEndpoint: Endpoint> = { + name: "获取OAuth Token", + params: [], + method: 'post', + type: "free", + fn: async (contextBuilder, params, header, req, body) => { + const context = await contextBuilder(); + + // ... 现有验证逻辑 ... + + // 获取用户信息 + const [user] = await context.select("user", { + data: { + id: 1, + name: 1, + nickname: 1, + email: 1, + phone: 1, + }, + filter: { id: authCodeRecord.userId } + }, {}); + + // 生成 Access Token + const accessToken = randomUUID(); + const refreshToken = randomUUID(); + + // 生成 ID Token (JWT) + const idTokenPayload = { + iss: context.getApplicationId(), // 发行者 + sub: user.id, // 用户ID + aud: client_id, // 客户端ID + exp: Math.floor(Date.now() / 1000) + 3600, // 1小时后过期 + iat: Math.floor(Date.now() / 1000), + + // 标准用户信息 + name: user.name, + nickname: user.nickname, + email: user.email, + phone: user.phone, + + // 自定义声明 + preferred_username: user.name, + email_verified: !!user.email, + phone_number_verified: !!user.phone, + }; + + // 签名 ID Token + const jwtSecret = process.env.JWT_SECRET || 'your-secret-key'; + const idToken = jwt.sign(idTokenPayload, jwtSecret, { + algorithm: 'HS256' + }); + + // 存储 ID Token + await context.operate("oauthIdToken", { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + iss: idTokenPayload.iss, + sub: idTokenPayload.sub, + aud: idTokenPayload.aud, + exp: idTokenPayload.exp * 1000, + iat: idTokenPayload.iat * 1000, + userId: user.id, + applicationId: client_id, + rawToken: idToken, + } + }, {}); + + // ... 创建 Access Token 和 Refresh Token ... + + await context.commit(); + return { + statusCode: 200, + data: { + access_token: accessToken, + token_type: "Bearer", + expires_in: 3600, + refresh_token: refreshToken, + refresh_expires_in: 2592000, + + // 新增:返回 ID Token + id_token: idToken, + + success: true, + } + }; + } +}; +``` + +**步骤 3:实现 OIDC Discovery 端点** + +```typescript +// src/endpoints/oidc.ts + +const oidcDiscoveryEndpoint: Endpoint> = { + name: "OpenID Connect Discovery", + params: [], + method: 'get', + type: "free", + fn: async (contextBuilder, params, header, req, body) => { + const context = await contextBuilder(); + const baseUrl = `${req.protocol}://${req.get('host')}`; + + return { + statusCode: 200, + data: { + issuer: baseUrl, + authorization_endpoint: `${baseUrl}/authorize`, + token_endpoint: `${baseUrl}/oauth/access_token`, + userinfo_endpoint: `${baseUrl}/oauth/userinfo`, + jwks_uri: `${baseUrl}/.well-known/jwks.json`, + + // 支持的功能 + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["HS256", "RS256"], + + // 支持的声明 + claims_supported: [ + "sub", "iss", "aud", "exp", "iat", + "name", "nickname", "email", "phone", + "preferred_username", "email_verified", "phone_number_verified" + ], + + // 支持的范围 + scopes_supported: ["openid", "profile", "email", "phone"], + } + }; + } +}; + +const endpoints: Record>> = { + '.well-known/openid-configuration': oidcDiscoveryEndpoint, + // ... 其他端点 +}; + +export default endpoints; +``` + +**步骤 4:客户端集成示例** + +```javascript +// 使用 OIDC 客户端库 +import { Issuer, generators } from 'openid-client'; + +// 1. 自动发现配置 +const issuer = await Issuer.discover('https://your-oak-system.com'); +console.log('Discovered issuer:', issuer.metadata); + +// 2. 创建客户端 +const client = new issuer.Client({ + client_id: 'your_client_id', + client_secret: 'your_client_secret', + redirect_uris: ['https://your-app.com/callback'], + response_types: ['code'], +}); + +// 3. 生成授权 URL +const nonce = generators.nonce(); +const authUrl = client.authorizationUrl({ + scope: 'openid profile email', + nonce: nonce, +}); + +// 4. 处理回调 +const params = client.callbackParams(req); +const tokenSet = await client.callback('https://your-app.com/callback', params, { + nonce: nonce +}); + +// 5. 验证并解析 ID Token +const claims = tokenSet.claims(); +console.log('User ID:', claims.sub); +console.log('User Name:', claims.name); +console.log('Email:', claims.email); +``` + +#### 3.3 OIDC 用户信息标准化 + +```typescript +// src/types/oidc.ts + +export interface OIDCStandardClaims { + // 标准声明 (OpenID Connect Core 1.0 §5.1) + sub: string; // Subject - 用户唯一标识 + name?: string; // 全名 + given_name?: string; // 名 + family_name?: string; // 姓 + middle_name?: string; // 中间名 + nickname?: string; // 昵称 + preferred_username?: string; // 首选用户名 + profile?: string; // 个人主页URL + picture?: string; // 头像URL + website?: string; // 网站 + email?: string; // 邮箱 + email_verified?: boolean; // 邮箱已验证 + gender?: string; // 性别 + birthdate?: string; // 生日 (YYYY-MM-DD) + zoneinfo?: string; // 时区 + locale?: string; // 语言环境 + phone_number?: string; // 电话 + phone_number_verified?: boolean; // 电话已验证 + address?: { // 地址 + formatted?: string; + street_address?: string; + locality?: string; + region?: string; + postal_code?: string; + country?: string; + }; + updated_at?: number; // 信息更新时间 +} +``` + +--- + +### 4. 添加管理后台 + +#### 4.1 管理后台架构 + +```mermaid +graph TB + subgraph "OAuth 管理后台" + Dashboard[控制面板] + + subgraph "应用管理" + AppList[应用列表] + AppCreate[创建应用] + AppEdit[编辑应用] + AppSecret[密钥管理] + end + + subgraph "提供商管理" + ProviderList[提供商列表] + ProviderCreate[创建提供商] + ProviderEdit[编辑提供商] + end + + subgraph "授权管理" + AuthList[授权记录] + AuthRevoke[撤销授权] + AuthStats[授权统计] + end + + subgraph "令牌管理" + TokenList[令牌列表] + TokenRevoke[撤销令牌] + TokenStats[令牌统计] + end + + subgraph "审计日志" + AuditLog[操作日志] + SecurityLog[安全事件] + end + + Dashboard --> AppList + Dashboard --> ProviderList + Dashboard --> AuthList + Dashboard --> TokenList + Dashboard --> AuditLog + end +``` + +#### 4.2 实现步骤 + +**步骤 1:创建控制面板组件** + +```tsx +// src/components/oauth/dashboard/web.pc.tsx + +import React from 'react'; +import { Card, Row, Col, Statistic } from 'antd'; +import { + AppstoreOutlined, + TeamOutlined, + KeyOutlined, + CheckCircleOutlined +} from '@ant-design/icons'; + +const OAuthDashboard = (props: any) => { + const { statistics } = props.data; + + return ( +
+ + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + {/* 图表区域 */} + + + + {/* 时间序列图表 */} + + + + + {/* 柱状图 */} + + + +
+ ); +}; +``` + +**步骤 2:添加统计查询端点** + +```typescript +// src/endpoints/oauthStats.ts + +const oauthStatsEndpoint: Endpoint> = { + name: "获取OAuth统计信息", + params: [], + method: 'get', + type: "protected", // 需要管理员权限 + fn: async (contextBuilder, params, header, req, body) => { + const context = await contextBuilder(); + + // 统计应用数量 + const totalApps = await context.count("oauthApplication", { + filter: { ableState: 'enabled' } + }); + + // 统计授权用户数 + const totalUsers = await context.count("oauthUserAuthorization", { + filter: { + usageState: 'granted', + token: { + revokedAt: { $exists: false } + } + } + }); + + // 统计活跃令牌 + const activeTokens = await context.count("oauthToken", { + filter: { + accessExpiresAt: { $gt: Date.now() }, + revokedAt: { $exists: false } + } + }); + + // 统计今日授权 + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + const todayAuths = await context.count("oauthUserAuthorization", { + filter: { + authorizedAt: { $gte: todayStart.getTime() } + } + }); + + // 获取授权趋势(最近7天) + const authTrend = await getAuthTrend(context, 7); + + // 获取应用使用排行 + const appRanking = await getAppRanking(context, 10); + + await context.commit(); + return { + statusCode: 200, + data: { + totalApps, + totalUsers, + activeTokens, + todayAuths, + authTrend, + appRanking, + } + }; + } +}; + +// 辅助函数:获取授权趋势 +async function getAuthTrend(context: any, days: number) { + const trend = []; + for (let i = days - 1; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + date.setHours(0, 0, 0, 0); + + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + 1); + + const count = await context.count("oauthUserAuthorization", { + filter: { + authorizedAt: { + $gte: date.getTime(), + $lt: nextDate.getTime() + } + } + }); + + trend.push({ + date: date.toISOString().split('T')[0], + count + }); + } + return trend; +} + +// 辅助函数:获取应用使用排行 +async function getAppRanking(context: any, limit: number) { + // 这里需要使用聚合查询,简化示例 + const apps = await context.select("oauthApplication", { + data: { + id: 1, + name: 1, + // 关联统计授权数量 + }, + filter: { ableState: 'enabled' } + }, {}); + + // 手动统计每个应用的授权数 + const ranking = []; + for (const app of apps.slice(0, limit)) { + const count = await context.count("oauthUserAuthorization", { + filter: { + applicationId: app.id, + usageState: 'granted' + } + }); + ranking.push({ name: app.name, count }); + } + + ranking.sort((a, b) => b.count - a.count); + return ranking; +} +``` + +**步骤 3:令牌管理界面** + +```tsx +// src/components/oauth/tokenManagement/web.pc.tsx + +import React from 'react'; +import { Table, Button, Modal, Tag } from 'antd'; + +const TokenManagement = (props: any) => { + const { tokens } = props.data; + const { revokeToken } = props.methods; + + const columns = [ + { + title: 'Token ID', + dataIndex: 'id', + key: 'id', + render: (id: string) => id.substring(0, 8) + '...', + }, + { + title: '用户', + dataIndex: ['user', 'name'], + key: 'user', + }, + { + title: '应用', + dataIndex: ['code', 'application', 'name'], + key: 'app', + }, + { + title: '状态', + key: 'status', + render: (record: any) => { + if (record.revokedAt) { + return 已撤销; + } + if (record.accessExpiresAt < Date.now()) { + return 已过期; + } + return 活跃; + }, + }, + { + title: '过期时间', + dataIndex: 'accessExpiresAt', + key: 'expires', + render: (time: number) => new Date(time).toLocaleString(), + }, + { + title: '最后使用', + dataIndex: 'lastUsedAt', + key: 'lastUsed', + render: (time?: number) => + time ? new Date(time).toLocaleString() : '-', + }, + { + title: '操作', + key: 'action', + render: (record: any) => { + if (record.revokedAt) return null; + return ( + + ); + }, + }, + ]; + + const handleRevoke = (token: any) => { + Modal.confirm({ + title: '确认撤销令牌?', + content: '撤销后,使用此令牌的应用将无法访问资源', + onOk: () => revokeToken(token.id), + }); + }; + + return ( + + ); +}; +``` + +**步骤 4:审计日志** + +```typescript +// src/entities/OauthAuditLog.ts + +import { String, Text, Datetime } from 'oak-domain/lib/types/DataType'; +import { Schema as User } from './User'; +import { EntityShape } from 'oak-domain/lib/types/Entity'; + +export interface Schema extends EntityShape { + action: + | 'app_create' + | 'app_update' + | 'app_delete' + | 'secret_reset' + | 'auth_grant' + | 'auth_deny' + | 'auth_revoke' + | 'token_issue' + | 'token_refresh' + | 'token_revoke'; + + actor?: User; // 操作者 + targetType: String<64>; // 目标类型 (app/token/auth) + targetId: String<64>; // 目标ID + details: Object; // 详细信息 + ipAddress?: String<45>; // IP地址 + userAgent?: Text; // User Agent + timestamp: Datetime; // 时间戳 +} +``` + +```typescript +// 在关键操作中记录日志 +await context.operate("oauthAuditLog", { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + action: 'token_issue', + actorId: currentUserId, + targetType: 'token', + targetId: tokenId, + details: { + applicationId: app.id, + scopes: scopes, + }, + ipAddress: req.ip, + userAgent: req.get('User-Agent'), + timestamp: Date.now(), + } +}, {}); +``` + +--- + +### 5. 完善监控和审计 + +#### 5.1 监控指标体系 + +```mermaid +graph TB + subgraph "OAuth 监控指标" + subgraph "业务指标" + M1[授权成功率] + M2[令牌颁发量] + M3[令牌刷新量] + M4[用户活跃度] + end + + subgraph "性能指标" + P1[授权响应时间] + P2[令牌颁发延迟] + P3[用户信息查询时间] + end + + subgraph "安全指标" + S1[失败授权次数] + S2[异常令牌使用] + S3[撤销令牌数量] + S4[CSRF攻击尝试] + end + + subgraph "资源指标" + R1[活跃令牌数] + R2[过期令牌清理] + R3[数据库连接数] + end + end +``` + +#### 5.2 实施步骤 + +**步骤 1:定义监控指标实体** + +```typescript +// src/entities/OauthMetrics.ts + +export interface Schema extends EntityShape { + metricType: + | 'auth_success' + | 'auth_failure' + | 'token_issue' + | 'token_refresh' + | 'token_revoke' + | 'api_call'; + + value: Number; // 指标值 + timestamp: Datetime; // 时间戳 + dimensions: Object; // 维度信息(应用ID、用户ID等) +} +``` + +**步骤 2:埋点收集** + +```typescript +// src/utils/metrics.ts + +export async function recordMetric( + context: BRC, + type: string, + value: number, + dimensions: Record +) { + await context.operate("oauthMetrics", { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + metricType: type, + value, + timestamp: Date.now(), + dimensions, + } + }, {}); +} + +// 在关键流程中使用 +await recordMetric(context, 'auth_success', 1, { + applicationId: app.id, + userId: user.id, +}); +``` + +**步骤 3:实时告警规则** + +```typescript +// src/watchers/oauthSecurityWatcher.ts + +import { Watcher } from 'oak-domain/lib/types'; + +const securityWatcher: Watcher> = { + name: "OAuth 安全监控", + cron: "*/5 * * * *", // 每5分钟执行 + fn: async (context) => { + const now = Date.now(); + const fiveMinutesAgo = now - 5 * 60 * 1000; + + // 检查失败授权次数 + const failedAuths = await context.count("oauthAuditLog", { + filter: { + action: 'auth_deny', + timestamp: { $gte: fiveMinutesAgo } + } + }); + + if (failedAuths > 100) { + // 发送告警 + await sendAlert({ + level: 'warning', + message: `检测到大量授权失败:${failedAuths}次`, + timestamp: now, + }); + } + + // 检查异常令牌使用 + const revokedTokenUse = await context.select("oauthAuditLog", { + data: { id: 1, targetId: 1 }, + filter: { + action: 'api_call', + timestamp: { $gte: fiveMinutesAgo }, + 'details.tokenRevoked': true, + } + }, {}); + + if (revokedTokenUse.length > 0) { + await sendAlert({ + level: 'critical', + message: `检测到已撤销令牌被使用`, + details: revokedTokenUse, + }); + } + + // 清理过期数据 + await context.operate("oauthAuthorizationCode", { + id: await generateNewIdAsync(), + action: 'remove', + data: {}, + filter: { + expiresAt: { $lt: now - 24 * 60 * 60 * 1000 } // 删除24小时前过期的 + } + }, {}); + } +}; + +async function sendAlert(alert: any) { + // 集成告警渠道(邮件、钉钉、企业微信等) + console.error('[OAuth Alert]', alert); +} +``` + +**步骤 4:监控面板** + +```tsx +// src/components/oauth/monitoring/web.pc.tsx + +import React, { useEffect, useState } from 'react'; +import { Card, Row, Col, Alert } from 'antd'; +import { Line, Column } from '@ant-design/charts'; + +const OAuthMonitoring = () => { + const [metrics, setMetrics] = useState(null); + const [alerts, setAlerts] = useState([]); + + useEffect(() => { + // 定时刷新数据 + const interval = setInterval(fetchMetrics, 10000); + return () => clearInterval(interval); + }, []); + + const fetchMetrics = async () => { + const response = await fetch('/api/oauth/metrics'); + const data = await response.json(); + setMetrics(data); + }; + + return ( +
+ {/* 告警区域 */} + {alerts.map(alert => ( + + ))} + + {/* 实时指标 */} + +
+ + + + + + + + + + + + {/* 性能监控 */} + + + + {/* P50, P90, P99 百分位图表 */} + + + + + ); +}; +``` + +**步骤 5:审计报告生成** + +```typescript +// src/routines/oauthAuditReport.ts + +import { Routine } from 'oak-domain/lib/types'; + +const generateAuditReport: Routine> = { + name: "生成OAuth审计报告", + cron: "0 0 * * 0", // 每周日0点执行 + fn: async (context) => { + const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + + // 统计本周数据 + const report = { + period: { + start: new Date(weekAgo).toISOString(), + end: new Date().toISOString(), + }, + summary: { + totalAuths: 0, + successAuths: 0, + deniedAuths: 0, + revokedAuths: 0, + newApps: 0, + tokensIssued: 0, + tokensRevoked: 0, + }, + topApps: [], + securityEvents: [], + }; + + // 查询授权统计 + report.summary.totalAuths = await context.count("oauthUserAuthorization", { + filter: { authorizedAt: { $gte: weekAgo } } + }); + + report.summary.successAuths = await context.count("oauthUserAuthorization", { + filter: { + authorizedAt: { $gte: weekAgo }, + usageState: 'granted' + } + }); + + // ... 更多统计 ... + + // 查询安全事件 + const securityLogs = await context.select("oauthAuditLog", { + data: { + id: 1, + action: 1, + timestamp: 1, + details: 1, + }, + filter: { + timestamp: { $gte: weekAgo }, + action: { $in: ['auth_deny', 'token_revoke'] } + } + }, {}); + + report.securityEvents = securityLogs; + + // 生成报告文件 + await generateReportFile(report); + + // 发送邮件通知 + await sendReportEmail(report); + } +}; + +async function generateReportFile(report: any) { + // 生成 PDF 或 HTML 报告 +} + +async function sendReportEmail(report: any) { + // 发送给管理员 +} +``` + +#### 5.3 监控指标说明 + +| 指标类型 | 指标名称 | 计算方式 | 告警阈值 | +|---------|---------|---------|---------| +| 业务指标 | 授权成功率 | 成功授权数 / 总授权数 | < 95% | +| 业务指标 | 令牌颁发量 | 每小时颁发数 | 突增 > 200% | +| 性能指标 | 授权响应时间 | P99 响应时间 | > 3秒 | +| 性能指标 | 令牌颁发延迟 | 平均处理时间 | > 500ms | +| 安全指标 | 失败授权次数 | 5分钟内失败数 | > 100次 | +| 安全指标 | 异常令牌使用 | 已撤销令牌调用 | > 0 | +| 资源指标 | 活跃令牌数 | 未过期未撤销数量 | > 100万 | +| 资源指标 | 过期令牌数 | 过期未清理数量 | > 10万 | + +--- + +## 总结 + +以上五个优化方向的具体实施方案涵盖了: + +1. **PKCE 支持**:完整的代码验证器流程,提升移动应用安全性 +2. **内置提供商**:7+ 常用平台的用户信息解析器和配置模板 +3. **OpenID Connect**:标准化的身份认证层,支持 ID Token 和自动发现 +4. **管理后台**:全面的应用、令牌、授权管理界面和统计面板 +5. **监控审计**:多维度指标体系、实时告警和审计报告 + +这些优化将使 OAuth 系统更加: +- 🔒 **安全**:PKCE 防护 + 实时监控 +- 🔌 **易用**:丰富的提供商支持 + 配置模板 +- 📊 **可观测**:全面的监控指标 + 审计日志 +- 🎯 **标准化**:符合 OIDC 标准