docs: 更新 OIDC Provider 文档并完善环境配置

文档更新:
- 从"实施方案"改为"实施文档",标记为已完成状态
- 添加快速开始章节,提供完整的使用示例
- 补充第一方应用自动授权的两种场景实现细节
- 补充 Grant Scope 存储的 payload 结构说明
- 新增客户端服务章节(cuid2 ID + 随机密钥)
- 更新关键文件清单(后端/前端/共享类型)

环境配置:
- 添加 FRONTEND_URL 配置
- 添加 OIDC Provider 开发环境配置

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-20 17:29:26 +08:00
parent 90513e8278
commit 3943bd112f
2 changed files with 296 additions and 40 deletions

View File

@@ -1,11 +1,90 @@
# OIDC Provider 实施方案
# OIDC Provider 实施文档
本文档描述如何将系统扩展为 OIDC Provider对外提供中央鉴权业务。
本文档描述系统作为 OIDC Provider 对外提供中央鉴权业务的实现方案
> **实施状态**:✅ 已完成2025年1月
## 一、方案概述
基于 [panva/node-oidc-provider](https://github.com/panva/node-oidc-provider) 库实现 OIDC Provider 功能,该库是 OpenID Certified 的成熟实现,支持完整的 OAuth 2.0 和 OpenID Connect 规范。
### 1.1 核心特性
- **混合存储策略**PostgreSQL 存储持久化数据Redis 存储短期/会话数据
- **第一方应用自动授权**:配置 `isFirstParty: true` 的客户端自动跳过授权确认页
- **完整的客户端管理**:支持创建、编辑、删除客户端,以及密钥重新生成
- **标准 OIDC 端点**支持授权、令牌、用户信息、JWKS、令牌撤销等标准端点
### 1.2 快速开始
**1. 环境配置**
确保 `.env` 文件包含以下配置:
```bash
OIDC_ISSUER=http://localhost:4000/oidc
OIDC_COOKIE_SECRET=your-oidc-cookie-secret
OIDC_JWKS_PRIVATE_KEY=<Base64编码的RSA私钥>
```
生成 JWKS 私钥:
```bash
openssl genrsa 2048 | base64 -w 0
```
**2. 创建客户端**
通过管理界面(/oidc-clients或 API 创建 OIDC 客户端:
```bash
curl -X POST http://localhost:4000/oidc-clients \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"clientName": "我的应用",
"redirectUris": ["http://localhost:3001/callback"],
"grantTypes": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "email"],
"isFirstParty": false
}'
```
响应会包含 `clientId``clientSecret`(仅显示一次)。
**3. 发起授权**
引导用户访问授权端点:
```
http://localhost:4000/oidc/authorize
?client_id=<clientId>
&redirect_uri=http://localhost:3001/callback
&response_type=code
&scope=openid profile email
&state=<random_state>
```
**4. 获取令牌**
用户授权后,使用授权码换取令牌:
```bash
curl -X POST http://localhost:4000/oidc/token \
-u "<clientId>:<clientSecret>" \
-d "grant_type=authorization_code" \
-d "code=<authorization_code>" \
-d "redirect_uri=http://localhost:3001/callback"
```
**5. 获取用户信息**
使用 Access Token 获取用户信息:
```bash
curl http://localhost:4000/oidc/userinfo \
-H "Authorization: Bearer <access_token>"
```
## 二、数据存储设计
### 2.1 存储策略
@@ -131,21 +210,21 @@ model User {
```
apps/api/src/oidc/
├── oidc.module.ts # OIDC 模块定义
├── oidc.controller.ts # OIDC 端点控制器
├── oidc.service.ts # OIDC 核心服务
├── oidc.controller.ts # 交互端点控制器(/oidc-interaction
├── oidc.service.ts # OIDC 核心服务Provider 初始化与管理)
├── adapters/
│ ├── index.ts # 混合适配器工厂
│ ├── prisma.adapter.ts # Prisma 存储适配器Client、Grant、RefreshToken
│ └── redis.adapter.ts # Redis 存储适配器AuthorizationCode、AccessToken、Session、Interaction
├── controllers/
│ └── client.controller.ts # 客户端管理控制器(/oidc-clients
├── services/
│ ├── account.service.ts # 账户查找服务
│ ├── client.service.ts # 客户端管理服务
│ └── interaction.service.ts # 交互处理服务
│ ├── account.service.ts # 账户查找服务findAccount 实现)
│ ├── client.service.ts # 客户端管理服务CRUD + 密钥生成)
│ └── interaction.service.ts # 交互处理服务(登录、授权确认)
├── dto/
│ ├── client.dto.ts # 客户端 DTO
── consent.dto.ts # 授权确认 DTO
│ └── interaction.dto.ts # 交互 DTO
├── guards/
│ └── oidc-interaction.guard.ts # 交互会话守卫
│ ├── client.dto.ts # 客户端 DTO(创建、更新、响应)
── interaction.dto.ts # 交互 DTO登录、授权确认
└── config/
└── oidc.config.ts # OIDC Provider 配置
```
@@ -326,7 +405,41 @@ export class OidcAccountService {
}
```
#### 3.2.5 OIDC 配置 (`config/oidc.config.ts`)
#### 3.2.5 客户端服务 (`services/client.service.ts`)
客户端 ID 使用 cuid2 生成,客户端密钥使用 64 字符随机十六进制:
```typescript
import { randomBytes } from 'crypto';
import { Injectable } from '@nestjs/common';
import { createId } from '@paralleldrive/cuid2';
@Injectable()
export class OidcClientService extends CrudService<...> {
// 生成客户端 ID使用 cuid2格式如 clxxx...
private generateClientId(): string {
return createId();
}
// 生成客户端密钥64 字符随机十六进制,无前缀)
private generateClientSecret(): string {
return randomBytes(32).toString('hex');
}
async createClient(dto: CreateOidcClientDto) {
const clientId = this.generateClientId();
const clientSecret = this.generateClientSecret();
// ...创建客户端逻辑
}
async regenerateSecret(id: string) {
const clientSecret = this.generateClientSecret();
// ...更新密钥逻辑
}
}
```
#### 3.2.6 OIDC 配置 (`config/oidc.config.ts`)
```typescript
import type { Configuration } from 'oidc-provider';
@@ -408,7 +521,7 @@ export function createOidcConfiguration(
}
```
#### 3.2.6 OIDC 控制器 (`oidc.controller.ts`)
#### 3.2.7 OIDC 控制器 (`oidc.controller.ts`)
```typescript
import { All, Controller, Get, Post, Req, Res, UseGuards } from '@nestjs/common';
@@ -467,7 +580,7 @@ export class OidcController {
}
```
#### 3.2.7 客户端管理控制器 (`client.controller.ts`)
#### 3.2.8 客户端管理控制器 (`client.controller.ts`)
```typescript
@ApiTags('OIDC 客户端管理')
@@ -909,7 +1022,7 @@ OIDC_JWKS_PRIVATE_KEY=
- 支持 private_key_jwt 高安全认证方式
4. **授权确认**
- 第三方应用必须经过用户授权确认
- 第一方应用可配置跳过确认
- **第一方应用自动授权**(见下文详细说明)
- 记住用户授权决定,避免重复确认
5. **令牌撤销**
- 支持主动撤销 Access Token 和 Refresh Token
@@ -918,34 +1031,138 @@ OIDC_JWKS_PRIVATE_KEY=
- Token 端点需要支持跨域请求
- 配置允许的 Origin 列表
### 8.1 第一方应用自动授权实现
第一方应用(`isFirstParty: true`)在两种场景下自动跳过授权确认页:
**场景 1登录时自动授权**
用户在 OIDC 登录页面输入凭据后,`finishLogin` 方法检测第一方应用并自动完成授权:
```typescript
// interaction.service.ts
async finishLogin(provider, ctx, accountId, options) {
// 检查是否为第一方应用
const client = await provider.Client.find(options.clientId);
const isFirstParty = client?.['urn:custom:first_party'];
if (isFirstParty) {
// 第一方应用:登录后自动完成授权
const grant = new provider.Grant({ accountId, clientId: options.clientId });
options.scopes.forEach(scope => grant.addOIDCScope(scope));
const grantId = await grant.save();
return provider.interactionFinished(ctx.req, ctx.res, {
login: { accountId },
consent: { grantId },
}, { mergeWithLastSubmission: true });
}
// 第三方应用:仅完成登录,等待授权确认
return provider.interactionFinished(ctx.req, ctx.res, {
login: { accountId },
}, { mergeWithLastSubmission: false });
}
```
**场景 2已登录用户访问时自动授权**
用户已在系统登录,访问第一方应用的授权 URL 时,`getInteraction` 接口检测并自动完成授权:
```typescript
// oidc.controller.ts
async getInteraction(req, res, uid) {
const details = await this.oidcService.interactionDetails(req, res);
const client = await provider.Client.find(details.params.client_id);
// 检查consent prompt + 已登录 + 第一方应用
if (
details.prompt.name === 'consent' &&
details.session?.accountId &&
client?.['urn:custom:first_party']
) {
const scopes = details.params.scope.split(' ');
const redirectTo = await this.interactionService.finishConsent(...);
// 返回自动授权标记,前端直接重定向
return res.json({ autoConsent: true, redirectTo });
}
// 返回交互详情,前端渲染登录或授权页面
return res.json({ uid, prompt, params, client, session });
}
```
**前端处理自动授权**
```tsx
// page.tsx
const details = await getInteractionDetails(uid);
// 第一方应用自动授权,直接重定向
if (details.autoConsent && details.redirectTo) {
redirect(details.redirectTo);
}
```
### 8.2 Grant Scope 存储
oidc-provider 的 Grant payload 中scope 存储在嵌套结构中:
```typescript
// prisma.adapter.ts - Grant 模型存储逻辑
case 'Grant': {
// payload 结构: { openid: { scope: "openid profile email" } }
const openid = payload.openid as { scope?: string } | undefined;
const scopeString = openid?.scope || '';
await this.prisma.oidcGrant.upsert({
where: { grantId: id },
update: { data: payload, scope: scopeString, expiresAt },
create: {
grantId: id,
clientId: payload.clientId,
userId: payload.accountId,
scope: scopeString,
data: payload,
expiresAt,
},
});
}
```
## 九、实施步骤
### 阶段一基础设施1-2天
> **状态**:全部阶段已完成 ✅
1. 添加 Prisma 数据模型
2. 实现 Prisma 适配器
3. 配置 oidc-provider
4. 集成到 NestJS 应用
### 阶段一:基础设施 ✅
### 阶段二核心功能2-3天
1. ✅ 添加 Prisma 数据模型OidcClient、OidcGrant、OidcRefreshToken
2. ✅ 实现混合适配器Prisma + Redis
3. ✅ 配置 oidc-provider
4. ✅ 集成到 NestJS 应用Express 中间件挂载)
1. 实现账户服务
2. 实现交互处理(登录、授权确认)
3. 实现客户端管理 CRUD
4. 添加权限控制
### 阶段二:核心功能 ✅
### 阶段三前端页面1-2天
1. ✅ 实现账户服务findAccount
2. ✅ 实现交互处理(登录、授权确认、中止)
3. ✅ 实现客户端管理 CRUD
4. ✅ 添加权限控制oidc-client:read/create/update/delete
5. ✅ 实现第一方应用自动授权
1. 授权确认页面
2. OIDC 客户端管理页面
3. 用户已授权应用管理
### 阶段三:前端页面 ✅
### 阶段四测试与文档1天
1. ✅ OIDC 登录页面(独立于现有登录系统
2. ✅ 授权确认页面scope 选择)
3. ✅ OIDC 客户端管理页面(表格 + 创建/编辑弹窗)
4. ✅ 客户端密钥重新生成功能
1. 单元测试
2. 集成测试
3. API 文档完善
4. 使用文档
### 阶段四:测试与完善 ✅
1. ✅ 端到端授权流程测试
2. ✅ 第一方应用自动授权测试
3. ✅ API 文档完善Swagger
4. ✅ 实施文档更新
## 十、NestJS 集成要点
@@ -1025,15 +1242,48 @@ oidc-provider 中间件挂载在 `/oidc` 路径下,会先执行。自定义交
## 十一、关键文件清单
### 后端文件
| 文件 | 说明 |
|------|------|
| `apps/api/src/main.ts` | 应用入口,挂载 oidc-provider 中间件 |
| `apps/api/prisma/schema.prisma` | 添加 OIDC 相关数据模型 |
| `apps/api/src/oidc/` | OIDC 模块目录 |
| `apps/api/src/auth/auth.service.ts` | 参考现有认证逻辑 |
| `apps/web/src/app/(oidc)/` | OIDC 交互页面(登录、授权确认 |
| `apps/web/src/app/(dashboard)/oidc-clients/` | OIDC 客户端管理页面 |
| `apps/api/prisma/schema.prisma` | OIDC 数据模型OidcClient、OidcGrant、OidcRefreshToken |
| `apps/api/src/oidc/oidc.module.ts` | OIDC 模块定义 |
| `apps/api/src/oidc/oidc.service.ts` | Provider 初始化与管理 |
| `apps/api/src/oidc/oidc.controller.ts` | 交互端点(获取详情、登录、授权、中止 |
| `apps/api/src/oidc/adapters/index.ts` | 混合适配器工厂 |
| `apps/api/src/oidc/adapters/prisma.adapter.ts` | PostgreSQL 存储适配器 |
| `apps/api/src/oidc/adapters/redis.adapter.ts` | Redis 存储适配器 |
| `apps/api/src/oidc/services/account.service.ts` | 账户查找服务 |
| `apps/api/src/oidc/services/client.service.ts` | 客户端管理cuid2 ID + 随机密钥) |
| `apps/api/src/oidc/services/interaction.service.ts` | 交互处理(含第一方自动授权) |
| `apps/api/src/oidc/controllers/client.controller.ts` | 客户端管理 API |
| `apps/api/src/oidc/config/oidc.config.ts` | OIDC Provider 配置 |
| `apps/api/src/oidc/dto/client.dto.ts` | 客户端 DTO |
| `apps/api/src/oidc/dto/interaction.dto.ts` | 交互 DTO |
### 前端文件
| 文件 | 说明 |
|------|------|
| `apps/web/src/app/(oidc)/layout.tsx` | OIDC 专用布局 |
| `apps/web/src/app/(oidc)/oidc/interaction/[uid]/page.tsx` | 交互页面SSR |
| `apps/web/src/app/(oidc)/oidc/interaction/[uid]/OidcLoginForm.tsx` | 登录表单 |
| `apps/web/src/app/(oidc)/oidc/interaction/[uid]/OidcConsentForm.tsx` | 授权确认表单 |
| `apps/web/src/app/(oidc)/oidc/error/page.tsx` | OIDC 错误页面 |
| `apps/web/src/app/(dashboard)/oidc-clients/page.tsx` | 客户端管理页面 |
| `apps/web/src/components/oidc-clients/OidcClientsTable.tsx` | 客户端列表表格 |
| `apps/web/src/components/oidc-clients/OidcClientCreateDialog.tsx` | 创建客户端弹窗 |
| `apps/web/src/components/oidc-clients/OidcClientEditDialog.tsx` | 编辑客户端弹窗 |
| `apps/web/src/hooks/useOidcClients.ts` | 客户端管理 hooks |
| `apps/web/src/services/oidc-client.service.ts` | 客户端 API 服务 |
### 共享类型
| 文件 | 说明 |
|------|------|
| `packages/shared/src/types/oidc.ts` | OIDC 共享类型定义 |
| `packages/shared/src/types/index.ts` | 类型导出(包含 OIDC 类型) |
## 十二、参考资料