46 KiB
46 KiB
下一步优化方案
1. 实现 PKCE 完整支持
1.1 什么是 PKCE?
PKCE(Proof Key for Code Exchange,RFC 7636)是 OAuth 2.0 的安全扩展,主要用于公开客户端(如移动应用、单页应用)防止授权码拦截攻击。
1.2 为什么需要 PKCE?
传统授权码流程的安全隐患:
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 解决方案:
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. 授权请求<br/>(code_challenge, method=S256)
AS->>App: 2. 返回授权码
alt 正常流程
App->>AS: 3. 换取令牌<br/>(code, code_verifier)
AS->>AS: 验证: SHA256(code_verifier) == code_challenge
AS->>App: 4. 返回令牌
else 攻击者拦截
Attacker->>AS: 3. 换取令牌<br/>(code, 无code_verifier)
AS->>Attacker: 4. 拒绝请求
Note over Attacker: 攻击失败!
end
1.3 实施步骤
步骤 1:更新数据库结构
实体 OauthAuthorizationCode 已经预留了字段:
// src/entities/OauthAuthorizationCode.ts
export interface Schema extends EntityShape {
// ...现有字段...
// PKCE 扩展 (RFC 7636) - 已预留
codeChallenge?: String<128>;
codeChallengeMethod?: String<10>; // "plain" or "S256"
}
步骤 2:修改授权端点
// src/aspects/oauth.ts - authorize()
export async function authorize<ED extends EntityDict>(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<ED>) {
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:修改令牌端点验证
// src/endpoints/oauth.ts - oauthTokenEndpoint
const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
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<boolean> {
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:更新应用配置
// src/entities/OauthApplication.ts
export interface Schema extends EntityShape {
// ... 现有字段 ...
// 新增:是否强制使用 PKCE
requirePKCE?: Boolean;
}
export const entityDesc: EntityDesc<Schema, Action, '', {
ableState: AbleState;
}> = {
locales: {
zh_CN: {
// ...
attr: {
// ...
requirePKCE: '强制使用 PKCE',
},
},
},
};
步骤 5:客户端集成示例
// 移动应用或 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 |
开源替代 |
| 🔲 待实现 | ⭐⭐⭐ | /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:创建处理器文件
// 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,
};
};
// 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,
};
};
// 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,
};
};
// 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,
};
};
// 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:注册所有处理器
// 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:更新实体类型定义
// src/entities/OauthProvider.ts
export interface Schema extends EntityShape {
// ...
type:
| "oak"
| "gitea"
| "github"
| "gitlab"
| "google"
| "microsoft"
| "apple"
| "weixin" // 微信开放平台
| "wecom" // 企业微信
| "dingtalk" // 钉钉
| "feishu" // 飞书
| "custom";
// ...
}
步骤 4:提供配置模板
// 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 的身份认证层,添加了标准化的用户信息获取方式。
核心概念:
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:添加新实体
// 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<Schema, '', '', {}> = {
locales: {
zh_CN: {
name: 'OpenID Connect ID Token',
attr: {
iss: '发行者',
sub: '用户标识',
aud: '接收方',
exp: '过期时间',
iat: '签发时间',
auth_time: '认证时间',
nonce: '随机数',
user: '用户',
application: '应用',
rawToken: 'Token 原文',
},
},
},
};
步骤 2:修改令牌端点返回 ID Token
// src/endpoints/oauth.ts
import jwt from 'jsonwebtoken';
const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
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 端点
// src/endpoints/oidc.ts
const oidcDiscoveryEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
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<string, Endpoint<EntityDict, BRC<EntityDict>>> = {
'.well-known/openid-configuration': oidcDiscoveryEndpoint,
// ... 其他端点
};
export default endpoints;
步骤 4:客户端集成示例
// 使用 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 用户信息标准化
// 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 管理后台架构
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:创建控制面板组件
// 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 (
<div>
<Row gutter={16}>
<Col span={6}>
<Card>
<Statistic
title="OAuth 应用"
value={statistics.totalApps}
prefix={<AppstoreOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="授权用户"
value={statistics.totalUsers}
prefix={<TeamOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="活跃令牌"
value={statistics.activeTokens}
prefix={<KeyOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="今日授权"
value={statistics.todayAuths}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
</Row>
{/* 图表区域 */}
<Row gutter={16} style={{ marginTop: 24 }}>
<Col span={12}>
<Card title="授权趋势">
{/* 时间序列图表 */}
</Card>
</Col>
<Col span={12}>
<Card title="应用使用排行">
{/* 柱状图 */}
</Card>
</Col>
</Row>
</div>
);
};
步骤 2:添加统计查询端点
// src/endpoints/oauthStats.ts
const oauthStatsEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
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:令牌管理界面
// 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 <Tag color="red">已撤销</Tag>;
}
if (record.accessExpiresAt < Date.now()) {
return <Tag color="orange">已过期</Tag>;
}
return <Tag color="green">活跃</Tag>;
},
},
{
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 (
<Button
danger
size="small"
onClick={() => handleRevoke(record)}
>
撤销
</Button>
);
},
},
];
const handleRevoke = (token: any) => {
Modal.confirm({
title: '确认撤销令牌?',
content: '撤销后,使用此令牌的应用将无法访问资源',
onOk: () => revokeToken(token.id),
});
};
return (
<Table
columns={columns}
dataSource={tokens}
rowKey="id"
/>
);
};
步骤 4:审计日志
// 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; // 时间戳
}
// 在关键操作中记录日志
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 监控指标体系
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:定义监控指标实体
// 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:埋点收集
// src/utils/metrics.ts
export async function recordMetric<ED extends EntityDict>(
context: BRC<ED>,
type: string,
value: number,
dimensions: Record<string, any>
) {
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:实时告警规则
// src/watchers/oauthSecurityWatcher.ts
import { Watcher } from 'oak-domain/lib/types';
const securityWatcher: Watcher<EntityDict, BRC<EntityDict>> = {
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:监控面板
// 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<any>(null);
const [alerts, setAlerts] = useState<any[]>([]);
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 (
<div>
{/* 告警区域 */}
{alerts.map(alert => (
<Alert
key={alert.id}
type={alert.level}
message={alert.message}
closable
style={{ marginBottom: 16 }}
/>
))}
{/* 实时指标 */}
<Row gutter={16}>
<Col span={12}>
<Card title="授权成功率">
<Line
data={metrics?.authSuccessRate || []}
xField="time"
yField="rate"
smooth
/>
</Card>
</Col>
<Col span={12}>
<Card title="令牌颁发量">
<Column
data={metrics?.tokenIssue || []}
xField="time"
yField="count"
/>
</Card>
</Col>
</Row>
{/* 性能监控 */}
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="响应时间分布">
{/* P50, P90, P99 百分位图表 */}
</Card>
</Col>
</Row>
</div>
);
};
步骤 5:审计报告生成
// src/routines/oauthAuditReport.ts
import { Routine } from 'oak-domain/lib/types';
const generateAuditReport: Routine<EntityDict, BRC<EntityDict>> = {
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万 |
总结
以上五个优化方向的具体实施方案涵盖了:
- PKCE 支持:完整的代码验证器流程,提升移动应用安全性
- 内置提供商:7+ 常用平台的用户信息解析器和配置模板
- OpenID Connect:标准化的身份认证层,支持 ID Token 和自动发现
- 管理后台:全面的应用、令牌、授权管理界面和统计面板
- 监控审计:多维度指标体系、实时告警和审计报告
这些优化将使 OAuth 系统更加:
- 🔒 安全:PKCE 防护 + 实时监控
- 🔌 易用:丰富的提供商支持 + 配置模板
- 📊 可观测:全面的监控指标 + 审计日志
- 🎯 标准化:符合 OIDC 标准