OAuth2 数据持久化文档 (Data Persistence)
本文档详细描述了 OAuth2 插件的持久化层设计、数据库 Schema、Redis 键值结构以及安全加固方案。
1. 设计目标
- 存储解耦:通过仓储接口(
libs/oauth2/include/fulla/oauth2/repository/下的IClientRepository、IGrantRepository、ITokenRepository等)抽象,支持内存、PostgreSQL、Redis 等多种存储后端,各后端以*RepositoryBundle装配实现。 - 数据持久化:确保 Client 信息、Token、Auth Code 等关键数据不丢失。
- 安全加固:Client Secret 绝不明文存储,强制使用 SHA256 加盐哈希。
- 异步高性能:底层操作全部采用
execSqlAsync和execCommandAsync,基于回调机制,充分利用 Drogon 的非阻塞 I/O 能力。
2. PostgreSQL 存储方案
适用于生产环境,提供严格的全部关系型数据一致性。
2.1 Database Schema
由迁移脚本 apps/server/migrations/V002__oauth2_core.sql 创建(幂等,IF NOT EXISTS;后续迁移会追加 scopes、device codes、lockout 等列)。核心表结构如下:
客户端表 (oauth2_clients)
存储接入的客户端应用信息。
CREATE TABLE IF NOT EXISTS oauth2_clients (
client_id VARCHAR(50) PRIMARY KEY,
client_type VARCHAR(20) NOT NULL DEFAULT 'CONFIDENTIAL',
client_secret VARCHAR(100) NOT NULL, -- 存储 SHA256(secret + salt) 的 Hex 字符串
salt VARCHAR(50) NOT NULL, -- 随机盐值
name VARCHAR(100),
redirect_uris TEXT, -- 逗号分隔或 JSON 数组
allowed_grant_types TEXT -- 允许的 grant_type 列表
);
授权码表 (oauth2_codes)
短期有效的授权凭证。
CREATE TABLE IF NOT EXISTS oauth2_codes (
code VARCHAR(100) PRIMARY KEY,
client_id VARCHAR(50) NOT NULL REFERENCES oauth2_clients(client_id),
user_id VARCHAR(50),
scope TEXT,
redirect_uri TEXT,
code_challenge VARCHAR(128), -- PKCE 支持
code_challenge_method VARCHAR(10), -- S256 / plain
expires_at BIGINT NOT NULL, -- Unix Timestamp
used BOOLEAN DEFAULT FALSE -- 防重放攻击
);
访问令牌表 (oauth2_access_tokens)
CREATE TABLE IF NOT EXISTS oauth2_access_tokens (
token VARCHAR(100) PRIMARY KEY, -- 存 SHA-256(token) 哈希(64 hex),非明文(ADR-0004)
client_id VARCHAR(50) NOT NULL REFERENCES oauth2_clients(client_id),
user_id VARCHAR(50),
scope TEXT,
expires_at BIGINT NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
issued_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT,
issuer VARCHAR(255) NOT NULL DEFAULT '',
audience VARCHAR(255),
not_before BIGINT DEFAULT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT,
introspect_count INTEGER DEFAULT 0,
revoked_at BIGINT,
revoked_by VARCHAR(50)
);
刷新令牌表 (oauth2_refresh_tokens)
CREATE TABLE IF NOT EXISTS oauth2_refresh_tokens (
token VARCHAR(100) PRIMARY KEY, -- 存 SHA-256(token) 哈希,非明文(ADR-0004)
access_token VARCHAR(100) NOT NULL, -- 关联的访问令牌哈希(无外键约束,按值引用)
client_id VARCHAR(50) NOT NULL REFERENCES oauth2_clients(client_id),
user_id VARCHAR(50),
scope TEXT,
expires_at BIGINT NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
revoked_at BIGINT,
revoked_by VARCHAR(50)
);
3. Redis 存储方案(已弃用)
⚠️ 独立 Redis 存储已弃用(F-005):该模式启动时打 ERROR 日志,并以
unsupported_grant_type拒绝refresh_token授权;历史上 refresh token 从未在该模式持久化。新部署一律用postgres+ 可选 Redis 缓存层(§缓存 一致性)。以下键空间仅作存量部署参考。
3.1 Key Pattern 设计
所有 Key 均以 oauth2: 前缀开头(缓存层另有独立的 fulla:cache: 前缀,
事务协调键族 oauth2:transaction:* 未列入下表)。
| 实体 | Key 格式 | 类型 | TTL | 说明 |
|---|---|---|---|---|
| Client | oauth2:client:{client_id} | Hash | 无 | 字段: secret (Hash), salt, redirect_uris (JSON), allowed_scopes (JSON) |
| Auth Code | oauth2:code:{code} | String | 10分钟 | Value: JSON 序列化对象 |
| Access Token | oauth2:token:{token} | String | 1小时 | Value: JSON 序列化对象 |
| Refresh Token | oauth2:refresh:{token} | String | 30天 | Value: JSON 序列化对象 |
3.2 示例数据
Client (Hash Structure):
HSET oauth2:client:fulla-portal secret "42a121b66fb9f1d4f73125788f42eb6799110c6aeae5a9a12a2fed5307a0088d" salt "random_salt" redirect_uris "[\"http://localhost:5173/callback\"]"
Auth Code (String Value):
{
"client_id": "fulla-portal",
"user_id": "admin",
"scope": "openid",
"redirect_uri": "http://localhost:5173/callback",
"expires_at": 1735689000,
"used": false
}