refactor: strengthen client boundaries and quality gates
This commit is contained in:
@@ -22,8 +22,13 @@ export type SessionValidationResult =
|
||||
|
||||
const SESSION_PREFIX = 'cmpp:auth:session:';
|
||||
const CAPTCHA_PREFIX = 'cmpp:auth:captcha:';
|
||||
const CAPTCHA_RATE_PREFIX = 'cmpp:auth:captcha-rate:ip:';
|
||||
const ANONYMOUS_FAILURE_PREFIX = 'cmpp:auth:failure:';
|
||||
const ANONYMOUS_LOCK_PREFIX = 'cmpp:auth:lock:';
|
||||
const ANONYMOUS_IP_FAILURE_PREFIX = 'cmpp:auth:failure:ip:';
|
||||
const ANONYMOUS_IP_LOCK_PREFIX = 'cmpp:auth:lock:ip:';
|
||||
const ANONYMOUS_PAIR_FAILURE_PREFIX = 'cmpp:auth:failure:pair:';
|
||||
const ANONYMOUS_PAIR_LOCK_PREFIX = 'cmpp:auth:lock:pair:';
|
||||
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';
|
||||
@@ -142,40 +147,87 @@ export class SessionService implements OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async isAnonymousLoginLocked(login: string) {
|
||||
async assertCaptchaRequestAllowed(sourceIp: string) {
|
||||
const key = `${CAPTCHA_RATE_PREFIX}${this.valueDigest(sourceIp)}`;
|
||||
try {
|
||||
return Boolean(await this.client.exists(`${ANONYMOUS_LOCK_PREFIX}${this.loginDigest(login)}`));
|
||||
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
|
||||
return count`,
|
||||
1,
|
||||
key,
|
||||
5 * 60,
|
||||
),
|
||||
);
|
||||
return count <= 30;
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('验证码服务暂不可用');
|
||||
}
|
||||
}
|
||||
|
||||
async anonymousLoginLockScope(login: string, sourceIp: string) {
|
||||
const accountDigest = this.loginDigest(login);
|
||||
const ipDigest = this.valueDigest(sourceIp);
|
||||
const pairDigest = this.valueDigest(`${accountDigest}:${ipDigest}`);
|
||||
try {
|
||||
const locks = await this.client.mget(
|
||||
`${ANONYMOUS_LOCK_PREFIX}${accountDigest}`,
|
||||
`${ANONYMOUS_IP_LOCK_PREFIX}${ipDigest}`,
|
||||
`${ANONYMOUS_PAIR_LOCK_PREFIX}${pairDigest}`,
|
||||
);
|
||||
if (locks[0]) return 'account' as const;
|
||||
if (locks[1]) return 'ip' as const;
|
||||
if (locks[2]) return 'pair' as const;
|
||||
return null;
|
||||
} 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}`;
|
||||
async recordAnonymousLoginFailure(login: string, sourceIp: string) {
|
||||
const accountDigest = this.loginDigest(login);
|
||||
const ipDigest = this.valueDigest(sourceIp);
|
||||
const pairDigest = this.valueDigest(`${accountDigest}:${ipDigest}`);
|
||||
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,
|
||||
const result = await this.client.eval(
|
||||
`local counts = {}
|
||||
for i = 1, 3 do
|
||||
counts[i] = redis.call('INCR', KEYS[i])
|
||||
if counts[i] == 1 then redis.call('EXPIRE', KEYS[i], ARGV[i]) end
|
||||
if counts[i] >= tonumber(ARGV[i + 3]) then redis.call('SET', KEYS[i + 3], '1', 'EX', ARGV[i]) end
|
||||
end
|
||||
return counts`,
|
||||
6,
|
||||
`${ANONYMOUS_FAILURE_PREFIX}${accountDigest}`,
|
||||
`${ANONYMOUS_IP_FAILURE_PREFIX}${ipDigest}`,
|
||||
`${ANONYMOUS_PAIR_FAILURE_PREFIX}${pairDigest}`,
|
||||
`${ANONYMOUS_LOCK_PREFIX}${accountDigest}`,
|
||||
`${ANONYMOUS_IP_LOCK_PREFIX}${ipDigest}`,
|
||||
`${ANONYMOUS_PAIR_LOCK_PREFIX}${pairDigest}`,
|
||||
24 * 60 * 60,
|
||||
15 * 60,
|
||||
24 * 60 * 60,
|
||||
5,
|
||||
));
|
||||
return count;
|
||||
30,
|
||||
5,
|
||||
);
|
||||
return (result as number[]).map(Number);
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('登录保护服务暂不可用');
|
||||
}
|
||||
}
|
||||
|
||||
async clearAnonymousLoginFailures(login: string) {
|
||||
const digest = this.loginDigest(login);
|
||||
async clearAnonymousLoginFailures(login: string, sourceIp: string) {
|
||||
const accountDigest = this.loginDigest(login);
|
||||
const pairDigest = this.valueDigest(`${accountDigest}:${this.valueDigest(sourceIp)}`);
|
||||
try {
|
||||
await this.client.del(`${ANONYMOUS_FAILURE_PREFIX}${digest}`, `${ANONYMOUS_LOCK_PREFIX}${digest}`);
|
||||
await this.client.del(
|
||||
`${ANONYMOUS_FAILURE_PREFIX}${accountDigest}`,
|
||||
`${ANONYMOUS_LOCK_PREFIX}${accountDigest}`,
|
||||
`${ANONYMOUS_PAIR_FAILURE_PREFIX}${pairDigest}`,
|
||||
`${ANONYMOUS_PAIR_LOCK_PREFIX}${pairDigest}`,
|
||||
);
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('登录保护服务暂不可用');
|
||||
}
|
||||
@@ -196,7 +248,10 @@ export class SessionService implements OnModuleDestroy {
|
||||
}
|
||||
|
||||
get cookieSecure() {
|
||||
return process.env.SESSION_COOKIE_SECURE === 'true' || (process.env.NODE_ENV === 'production' && process.env.SESSION_COOKIE_SECURE !== 'false');
|
||||
return (
|
||||
process.env.SESSION_COOKIE_SECURE === 'true' ||
|
||||
(process.env.NODE_ENV === 'production' && process.env.SESSION_COOKIE_SECURE !== 'false')
|
||||
);
|
||||
}
|
||||
|
||||
cookieName(portal: SessionPortal) {
|
||||
@@ -230,7 +285,7 @@ export class SessionService implements OnModuleDestroy {
|
||||
private async read(token: string): Promise<AuthSessionRecord | null> {
|
||||
try {
|
||||
const value = await this.client.get(this.key(token));
|
||||
return value ? JSON.parse(value) as AuthSessionRecord : null;
|
||||
return value ? (JSON.parse(value) as AuthSessionRecord) : null;
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('登录会话服务暂不可用');
|
||||
}
|
||||
@@ -254,7 +309,11 @@ export class SessionService implements OnModuleDestroy {
|
||||
}
|
||||
|
||||
private loginDigest(login: string) {
|
||||
return createHash('sha256').update(login.trim().toLocaleLowerCase('en-US')).digest('hex');
|
||||
return this.valueDigest(login.trim().toLocaleLowerCase('en-US'));
|
||||
}
|
||||
|
||||
private valueDigest(value: string) {
|
||||
return createHash('sha256').update(value.trim()).digest('hex');
|
||||
}
|
||||
|
||||
private get client() {
|
||||
|
||||
Reference in New Issue
Block a user