This commit is contained in:
longdayi 2025-05-26 21:57:05 +08:00
parent 251260aace
commit 0e08e6ff2c
6 changed files with 111 additions and 120 deletions

21
.env.example Normal file
View File

@ -0,0 +1,21 @@
TIMEZONE=Asia/Singapore
# PostgreSQL 配置
POSTGRES_VERSION=latest
# PostgreSQL Env
POSTGRES_DB=nice
POSTGRES_USER=nice
POSTGRES_PASSWORD=nice
# Redis 配置
REDIS_VERSION=7.2.4
REDIS_PASSWORD=nice
# MinIO 配置
MINIO_VERSION=latest
MINIO_ACCESS_KEY=nice
MINIO_SECRET_KEY=nice123
# Elasticsearch 配置
ELASTIC_VERSION=9.0.1
ELASTIC_PASSWORD=nice_elastic_password

View File

@ -44,6 +44,7 @@ app.get('/', (c) => {
app.use('/oidc/*', async (c, next) => {
// @ts-ignore
await oidc.callback(c.req.raw, c.res.raw)
return c.finalize()
// return void 也可以
return
})
export default app

View File

@ -1,7 +1,9 @@
import { Configuration } from 'oidc-provider';
import { nanoid } from 'nanoid';
import { RedisAdapter } from './redis-adapter';
const config: Configuration = {
adapter: RedisAdapter,
clients: [
{
client_id: 'example-client',

View File

@ -0,0 +1,86 @@
import type { Adapter, AdapterPayload } from 'oidc-provider';
import redis from '../redis';
export class RedisAdapter implements Adapter {
name: string;
constructor(name: string) {
this.name = name;
}
key(id: string) {
return `${this.name}:${id}`;
}
async upsert(id: string, payload: AdapterPayload, expiresIn: number) {
const key = this.key(id);
await redis.set(key, JSON.stringify(payload), 'EX', expiresIn);
if (payload && payload.grantId) {
// 记录grantId到id的映射便于revokeByGrantId
await redis.sadd(this.grantKey(payload.grantId), id);
await redis.expire(this.grantKey(payload.grantId), expiresIn);
}
if (payload && payload.userCode) {
await redis.set(this.userCodeKey(payload.userCode), id, 'EX', expiresIn);
}
if (payload && payload.uid) {
await redis.set(this.uidKey(payload.uid), id, 'EX', expiresIn);
}
}
async find(id: string) {
const data = await redis.get(this.key(id));
return data ? JSON.parse(data) : undefined;
}
async findByUserCode(userCode: string) {
const id = await redis.get(this.userCodeKey(userCode));
return id ? this.find(id) : undefined;
}
async findByUid(uid: string) {
const id = await redis.get(this.uidKey(uid));
return id ? this.find(id) : undefined;
}
async destroy(id: string) {
const data = await this.find(id);
await redis.del(this.key(id));
if (data && data.grantId) {
await redis.srem(this.grantKey(data.grantId), id);
}
if (data && data.userCode) {
await redis.del(this.userCodeKey(data.userCode));
}
if (data && data.uid) {
await redis.del(this.uidKey(data.uid));
}
}
async revokeByGrantId(grantId: string) {
const key = this.grantKey(grantId);
const ids = await redis.smembers(key);
if (ids && ids.length) {
await Promise.all(ids.map((id) => this.destroy(id)));
}
await redis.del(key);
}
async consume(id: string) {
const key = this.key(id);
const data = await this.find(id);
if (data) {
data.consumed = Math.floor(Date.now() / 1000);
await redis.set(key, JSON.stringify(data));
}
}
grantKey(grantId: string) {
return `${this.name}:grant:${grantId}`;
}
userCodeKey(userCode: string) {
return `${this.name}:userCode:${userCode}`;
}
uidKey(uid: string) {
return `${this.name}:uid:${uid}`;
}
}

View File

@ -1,2 +1 @@
export * from "./useEntity"

View File

@ -98,121 +98,3 @@ model UserLastVisit {
@@index([userId, resourceType])
@@map("user_last_visit")
}
// OIDC 客户端相关模型
model OidcClient {
id String @id @default(cuid())
clientId String @unique @map("client_id")
clientSecret String? @map("client_secret")
clientName String @map("client_name")
clientUri String? @map("client_uri")
logoUri String? @map("logo_uri")
contacts String[]
redirectUris String[] @map("redirect_uris")
postLogoutRedirectUris String[] @map("post_logout_redirect_uris")
tokenEndpointAuthMethod String @map("token_endpoint_auth_method")
grantTypes String[] @map("grant_types")
responseTypes String[] @map("response_types")
scope String
jwksUri String? @map("jwks_uri")
jwks String?
policyUri String? @map("policy_uri")
tosUri String? @map("tos_uri")
requirePkce Boolean @default(false) @map("require_pkce")
active Boolean @default(true)
createdBy String? @map("created_by")
createdTime DateTime @default(now()) @map("created_time")
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
// 关联模型
consents OidcConsent[]
authorizationCodes OidcCode[]
tokens OidcToken[]
@@map("oidc_clients")
}
// 用户同意记录
model OidcConsent {
id String @id @default(cuid())
userId String @map("user_id")
clientId String @map("client_id")
scope String
createdTime DateTime @default(now()) @map("created_time")
expiresAt DateTime? @map("expires_at")
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@unique([userId, clientId])
@@map("oidc_consents")
}
// 授权码
model OidcCode {
id String @id @default(cuid())
code String @unique
userId String @map("user_id")
clientId String @map("client_id")
scope String
redirectUri String @map("redirect_uri")
codeChallenge String? @map("code_challenge")
codeChallengeMethod String? @map("code_challenge_method")
nonce String?
authTime DateTime @default(now()) @map("auth_time")
expiresAt DateTime @map("expires_at")
used Boolean @default(false)
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@map("oidc_authorization_codes")
}
// 统一令牌表合并access和refresh token
model OidcToken {
id String @id @default(cuid())
token String @unique
userId String @map("user_id")
clientId String @map("client_id")
tokenType String @map("token_type") // "access" 或 "refresh"
scope String
expiresAt DateTime @map("expires_at")
createdTime DateTime @default(now()) @map("created_time")
isRevoked Boolean @default(false) @map("is_revoked")
parentId String? @map("parent_id") // 用于关联refresh token和对应的access token
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([userId, tokenType, isRevoked])
@@map("oidc_tokens")
}
// Session管理
model OidcSession {
id String @id @default(cuid())
sessionId String @unique @map("session_id")
userId String @map("user_id")
expiresAt DateTime @map("expires_at")
lastActive DateTime @default(now()) @map("last_active")
deviceInfo String? @map("device_info")
createdTime DateTime @default(now()) @map("created_time")
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
@@map("oidc_sessions")
}
// 供应商的密钥对
model OidcKeyPair {
id String @id @default(cuid())
kid String @unique
privateKey String @map("private_key")
publicKey String @map("public_key")
algorithm String
active Boolean @default(true)
createdTime DateTime @default(now()) @map("created_time")
expiresAt DateTime? @map("expires_at")
@@map("oidc_key_pairs")
}