[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_EXISTS = 2001, // 用户已存在
USER_NOT_EXISTS = 2002, // 用户不存在 USER_NOT_EXISTS = 2002, // 用户不存在
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误 USER_PASSWORD_WRONG = 2003, // 用户名或密码错误
PASSWORD_INVALID = 2004, // 密码无效
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限 NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错 SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误 SURVEY_TYPE_ERROR = 3003, // 问卷类型错误

View File

@ -82,6 +82,19 @@ describe('AuthController', () => {
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT), 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', () => { 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 { ConfigService } from '@nestjs/config';
import { UserService } from '../services/user.service'; import { UserService } from '../services/user.service';
import { CaptchaService } from '../services/captcha.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 { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { create } from 'svg-captcha'; import { create } from 'svg-captcha';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/;
@ApiTags('auth') @ApiTags('auth')
@Controller('/api/auth') @Controller('/api/auth')
export class AuthController { export class AuthController {
@ -28,6 +31,24 @@ export class AuthController {
captcha: string; 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({ const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
captcha: userInfo.captcha, captcha: userInfo.captcha,
id: userInfo.captchaId, 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) => { export const login = (data) => {
return axios.post('/auth/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-input type="password" v-model="formData.password" size="large"></el-input>
</el-form-item> </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"> <el-form-item label="验证码" prop="captcha">
<div class="captcha-wrapper"> <div class="captcha-wrapper">
<el-input style="width: 150px" v-model="formData.captcha" size="large"></el-input> <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 { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' 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 { refreshCaptcha as refreshCaptchaApi } from '@/management/api/captcha'
import { CODE_MAP } from '@/management/api/base' import { CODE_MAP } from '@/management/api/base'
import { useUserStore } from '@/management/stores/user' import { useUserStore } from '@/management/stores/user'
@ -89,6 +98,55 @@ const formData = reactive<FormData>({
captchaId: '' 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 = { const rules = {
name: [ name: [
{ required: true, message: '请输入账号', trigger: 'blur' }, { required: true, message: '请输入账号', trigger: 'blur' },
@ -100,11 +158,8 @@ const rules = {
} }
], ],
password: [ password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ {
min: 8, validator: passwordValidator,
max: 16,
message: '长度在 8 到 16 个字符',
trigger: 'blur' trigger: 'blur'
} }
], ],
@ -128,6 +183,7 @@ const pending = reactive<Pending>({
const captchaImgData = ref<string>('') const captchaImgData = ref<string>('')
const formDataRef = ref<any>(null) const formDataRef = ref<any>(null)
const passwordStrength = ref<'Strong' | 'Medium' | 'Weak'>()
const submitForm = (type: 'login' | 'register') => { const submitForm = (type: 'login' | 'register') => {
formDataRef.value.validate(async (valid: boolean) => { 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> </style>