- 后端:基于 node-oidc-provider 实现 OIDC Provider - 支持 authorization_code、refresh_token、client_credentials 授权类型 - Redis adapter 存储会话数据,Prisma adapter 存储持久化数据 - 客户端管理 CRUD API(创建、更新、删除、重新生成密钥) - 交互 API(登录、授权确认、中止) - 第一方应用自动跳过授权确认页面 - 使用 cuid2 生成客户端 ID - 前端:OIDC 客户端管理界面 - 客户端列表表格(支持分页、排序) - 创建/编辑弹窗(支持所有 OIDC 配置字段) - OIDC 交互页面(登录表单、授权确认表单) - 共享类型:添加 OIDC 相关 TypeScript 类型定义 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import type { Adapter, AdapterPayload } from 'oidc-provider';
|
||
|
||
import { RedisService } from '@/common/redis/redis.service';
|
||
|
||
// Redis Key 前缀
|
||
const KEY_PREFIX = 'oidc';
|
||
|
||
// 各模型的默认 TTL(秒)
|
||
const MODEL_TTL: Record<string, number> = {
|
||
AuthorizationCode: 600, // 10 分钟
|
||
AccessToken: 3600, // 1 小时
|
||
Session: 14 * 24 * 3600, // 14 天
|
||
Interaction: 3600, // 1 小时
|
||
DeviceCode: 600, // 10 分钟
|
||
BackchannelAuthenticationRequest: 600, // 10 分钟
|
||
};
|
||
|
||
/**
|
||
* Redis 适配器
|
||
* 用于存储短期/临时数据:AuthorizationCode、AccessToken、Session、Interaction
|
||
*/
|
||
export class RedisAdapter implements Adapter {
|
||
private keyPrefix: string;
|
||
|
||
constructor(
|
||
private redis: RedisService,
|
||
private model: string
|
||
) {
|
||
this.keyPrefix = `${KEY_PREFIX}:${model.toLowerCase()}`;
|
||
}
|
||
|
||
private key(id: string): string {
|
||
return `${this.keyPrefix}:${id}`;
|
||
}
|
||
|
||
private grantKey(grantId: string): string {
|
||
return `${KEY_PREFIX}:grant:${grantId}`;
|
||
}
|
||
|
||
private userCodeKey(userCode: string): string {
|
||
return `${KEY_PREFIX}:usercode:${userCode}`;
|
||
}
|
||
|
||
private uidKey(uid: string): string {
|
||
return `${KEY_PREFIX}:uid:${uid}`;
|
||
}
|
||
|
||
async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise<void> {
|
||
const key = this.key(id);
|
||
const ttl = expiresIn || MODEL_TTL[this.model] || 3600;
|
||
|
||
// 存储主数据
|
||
await this.redis.setJson(key, payload, ttl);
|
||
|
||
// 如果有 grantId,建立索引
|
||
if (payload.grantId) {
|
||
const grantKey = this.grantKey(payload.grantId);
|
||
await this.redis.sadd(grantKey, key);
|
||
await this.redis.expire(grantKey, ttl);
|
||
}
|
||
|
||
// 如果有 userCode,建立索引
|
||
if (payload.userCode) {
|
||
const userCodeKey = this.userCodeKey(payload.userCode);
|
||
await this.redis.set(userCodeKey, id, ttl);
|
||
}
|
||
|
||
// 如果有 uid,建立索引
|
||
if (payload.uid) {
|
||
const uidKey = this.uidKey(payload.uid);
|
||
await this.redis.set(uidKey, id, ttl);
|
||
}
|
||
}
|
||
|
||
async find(id: string): Promise<AdapterPayload | undefined> {
|
||
const data = await this.redis.getJson<AdapterPayload>(this.key(id));
|
||
return data ?? undefined;
|
||
}
|
||
|
||
async findByUserCode(userCode: string): Promise<AdapterPayload | undefined> {
|
||
const id = await this.redis.get(this.userCodeKey(userCode));
|
||
if (!id) return undefined;
|
||
return this.find(id);
|
||
}
|
||
|
||
async findByUid(uid: string): Promise<AdapterPayload | undefined> {
|
||
const id = await this.redis.get(this.uidKey(uid));
|
||
if (!id) return undefined;
|
||
return this.find(id);
|
||
}
|
||
|
||
async consume(id: string): Promise<void> {
|
||
const data = await this.find(id);
|
||
if (data) {
|
||
data.consumed = Math.floor(Date.now() / 1000);
|
||
const ttl = await this.redis.ttl(this.key(id));
|
||
if (ttl > 0) {
|
||
await this.redis.setJson(this.key(id), data, ttl);
|
||
}
|
||
}
|
||
}
|
||
|
||
async destroy(id: string): Promise<void> {
|
||
const data = await this.find(id);
|
||
if (data) {
|
||
// 清理关联索引
|
||
if (data.grantId) {
|
||
const grantKey = this.grantKey(data.grantId);
|
||
await this.redis.getClient().srem(grantKey, this.key(id));
|
||
}
|
||
if (data.userCode) {
|
||
await this.redis.del(this.userCodeKey(data.userCode));
|
||
}
|
||
if (data.uid) {
|
||
await this.redis.del(this.uidKey(data.uid));
|
||
}
|
||
}
|
||
await this.redis.del(this.key(id));
|
||
}
|
||
|
||
async revokeByGrantId(grantId: string): Promise<void> {
|
||
const grantKey = this.grantKey(grantId);
|
||
const keys = await this.redis.smembers(grantKey);
|
||
if (keys.length > 0) {
|
||
await this.redis.del(...keys, grantKey);
|
||
}
|
||
}
|
||
}
|