Repository navigation
Core Identity
worable edited this page Oct 20, 2026
·
1 revision
实现依据:
REVIEW-通知通道与注册验证.md§5。 内核拥有一份短时、单次、限次的邮箱验证码底座与一个人机验证受控契约;投递渠道与验证 提供者由插件经扩展点供给,内核不绑定任何厂商(无 SMTP、无 Turnstile/reCAPTCHA 内建实现)。
- 未注册用户没有可靠
userId——通知表按userId归属,写不进去。 - 验证码是事务性凭证,一旦进入站内消息、SSE、通用事件、审计或应用日志就等于泄露。
- 因此内核用独立身份挑战流程:按「规范化邮箱 + 用途」寻址,只保存摘要与盐,明文只在发送 时出现一次。
ctx.identity 由内核提供(KernelIdentityChallengeService),插件(如 login)调用:
interface IdentityChallengeService {
requiresEmailVerification(): boolean; // 部署策略:注册是否强制邮箱验证
isDeliveryConfigured(): boolean; // 是否已注册发送渠道
senderIds(): string[];
issue(input: { purpose; email; ip?; userAgent?; locale? }): Promise<IssuedIdentityChallenge>;
consume(input: { purpose; email; code }): Promise<void>;
}-
存储:
identity_challenges表(迁移20261017000000_identity_challenge)。字段含用途、 规范化邮箱、codeDigest/codeSalt、attempts/maxAttempts、expiresAt、consumedAt。 -
摘要:HMAC-SHA256,密钥由内核
JWT_SECRET派生——数据库泄露也无法离线爆破 6 位码。 - TTL / 限次 / 限速:由环境变量控制(见下)。超时、错误次数用尽即作废并需重取。
-
单次消费:
consume以updateMany({ id, consumedAt: null })原子翻转;并发第二次消费 看到count === 0被拒(重放防护)。 -
失败码:
auth.code_missing/auth.code_invalid/auth.code_expired/auth.code_locked/auth.code_rate_limited/auth.code_channel_unavailable/auth.code_delivery_failed。 -
不静默跳过:未配置发送渠道时
issue直接抛auth.code_channel_unavailable,绝不假装成功。
插件注册一个 IdentityChallengeSender:
interface IdentityChallengeSender {
readonly id: string;
readonly name: string;
send(message: {
purpose: 'register' | 'email_change' | 'password_reset';
email: string;
code: string; // 明文,仅此一次
expiresAt: Date;
locale?: string;
}): Promise<void>;
}内核按注册顺序尝试发送者;全部失败则作废该挑战并抛 auth.code_delivery_failed。SMTP/短信渠道
作为独立插件(建议放插件仓 StackPanel-plugins)实现此接口,同时实现 notification.channel
承担普通通知。
内核提供 KernelCaptchaService,策略由内核拥有(AUTH_CAPTCHA_MODE),调用方不自行分支:
type CaptchaMode = 'off' | 'optional' | 'required';
interface CaptchaService {
mode(): CaptchaMode;
isConfigured(): boolean;
providerIds(): string[];
verify(input: { token; action?; remoteIp? }): Promise<{ success; reason?; providerId? }>;
enforce(input): Promise<void>; // 按策略校验,失败抛确定性错误
clientConfig(): Record<string, unknown> | null; // 前端部件公开配置(非密钥)
}-
off从不过问;optional有 token 则校验、无则放行;required无有效 token 直接拒绝, 且未配置 provider 时也拒绝(fail closed)。 - 前端部件从
GET /auth/config读取captcha.mode/configured/clientConfig,把 token 随 注册与发送验证码请求一并提交。 - provider 实现
verify()(并可选clientConfig());前端 token 的 action/site/有效期校验属于 provider 责任。
| 变量 | 默认 | 说明 |
|---|---|---|
AUTH_EMAIL_VERIFICATION |
off |
required 时注册必须先消费邮箱验证码,验证通过才创建 ACTIVE 账户 |
AUTH_CAPTCHA_MODE |
off |
off / optional / required
|
AUTH_CODE_TTL_SECONDS |
600 |
验证码有效期 |
AUTH_CODE_MAX_ATTEMPTS |
5 |
单个挑战的最大错误次数 |
AUTH_CODE_SEND_PER_HOUR |
5 |
每邮箱每用途每小时的发送上限(0 关闭) |
AUTH_CODE_SEND_PER_HOUR_PER_IP |
20 |
每来源 IP 每小时的发送上限(0 关闭) |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /register/send-code |
发送注册验证码(受 captcha 与限速约束) |
| POST | /register |
注册;AUTH_EMAIL_VERIFICATION=required 时 code 必填且先消费 |
| GET | /auth/config |
公开认证策略(是否强制邮箱验证、captcha 模式与公开配置) |
账户验证时间写入 users.emailVerifiedAt:仅在验证通过(或明确标记)时写入;null 不代表
「按策略未要求验证」,也覆盖历史账户与第三方免密身份。
- 单元测试:
apps/api/tests/unit/identity-challenge.test.ts(摘要不落明文、单次消费、重放拒绝、 错误次数锁定、过期、无渠道拒绝、投递失败作废、重发作废旧码)。 - 单元测试:
apps/api/tests/unit/captcha-service.test.ts(策略、缺失 token、provider 故障不算通过、 多 provider 回落、clientConfig)。 - 集成测试:
apps/api/tests/integration/register-verification.test.ts(真实 DB:无码拒绝、发码、 错码拒绝、正码注册 ACTIVE 且写emailVerifiedAt、重放拒绝)。 - 未覆盖:真实 SMTP/短信投递、真实第三方 captcha 联调(本轮无具体 provider)。
开始
- 核心开发
构建内核扩展
- 插件
- 主题与前端包
AI 与自动化
- 内核 Agent
- MCP
对接平台
- Open Platform API
- Partner API