102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
const COMMON_PASSWORDS = new Set([
|
|
'12345678',
|
|
'password',
|
|
'password123',
|
|
'qwertyui',
|
|
'qwerty123',
|
|
'admin123',
|
|
'admin123456',
|
|
'11111111',
|
|
'00000000',
|
|
'abc12345',
|
|
'iloveyou',
|
|
'letmein',
|
|
'welcome',
|
|
'voicehub'
|
|
])
|
|
|
|
export const PASSWORD_MIN_LENGTH = 6
|
|
export const PASSWORD_MAX_LENGTH = 128
|
|
export const INITIAL_PASSWORD_MIN_CLASSES = 2
|
|
|
|
export interface PasswordPolicyOptions {
|
|
minClasses?: number
|
|
complexityCode?: string
|
|
}
|
|
|
|
function getUtf8ByteLength(value: string): number {
|
|
return new TextEncoder().encode(value).length
|
|
}
|
|
|
|
const INITIAL_PASSWORD_POLICY_OPTIONS: PasswordPolicyOptions = {
|
|
minClasses: INITIAL_PASSWORD_MIN_CLASSES,
|
|
complexityCode: 'AUTH_INITIAL_PASSWORD_COMPLEXITY_REQUIRED'
|
|
}
|
|
|
|
export function getPasswordPolicyViolation(password: unknown, options: PasswordPolicyOptions = {}) {
|
|
const minClasses = options.minClasses ?? 3
|
|
const complexityCode = options.complexityCode ?? 'AUTH_PASSWORD_COMPLEXITY_REQUIRED'
|
|
|
|
if (typeof password !== 'string' || !password) {
|
|
return { code: 'AUTH_NEW_PASSWORD_REQUIRED', message: '密码不能为空' }
|
|
}
|
|
|
|
if (password.length < PASSWORD_MIN_LENGTH) {
|
|
return {
|
|
code: 'AUTH_PASSWORD_TOO_SHORT',
|
|
message: `密码长度至少为${PASSWORD_MIN_LENGTH}位`
|
|
}
|
|
}
|
|
|
|
if (password.length > PASSWORD_MAX_LENGTH) {
|
|
return {
|
|
code: 'AUTH_PASSWORD_TOO_LONG',
|
|
message: `密码长度不能超过${PASSWORD_MAX_LENGTH}位`
|
|
}
|
|
}
|
|
|
|
// bcrypt 只处理前 72 字节,拒绝超出范围的输入,避免用户以为完整密码参与校验。
|
|
if (getUtf8ByteLength(password) > 72) {
|
|
return {
|
|
code: 'AUTH_PASSWORD_TOO_MANY_BYTES',
|
|
message: '密码有效长度不能超过72字节'
|
|
}
|
|
}
|
|
|
|
const normalized = password.toLowerCase().replace(/\s+/g, '')
|
|
if (COMMON_PASSWORDS.has(normalized)) {
|
|
return {
|
|
code: 'AUTH_PASSWORD_TOO_COMMON',
|
|
message: '该密码过于常见,请更换更安全的密码'
|
|
}
|
|
}
|
|
|
|
const categories = [
|
|
/[a-z]/.test(password),
|
|
/[A-Z]/.test(password),
|
|
/[0-9]/.test(password),
|
|
/[^A-Za-z0-9]/.test(password)
|
|
].filter(Boolean).length
|
|
|
|
if (categories < minClasses) {
|
|
return {
|
|
code: complexityCode,
|
|
message: `密码至少需要包含大写字母、小写字母、数字、特殊字符中的${minClasses === 2 ? '两' : '三'}类`
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function getInitialPasswordPolicyViolation(password: unknown) {
|
|
return getPasswordPolicyViolation(password, INITIAL_PASSWORD_POLICY_OPTIONS)
|
|
}
|
|
|
|
export function validatePasswordPolicy(password: unknown): string | null {
|
|
return getPasswordPolicyViolation(password)?.message ?? null
|
|
}
|
|
|
|
export function validateInitialPasswordPolicy(password: unknown): string | null {
|
|
return getInitialPasswordPolicyViolation(password)?.message ?? null
|
|
}
|