feat: 添加密码找回页面和更新认证逻辑
- 新增 `forgot-password/index.tsx` 页面,实现密码找回功能 - 更新 `AndroidAuthServiceImpl`,添加 `authenticateHttpIgnoreToken` 方法,并在 `authenticateHttp` 方法中增加 `ignoreTokenValidation` 参数 - 更新 `AndroidAuthService` 接口,添加 `authenticateHttpIgnoreToken` 方法 - 更新 `AndroidDeviceController`,使用 `authenticateHttpIgnoreToken` 方法进行认证 - 优化 `AndroidDeviceRegistrationServiceImpl` 中的异常信息 - 更新 `.gitignore`,忽略不必要的文件和目录dev_na
parent
ee1e75eda2
commit
194a05cbe0
|
|
@ -9,3 +9,28 @@ backend/src/main/resources/application-local.yml
|
||||||
*.log
|
*.log
|
||||||
!backend/.env.example
|
!backend/.env.example
|
||||||
.omx/
|
.omx/
|
||||||
|
/backend/.env.example
|
||||||
|
/backend/.mvn-settings-ali.xml
|
||||||
|
/backend/.mvn-settings-codex.xml
|
||||||
|
/backend/imeeting-backend.iml
|
||||||
|
/backend/lombok.config
|
||||||
|
/database/
|
||||||
|
/deploy/
|
||||||
|
/docs/
|
||||||
|
/rebel.xml
|
||||||
|
/.editorconfig
|
||||||
|
/frontend/design/
|
||||||
|
/components/
|
||||||
|
/bat/
|
||||||
|
/.agents/
|
||||||
|
/.codex/
|
||||||
|
/.gemini/
|
||||||
|
/.idea/
|
||||||
|
/.m2-temp/
|
||||||
|
/.m2-test/
|
||||||
|
/.omx/
|
||||||
|
/APP_LOG_PATH_IS_UNDEFINED/
|
||||||
|
/backend/.m2repo/
|
||||||
|
/backend/m2repo_local/
|
||||||
|
/backend/src/test/
|
||||||
|
/backend/target/
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public class AndroidDeviceController {
|
||||||
}
|
}
|
||||||
String tenantCode = resolveTenantCode(request, command);
|
String tenantCode = resolveTenantCode(request, command);
|
||||||
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command, "tenantCode", tenantCode);
|
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command, "tenantCode", tenantCode);
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, false);
|
AndroidAuthContext authContext = androidAuthService.authenticateHttpIgnoreToken(request, false);
|
||||||
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
|
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
|
||||||
tenantCode,
|
tenantCode,
|
||||||
authContext.getDeviceId(),
|
authContext.getDeviceId(),
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,6 @@ public interface AndroidAuthService {
|
||||||
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered);
|
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered);
|
||||||
|
|
||||||
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken);
|
AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken);
|
||||||
|
|
||||||
|
AndroidAuthContext authenticateHttpIgnoreToken(HttpServletRequest request, boolean requireRegistered);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,16 +68,26 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
||||||
return authenticateHttp(request, true, false);
|
return authenticateHttp(request, true, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
||||||
return authenticateHttp(request, requireRegistered, false);
|
return authenticateHttp(request, requireRegistered, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken) {
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken) {
|
||||||
|
return authenticateHttp(request, requireRegistered, allowOptionalToken, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidAuthContext authenticateHttpIgnoreToken(HttpServletRequest request, boolean requireRegistered) {
|
||||||
|
return authenticateHttp(request, requireRegistered, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered,
|
||||||
|
boolean allowOptionalToken, boolean ignoreTokenValidation) {
|
||||||
LoginUser loginUser = currentLoginUser();
|
LoginUser loginUser = currentLoginUser();
|
||||||
String resolvedToken = resolveHttpToken(request);
|
String resolvedToken = resolveHttpToken(request);
|
||||||
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
||||||
|
|
@ -108,7 +118,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
return applyLicenseContext(context, license, allowOptionalToken);
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.hasText(resolvedToken)) {
|
if (StringUtils.hasText(resolvedToken) && !ignoreTokenValidation) {
|
||||||
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
||||||
if (requireRegistered && !allowOptionalToken) {
|
if (requireRegistered && !allowOptionalToken) {
|
||||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@ public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegist
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public AndroidDeviceRegisterResponse register(String tenantCode, String deviceCode, String deviceName, String terminalType, String terminalVersion) {
|
public AndroidDeviceRegisterResponse register(String tenantCode, String deviceCode, String deviceName, String terminalType, String terminalVersion) {
|
||||||
if (!StringUtils.hasText(tenantCode)) {
|
if (!StringUtils.hasText(tenantCode)) {
|
||||||
throw new BusinessException("400", "tenantCode不能为空");
|
throw new BusinessException("tenantCode不能为空");
|
||||||
}
|
}
|
||||||
if (!StringUtils.hasText(deviceCode)) {
|
if (!StringUtils.hasText(deviceCode)) {
|
||||||
throw new BusinessException("400", "deviceId不能为空");
|
throw new BusinessException("deviceId不能为空");
|
||||||
}
|
}
|
||||||
SysTenant tenant = requireTenant(tenantCode.trim());
|
SysTenant tenant = requireTenant(tenantCode.trim());
|
||||||
String normalizedDeviceCode = deviceCode.trim();
|
String normalizedDeviceCode = deviceCode.trim();
|
||||||
|
|
@ -81,7 +81,7 @@ public class AndroidDeviceRegistrationServiceImpl implements AndroidDeviceRegist
|
||||||
.eq(SysTenant::getIsDeleted, 0)
|
.eq(SysTenant::getIsDeleted, 0)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (tenant == null || tenant.getId() == null) {
|
if (tenant == null || tenant.getId() == null) {
|
||||||
throw new BusinessException("400", "租户不存在");
|
throw new BusinessException("租户不存在");
|
||||||
}
|
}
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
import {LockOutlined, MailOutlined, ReloadOutlined, SafetyOutlined, UserOutlined} from "@ant-design/icons";
|
||||||
|
import {App, Button, Card, Form, Input, Space, Typography} from "antd";
|
||||||
|
import {useEffect, useMemo, useState} from "react";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchCaptcha,
|
||||||
|
fetchPublicPasswordPolicy,
|
||||||
|
resetPasswordByRecovery,
|
||||||
|
sendPasswordRecoveryCode,
|
||||||
|
type PasswordPolicyPublic,
|
||||||
|
} from "@/api/auth";
|
||||||
|
import {usePlatformConfig} from "@/components/PlatformConfigProvider";
|
||||||
|
import usePageTitle from "@/hooks/usePageTitle";
|
||||||
|
import type {CaptchaResponse} from "@/types";
|
||||||
|
import {buildPasswordPolicyValidator, buildPolicyHints} from "@/utils/password";
|
||||||
|
|
||||||
|
const {Paragraph, Text, Title} = Typography;
|
||||||
|
|
||||||
|
type RecoveryFormValues = {
|
||||||
|
username: string;
|
||||||
|
captchaCode?: string;
|
||||||
|
code: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
const {message} = App.useApp();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {platformConfig, captchaEnabled, loaded} = usePlatformConfig();
|
||||||
|
const [form] = Form.useForm<RecoveryFormValues>();
|
||||||
|
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
||||||
|
const [policy, setPolicy] = useState<PasswordPolicyPublic | null>(null);
|
||||||
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
usePageTitle("忘记密码");
|
||||||
|
|
||||||
|
const policyHints = useMemo(() => buildPolicyHints(policy), [policy]);
|
||||||
|
|
||||||
|
const loadCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
if (!captchaEnabled) {
|
||||||
|
setCaptcha(null);
|
||||||
|
form.setFieldValue("captchaCode", undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchCaptcha();
|
||||||
|
setCaptcha(data);
|
||||||
|
form.setFieldValue("captchaCode", "");
|
||||||
|
} catch {
|
||||||
|
setCaptcha(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
const policyData = await fetchPublicPasswordPolicy();
|
||||||
|
setPolicy(policyData);
|
||||||
|
await loadCaptcha();
|
||||||
|
};
|
||||||
|
|
||||||
|
void init();
|
||||||
|
}, [loaded, captchaEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => setCountdown((value) => value - 1), 1000);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
const fields = captchaEnabled ? ["username", "captchaCode"] : ["username"];
|
||||||
|
const values = await form.validateFields(fields);
|
||||||
|
|
||||||
|
if (captchaEnabled && !captcha?.captchaId) {
|
||||||
|
await loadCaptcha();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
const resp = await sendPasswordRecoveryCode({
|
||||||
|
username: values.username,
|
||||||
|
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
||||||
|
captchaCode: captchaEnabled ? values.captchaCode : undefined,
|
||||||
|
channel: "EMAIL",
|
||||||
|
});
|
||||||
|
message.success(resp.msg || "验证码已发送");
|
||||||
|
if (resp.data) {
|
||||||
|
setCountdown(60);
|
||||||
|
}
|
||||||
|
form.setFieldValue("captchaCode", "");
|
||||||
|
} finally {
|
||||||
|
if (captchaEnabled) {
|
||||||
|
await loadCaptcha();
|
||||||
|
}
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = async (values: RecoveryFormValues) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await resetPasswordByRecovery({
|
||||||
|
username: values.username,
|
||||||
|
channel: "EMAIL",
|
||||||
|
code: values.code,
|
||||||
|
newPassword: values.newPassword,
|
||||||
|
});
|
||||||
|
message.success("密码已重置,请重新登录");
|
||||||
|
navigate("/login", {replace: true});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="login-page login-page--compact"
|
||||||
|
style={
|
||||||
|
platformConfig?.loginBgUrl
|
||||||
|
? {
|
||||||
|
backgroundImage: `linear-gradient(rgba(251,253,255,0.9), rgba(242,246,251,0.96)), url(${platformConfig.loginBgUrl})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card className="surface-card login-card login-card--compact">
|
||||||
|
<Space direction="vertical" size={8} style={{width: "100%", marginBottom: 18}}>
|
||||||
|
<Text className="login-page__badge">{platformConfig?.projectName || "iMeeting"}</Text>
|
||||||
|
<Title level={3} style={{margin: 0}}>
|
||||||
|
忘记密码
|
||||||
|
</Title>
|
||||||
|
<Paragraph type="secondary" style={{marginBottom: 0}}>
|
||||||
|
输入账号并完成邮箱验证后重置密码。
|
||||||
|
</Paragraph>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleFinish} requiredMark={false}>
|
||||||
|
<Form.Item name="username" label="账号" rules={[{required: true, message: "请输入账号"}]}>
|
||||||
|
<Input size="large" prefix={<UserOutlined/>} placeholder="用户名或手机号"/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{captchaEnabled ? (
|
||||||
|
<>
|
||||||
|
<Form.Item name="captchaCode" label="图形验证码" rules={[{required: true, message: "请输入图形验证码"}]}>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
prefix={<SafetyOutlined/>}
|
||||||
|
placeholder="请输入图形验证码"
|
||||||
|
addonAfter={
|
||||||
|
<Button type="text" icon={<ReloadOutlined/>} onClick={() => void loadCaptcha()}
|
||||||
|
aria-label="刷新验证码"/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{captcha?.imageBase64 ? (
|
||||||
|
<div className="captcha-preview captcha-preview--compact">
|
||||||
|
<img src={captcha.imageBase64} alt="验证码"/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Form.Item label="邮箱验证码" required>
|
||||||
|
<div className="login-inline-field">
|
||||||
|
<Form.Item name="code" noStyle rules={[{required: true, message: "请输入邮箱验证码"}]}>
|
||||||
|
<Input size="large" prefix={<MailOutlined/>} placeholder="邮箱验证码" maxLength={6}/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button size="large" onClick={() => void handleSendCode()} disabled={countdown > 0} loading={sending}>
|
||||||
|
{countdown > 0 ? `${countdown}s` : "发送"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
validateFirst
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "请输入新密码"},
|
||||||
|
{
|
||||||
|
validator: buildPasswordPolicyValidator(policy, () => form.getFieldValue("username")),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
extra={
|
||||||
|
policyHints.length > 0 ? (
|
||||||
|
<div className="login-form__hint-list">{policyHints.slice(0, 3).join(" · ")}</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input.Password size="large" prefix={<LockOutlined/>} placeholder="请输入新密码"
|
||||||
|
autoComplete="new-password"/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="confirmPassword"
|
||||||
|
label="确认新密码"
|
||||||
|
dependencies={["newPassword"]}
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "请再次输入新密码"},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue("newPassword") === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("两次输入的新密码不一致"));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password size="large" prefix={<LockOutlined/>} placeholder="请再次输入新密码"
|
||||||
|
autoComplete="new-password"/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space direction="vertical" size={10} style={{width: "100%"}}>
|
||||||
|
<Button type="primary" htmlType="submit" size="large" block loading={submitting}>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
<Button size="large" block onClick={() => navigate("/login")}>
|
||||||
|
返回登录
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
import type {PasswordPolicyPublic} from "@/api/auth";
|
||||||
|
|
||||||
|
const DEFAULT_SPECIAL_CHARS = "!@#$%^&*()_+-=[]{}|;:',.<>/?`~\"\\";
|
||||||
|
type UsernameResolver = string | undefined | (() => string | undefined);
|
||||||
|
|
||||||
|
export function buildPolicyHints(policy: PasswordPolicyPublic | null): string[] {
|
||||||
|
if (!policy || policy.enabled === false) {
|
||||||
|
return ["密码长度至少 6 位"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hints: string[] = [`长度 ${policy.minLength}-${policy.maxLength} 位`];
|
||||||
|
if (policy.requireUppercase) hints.push("包含大写字母");
|
||||||
|
if (policy.requireLowercase) hints.push("包含小写字母");
|
||||||
|
if (policy.requireDigit) hints.push("包含数字");
|
||||||
|
if (policy.requireSpecialChar) {
|
||||||
|
const chars = policy.specialCharSet?.trim() || DEFAULT_SPECIAL_CHARS;
|
||||||
|
hints.push(`包含特殊字符(如 ${chars})`);
|
||||||
|
}
|
||||||
|
if (policy.forbidUsernameContain) hints.push("不能包含登录名");
|
||||||
|
if (policy.forbidSequentialChars) hints.push("不能包含 3 位及以上连续字符");
|
||||||
|
if (policy.forbidRepeatedChars) hints.push("不能包含 3 位及以上重复字符");
|
||||||
|
if (policy.customRuleMessage) hints.push(policy.customRuleMessage);
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSequentialChars(value: string): boolean {
|
||||||
|
for (let i = 0; i + 2 < value.length; i += 1) {
|
||||||
|
const a = value.charCodeAt(i);
|
||||||
|
const b = value.charCodeAt(i + 1);
|
||||||
|
const c = value.charCodeAt(i + 2);
|
||||||
|
if (b - a === 1 && c - b === 1) return true;
|
||||||
|
if (a - b === 1 && b - c === 1) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRepeatedChars(value: string): boolean {
|
||||||
|
for (let i = 0; i + 2 < value.length; i += 1) {
|
||||||
|
if (value[i] === value[i + 1] && value[i + 1] === value[i + 2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUsername(username: UsernameResolver): string | undefined {
|
||||||
|
return typeof username === "function" ? username() : username;
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsConfiguredSpecialChar(password: string, specialCharSet: string): boolean {
|
||||||
|
for (const char of password) {
|
||||||
|
if (specialCharSet.includes(char)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePasswordAgainstPolicy(
|
||||||
|
policy: PasswordPolicyPublic | null,
|
||||||
|
password: string,
|
||||||
|
username?: string,
|
||||||
|
): string | null {
|
||||||
|
if (!password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!policy || policy.enabled === false) {
|
||||||
|
return password.length >= 6 ? null : "密码长度至少 6 位";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < policy.minLength) {
|
||||||
|
return `密码长度不能少于 ${policy.minLength} 位`;
|
||||||
|
}
|
||||||
|
if (password.length > policy.maxLength) {
|
||||||
|
return `密码长度不能超过 ${policy.maxLength} 位`;
|
||||||
|
}
|
||||||
|
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
|
||||||
|
return "密码需包含大写字母";
|
||||||
|
}
|
||||||
|
if (policy.requireLowercase && !/[a-z]/.test(password)) {
|
||||||
|
return "密码需包含小写字母";
|
||||||
|
}
|
||||||
|
if (policy.requireDigit && !/[0-9]/.test(password)) {
|
||||||
|
return "密码需包含数字";
|
||||||
|
}
|
||||||
|
if (policy.requireSpecialChar) {
|
||||||
|
const chars = policy.specialCharSet?.trim() || DEFAULT_SPECIAL_CHARS;
|
||||||
|
if (!containsConfiguredSpecialChar(password, chars)) {
|
||||||
|
return "密码需包含特殊字符";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (policy.forbidUsernameContain && username) {
|
||||||
|
const name = username.trim().toLowerCase();
|
||||||
|
if (name && password.toLowerCase().includes(name)) {
|
||||||
|
return "密码不能包含登录名";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (policy.forbidSequentialChars && hasSequentialChars(password)) {
|
||||||
|
return "密码不能包含 3 位及以上连续字符";
|
||||||
|
}
|
||||||
|
if (policy.forbidRepeatedChars && hasRepeatedChars(password)) {
|
||||||
|
return "密码不能包含 3 位及以上重复字符";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPasswordPolicyValidator(
|
||||||
|
policy: PasswordPolicyPublic | null,
|
||||||
|
username?: UsernameResolver,
|
||||||
|
) {
|
||||||
|
return async (_: unknown, value: string) => {
|
||||||
|
const error = validatePasswordAgainstPolicy(policy, value, resolveUsername(username));
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue