[Feature]: 密码复杂度检测 (#407)

* feat: 密码复杂度检测

* chore: 改为服务端校验
This commit is contained in:
Stahsf 2024-09-02 16:58:53 +08:00 committed by sudoooooo
parent 99739064cc
commit 1c6908b6a5
5 changed files with 148 additions and 6 deletions

View File

@ -6,6 +6,7 @@ export enum EXCEPTION_CODE {
USER_EXISTS = 2001, // 用户已存在
USER_NOT_EXISTS = 2002, // 用户不存在
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误
PASSWORD_INVALID = 2004, // 密码无效
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误

View File

@ -82,6 +82,19 @@ describe('AuthController', () => {
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
);
});
it('should throw HttpException with PASSWORD_INVALID code when password is invalid', async () => {
const mockUserInfo = {
username: 'testUser',
password: '无效的密码abc123',
captchaId: 'testCaptchaId',
captcha: 'testCaptcha',
};
await expect(controller.register(mockUserInfo)).rejects.toThrow(
new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID),
);
});
});
describe('login', () => {

View File

@ -1,4 +1,4 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { Controller, Post, Body, HttpCode, Get, Query } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UserService } from '../services/user.service';
import { CaptchaService } from '../services/captcha.service';
@ -7,6 +7,9 @@ import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { create } from 'svg-captcha';
import { ApiTags } from '@nestjs/swagger';
const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/;
@ApiTags('auth')
@Controller('/api/auth')
export class AuthController {
@ -28,6 +31,24 @@ export class AuthController {
captcha: string;
},
) {
if (!userInfo.password) {
throw new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID);
}
if (userInfo.password.length < 6 || userInfo.password.length > 16) {
throw new HttpException(
'密码长度在 6 到 16 个字符',
EXCEPTION_CODE.PASSWORD_INVALID,
);
}
if (!passwordReg.test(userInfo.password)) {
throw new HttpException(
'密码只能输入数字、字母、特殊字符',
EXCEPTION_CODE.PASSWORD_INVALID,
);
}
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
captcha: userInfo.captcha,
id: userInfo.captchaId,
@ -162,4 +183,35 @@ export class AuthController {
},
};
}
/**
*
*/
@Get('register/password/strength')
@HttpCode(200)
async getPasswordStrength(@Query('password') password: string) {
const numberReg = /[0-9]/.test(password);
const letterReg = /[a-zA-Z]/.test(password);
const symbolReg = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
// 包含三种、且长度大于8
if (numberReg && letterReg && symbolReg && password.length >= 8) {
return {
code: 200,
data: 'Strong',
};
}
// 满足任意两种
if ([numberReg, letterReg, symbolReg].filter(Boolean).length >= 2) {
return {
code: 200,
data: 'Medium',
};
}
return {
code: 200,
data: 'Weak',
};
}
}

View File

@ -7,3 +7,12 @@ export const register = (data) => {
export const login = (data) => {
return axios.post('/auth/login', data)
}
/** 获取密码强度 */
export const getPasswordStrength = (password) => {
return axios.get('/auth/register/password/strength', {
params: {
password
}
})
}

View File

@ -27,6 +27,15 @@
<el-input type="password" v-model="formData.password" size="large"></el-input>
</el-form-item>
<el-form-item label="" v-if="passwordStrength">
<span
class="strength"
v-for="item in 3"
:key="item"
:style="{ backgroundColor: strengthColor[item - 1][passwordStrength] }"
></span>
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<div class="captcha-wrapper">
<el-input style="width: 150px" v-model="formData.captcha" size="large"></el-input>
@ -62,7 +71,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { login, register } from '@/management/api/auth'
import { getPasswordStrength, login, register } from '@/management/api/auth'
import { refreshCaptcha as refreshCaptchaApi } from '@/management/api/captcha'
import { CODE_MAP } from '@/management/api/base'
import { useUserStore } from '@/management/stores/user'
@ -89,6 +98,55 @@ const formData = reactive<FormData>({
captchaId: ''
})
// 0
const strengthColor = reactive([
{
Strong: '#67C23A',
Medium: '#ebb563',
Weak: '#f78989'
},
{
Strong: '#67C23A',
Medium: '#ebb563',
Weak: '#2a598a'
},
{
Strong: '#67C23A',
Medium: '#2a598a',
Weak: '#2a598a'
}
])
//
const passwordValidator = (_: any, value: any, callback: any) => {
if (!value) {
callback(new Error('请输入密码'))
passwordStrength.value = undefined
return
}
if (value.length < 6 || value.length > 16) {
callback(new Error('长度在 6 到 16 个字符'))
passwordStrength.value = undefined
return
}
if (!/^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/.test(value)) {
callback(new Error('只能输入数字、字母、特殊字符'))
passwordStrength.value = undefined
return
}
passwordStrengthHandle(value)
callback()
}
const passwordStrengthHandle = async (value: string) => {
const res: any = await getPasswordStrength(value)
if (res.code === CODE_MAP.SUCCESS) {
passwordStrength.value = res.data
}
}
const rules = {
name: [
{ required: true, message: '请输入账号', trigger: 'blur' },
@ -100,11 +158,8 @@ const rules = {
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
min: 8,
max: 16,
message: '长度在 8 到 16 个字符',
validator: passwordValidator,
trigger: 'blur'
}
],
@ -128,6 +183,7 @@ const pending = reactive<Pending>({
const captchaImgData = ref<string>('')
const formDataRef = ref<any>(null)
const passwordStrength = ref<'Strong' | 'Medium' | 'Weak'>()
const submitForm = (type: 'login' | 'register') => {
formDataRef.value.validate(async (valid: boolean) => {
@ -258,5 +314,16 @@ const refreshCaptcha = async () => {
}
}
}
.strength {
display: inline-block;
width: 20%;
height: 6px;
border-radius: 8px;
background: red;
&:not(:first-child) {
margin-left: 10px;
}
}
}
</style>