fix: harden tenant auth and quality gates

This commit is contained in:
hectorzhao
2026-08-28 11:44:16 +08:00
parent c3bf8af3e6
commit 2744690f9f
51 changed files with 1750 additions and 466 deletions
+62
View File
@@ -21,6 +21,9 @@ export type SessionValidationResult =
| { status: 'expired'; code: 'SESSION_INVALID' | 'SESSION_ABSOLUTE_TIMEOUT' | 'SESSION_LOCK_TIMEOUT' };
const SESSION_PREFIX = 'cmpp:auth:session:';
const CAPTCHA_PREFIX = 'cmpp:auth:captcha:';
const ANONYMOUS_FAILURE_PREFIX = 'cmpp:auth:failure:';
const ANONYMOUS_LOCK_PREFIX = 'cmpp:auth:lock:';
export const SESSION_COOKIE_NAME = '__Host-cmpp_session';
export const DEVELOPMENT_SESSION_COOKIE_NAME = 'cmpp_session';
export const ADMIN_SESSION_COOKIE_NAME = '__Host-cmpp_admin_session';
@@ -123,6 +126,61 @@ export class SessionService implements OnModuleDestroy {
return this.client.del(this.key(token));
}
async storeCaptcha(captchaId: string, answer: string, ttlSeconds: number) {
try {
await this.client.set(`${CAPTCHA_PREFIX}${captchaId}`, answer, 'EX', ttlSeconds);
} catch {
throw new ServiceUnavailableException('验证码服务暂不可用');
}
}
async consumeCaptcha(captchaId: string) {
try {
return await this.client.getdel(`${CAPTCHA_PREFIX}${captchaId}`);
} catch {
throw new ServiceUnavailableException('验证码服务暂不可用');
}
}
async isAnonymousLoginLocked(login: string) {
try {
return Boolean(await this.client.exists(`${ANONYMOUS_LOCK_PREFIX}${this.loginDigest(login)}`));
} catch {
throw new ServiceUnavailableException('登录保护服务暂不可用');
}
}
async recordAnonymousLoginFailure(login: string) {
const digest = this.loginDigest(login);
const failureKey = `${ANONYMOUS_FAILURE_PREFIX}${digest}`;
const lockKey = `${ANONYMOUS_LOCK_PREFIX}${digest}`;
try {
const count = Number(await this.client.eval(
`local count = redis.call('INCR', KEYS[1])
if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
if count >= tonumber(ARGV[2]) then redis.call('SET', KEYS[2], '1', 'EX', ARGV[1]) end
return count`,
2,
failureKey,
lockKey,
24 * 60 * 60,
5,
));
return count;
} catch {
throw new ServiceUnavailableException('登录保护服务暂不可用');
}
}
async clearAnonymousLoginFailures(login: string) {
const digest = this.loginDigest(login);
try {
await this.client.del(`${ANONYMOUS_FAILURE_PREFIX}${digest}`, `${ANONYMOUS_LOCK_PREFIX}${digest}`);
} catch {
throw new ServiceUnavailableException('登录保护服务暂不可用');
}
}
isRecentlyAuthenticated(record: AuthSessionRecord) {
return Date.now() - record.lastAuthenticatedAt < this.recentAuthenticationMs;
}
@@ -195,6 +253,10 @@ export class SessionService implements OnModuleDestroy {
return `${SESSION_PREFIX}${createHash('sha256').update(token).digest('hex')}`;
}
private loginDigest(login: string) {
return createHash('sha256').update(login.trim().toLocaleLowerCase('en-US')).digest('hex');
}
private get client() {
if (!this.redis) {
this.redis = new IORedis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', {