From 90ecaab63241b8d351ebaffa4faece8c609882b1 Mon Sep 17 00:00:00 2001
From: Charles7c <charles7c@126.com>
Date: Wed, 15 May 2024 23:14:51 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=AF=86?=
 =?UTF-8?q?=E7=A0=81=E7=AD=96=E7=95=A5=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/common/constant/CacheConstants.java |  5 ++
 .../admin/common/constant/RegexConstants.java | 19 +++--
 .../admin/common/constant/SysConstants.java   | 10 +++
 .../admin/auth/service/LoginService.java      |  4 +-
 .../auth/service/impl/LoginServiceImpl.java   | 63 ++++++++------
 .../system/enums/MessageTemplateEnum.java     |  2 +-
 .../admin/system/enums/OptionCodeEnum.java    | 63 --------------
 .../system/enums/PasswordPolicyEnum.java      | 77 +++++++++++++++++
 .../admin/system/model/req/DeptReq.java       |  2 +-
 .../admin/system/model/req/DictReq.java       |  4 +-
 .../admin/system/model/req/RoleReq.java       |  4 +-
 .../admin/system/model/req/StorageReq.java    |  2 +-
 .../model/req/UserBasicInfoUpdateReq.java     |  2 +-
 .../admin/system/model/req/UserReq.java       |  4 +-
 .../admin/system/service/OptionService.java   | 29 ++++---
 .../service/impl/OptionServiceImpl.java       | 27 +++---
 .../system/service/impl/UserServiceImpl.java  | 83 ++++++++-----------
 .../admin/webapi/auth/AuthController.java     | 29 +++----
 .../admin/webapi/system/UserController.java   |  4 +-
 .../changelog/mysql/continew-admin_data.sql   | 27 +++---
 .../changelog/mysql/continew-admin_table.sql  | 28 +++----
 .../postgresql/continew-admin_data.sql        | 27 +++---
 .../postgresql/continew-admin_table.sql       | 54 ++++++------
 23 files changed, 301 insertions(+), 268 deletions(-)
 delete mode 100644 continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java
 create mode 100644 continew-admin-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java

diff --git a/continew-admin-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java b/continew-admin-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java
index 79d869d6d..074dbaa98 100644
--- a/continew-admin-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java
+++ b/continew-admin-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java
@@ -66,6 +66,11 @@ public class CacheConstants {
      */
     public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER;
 
+    /**
+     * 用户密码错误次数缓存键前缀
+     */
+    public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER;
+
     private CacheConstants() {
     }
 }
diff --git a/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java b/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java
index f11cb5bd1..b73e5e569 100644
--- a/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java
+++ b/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java
@@ -25,27 +25,32 @@
 public class RegexConstants {
 
     /**
-     * 用户名正则(长度为 4 到 64 位,可以包含字母、数字,下划线,以字母开头)
+     * 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头)
      */
     public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$";
 
     /**
-     * 密码正则(长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字)
+     * 密码正则模板(密码长度为 min-max 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字)
      */
-    public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{6,32}$";
+    public static final String PASSWORD_TEMPLATE = "^(?=.*\\d)(?=.*[a-z]).{%s,%s}$";
 
     /**
-     * 密码正则严格版(长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符)
+     * 密码正则(密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字)
      */
-    public static final String PASSWORD_STRICT = "^\\S*(?=\\S{8,32})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*$";
+    public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{8,32}$";
 
     /**
-     * 通用编码正则(长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头)
+     * 特殊字符正则
+     */
+    public static final String SPECIAL_CHARACTER = "[-_`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\n|\\r|\\t";
+
+    /**
+     * 通用编码正则(长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头)
      */
     public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$";
 
     /**
-     * 通用名称正则(长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线)
+     * 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线)
      */
     public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$";
 
diff --git a/continew-admin-common/src/main/java/top/continew/admin/common/constant/SysConstants.java b/continew-admin-common/src/main/java/top/continew/admin/common/constant/SysConstants.java
index 3f82f3483..a07381ea4 100644
--- a/continew-admin-common/src/main/java/top/continew/admin/common/constant/SysConstants.java
+++ b/continew-admin-common/src/main/java/top/continew/admin/common/constant/SysConstants.java
@@ -24,6 +24,16 @@
  */
 public class SysConstants {
 
+    /**
+     * 否
+     */
+    public static final Integer NO = 0;
+
+    /**
+     * 是
+     */
+    public static final Integer YES = 1;
+
     /**
      * 管理员角色编码
      */
diff --git a/continew-admin-system/src/main/java/top/continew/admin/auth/service/LoginService.java b/continew-admin-system/src/main/java/top/continew/admin/auth/service/LoginService.java
index 8a46565d3..34a813594 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/auth/service/LoginService.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/auth/service/LoginService.java
@@ -16,6 +16,7 @@
 
 package top.continew.admin.auth.service;
 
+import jakarta.servlet.http.HttpServletRequest;
 import me.zhyd.oauth.model.AuthUser;
 import top.continew.admin.auth.model.resp.RouteResp;
 
@@ -34,9 +35,10 @@ public interface LoginService {
      *
      * @param username 用户名
      * @param password 密码
+     * @param request  请求对象
      * @return 令牌
      */
-    String accountLogin(String username, String password);
+    String accountLogin(String username, String password, HttpServletRequest request);
 
     /**
      * 手机号登录
diff --git a/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java
index 00be31504..220a6ac93 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java
@@ -21,7 +21,9 @@
 import cn.hutool.core.lang.tree.Tree;
 import cn.hutool.core.lang.tree.TreeNodeConfig;
 import cn.hutool.core.util.*;
+import cn.hutool.extra.servlet.JakartaServletUtil;
 import cn.hutool.json.JSONUtil;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import me.zhyd.oauth.model.AuthUser;
 import org.springframework.security.crypto.password.PasswordEncoder;
@@ -39,7 +41,7 @@
 import top.continew.admin.common.model.dto.LoginUser;
 import top.continew.admin.common.util.helper.LoginHelper;
 import top.continew.admin.system.enums.MessageTemplateEnum;
-import top.continew.admin.system.enums.OptionCodeEnum;
+import top.continew.admin.system.enums.PasswordPolicyEnum;
 import top.continew.admin.system.model.entity.DeptDO;
 import top.continew.admin.system.model.entity.RoleDO;
 import top.continew.admin.system.model.entity.UserDO;
@@ -80,36 +82,15 @@ public class LoginServiceImpl implements LoginService {
     private final OptionService optionService;
 
     @Override
-    public String accountLogin(String username, String password) {
+    public String accountLogin(String username, String password, HttpServletRequest request) {
         UserDO user = userService.getByUsername(username);
         boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword());
-        isPasswordLocked(username, isError);
+        this.checkUserLocked(username, request, isError);
         CheckUtils.throwIf(isError, "用户名或密码错误");
         this.checkUserStatus(user);
         return this.login(user);
     }
 
-    /**
-     * 检测用户是否被密码锁定
-     *
-     * @param username 用户名
-     */
-    private void isPasswordLocked(String username, boolean isError) {
-        // 不锁定账户
-        int maxErrorCount = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_ERROR_COUNT);
-        if (maxErrorCount <= 0) {
-            return;
-        }
-        int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES);
-        String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username;
-        long currentErrorCount = 0;
-        if (isError) {
-            currentErrorCount = RedisUtils.incr(key);
-            RedisUtils.expire(key, Duration.ofMinutes(lockMinutes));
-        }
-        CheckUtils.throwIf(currentErrorCount > maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes);
-    }
-
     @Override
     public String phoneLogin(String phone) {
         UserDO user = userService.getByPhone(phone);
@@ -229,6 +210,36 @@ private void checkUserStatus(UserDO user) {
         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
     }
 
+    /**
+     * 检测用户是否已被锁定
+     *
+     * @param username 用户名
+     * @param request  请求对象
+     * @param isError  是否登录错误
+     */
+    private void checkUserLocked(String username, HttpServletRequest request, boolean isError) {
+        // 不锁定
+        int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name());
+        if (maxErrorCount <= SysConstants.NO) {
+            return;
+        }
+        // 检测是否已被锁定
+        String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil
+            .getClientIP(request));
+        int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name());
+        Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0);
+        CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes);
+        // 登录成功清除计数
+        if (!isError) {
+            RedisUtils.delete(key);
+            return;
+        }
+        // 登录失败递增计数
+        currentErrorCount++;
+        RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
+        CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes);
+    }
+
     /**
      * 发送系统消息
      *
@@ -237,8 +248,8 @@ private void checkUserStatus(UserDO user) {
     private void sendSystemMsg(UserDO user) {
         MessageReq req = new MessageReq();
         MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
-        req.setTitle(StrUtil.format(socialRegister.getTitle(), projectProperties.getName()));
-        req.setContent(StrUtil.format(socialRegister.getContent(), user.getNickname()));
+        req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
+        req.setContent(socialRegister.getContent().formatted(user.getNickname()));
         req.setType(MessageTypeEnum.SYSTEM);
         messageService.add(req, CollUtil.toList(user.getId()));
     }
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/enums/MessageTemplateEnum.java b/continew-admin-system/src/main/java/top/continew/admin/system/enums/MessageTemplateEnum.java
index 3ceb34c2e..ef61af58e 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/enums/MessageTemplateEnum.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/enums/MessageTemplateEnum.java
@@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
     /**
      * 第三方登录
      */
-    SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。");
+    SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");
 
     private final String title;
     private final String content;
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java b/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java
deleted file mode 100644
index 4233799c5..000000000
--- a/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.admin.system.enums;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-/**
- * 参数枚举
- *
- * @author Kils
- * @since 2024/05/09 11:25
- */
-@Getter
-@RequiredArgsConstructor
-public enum OptionCodeEnum {
-
-    /**
-     * 密码是否允许包含正反序帐户名
-     */
-    PASSWORD_CONTAIN_NAME("password_contain_name", "密码不允许包含正反序帐户名"),
-    /**
-     * 密码错误锁定帐户次数
-     */
-    PASSWORD_ERROR_COUNT("password_error_count", "密码错误锁定帐户次数"),
-    /**
-     * 密码有效期
-     */
-    PASSWORD_EXPIRATION_DAYS("password_expiration_days", "密码有效期"),
-    /**
-     * 密码是否允许包含正反序帐户名
-     */
-    PASSWORD_LOCK_MINUTES("password_lock_minutes", "密码错误锁定帐户的时间"),
-    /**
-     * 密码最小长度
-     */
-    PASSWORD_MIN_LENGTH("password_min_length", "密码最小长度"),
-    /**
-     * 密码是否必须包含特殊字符
-     */
-    PASSWORD_SPECIAL_CHAR("password_special_char", "密码是否必须包含特殊字符"),
-    /**
-     * 修改密码最短间隔
-     */
-    PASSWORD_UPDATE_INTERVAL("password_update_interval", "修改密码最短间隔");
-
-    private final String value;
-    private final String description;
-}
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java b/continew-admin-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java
new file mode 100644
index 000000000..0764d64a3
--- /dev/null
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.admin.system.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import top.continew.admin.common.constant.SysConstants;
+
+/**
+ * 密码策略枚举
+ *
+ * @author Kils
+ * @author Charles7c
+ * @since 2024/5/9 11:25
+ */
+@Getter
+@RequiredArgsConstructor
+public enum PasswordPolicyEnum {
+
+    /**
+     * 登录密码错误锁定账号的次数
+     */
+    PASSWORD_ERROR_LOCK_COUNT(null, SysConstants.NO, 10),
+
+    /**
+     * 登录密码错误锁定账号的时间(min)
+     */
+    PASSWORD_ERROR_LOCK_MINUTES(null, 1, 1440),
+
+    /**
+     * 密码到期提前提示(天)
+     */
+    PASSWORD_EXPIRATION_WARNING_DAYS(null, SysConstants.NO, Integer.MAX_VALUE),
+
+    /**
+     * 密码有效期(天)
+     */
+    PASSWORD_EXPIRATION_DAYS(null, SysConstants.NO, 999),
+
+    /**
+     * 密码重复使用规则
+     */
+    PASSWORD_REUSE_POLICY("不允许使用最近 %s 次的历史密码", 3, 32),
+
+    /**
+     * 密码最小长度
+     */
+    PASSWORD_MIN_LENGTH("密码最小长度为 %s 个字符", 8, 32),
+
+    /**
+     * 密码是否允许包含正反序账号名
+     */
+    PASSWORD_ALLOW_CONTAIN_USERNAME("密码不允许包含正反序账号名", SysConstants.NO, SysConstants.YES),
+
+    /**
+     * 密码是否必须包含特殊字符
+     */
+    PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码必须包含特殊字符", SysConstants.NO, SysConstants.YES),;
+
+    private final String description;
+    private final Integer min;
+    private final Integer max;
+}
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DeptReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DeptReq.java
index cda0dd95a..3a93009d4 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DeptReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DeptReq.java
@@ -54,7 +54,7 @@ public class DeptReq extends BaseReq {
      */
     @Schema(description = "名称", example = "测试部")
     @NotBlank(message = "名称不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
+    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
     private String name;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DictReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DictReq.java
index d30faa09a..37a54f43b 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DictReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/DictReq.java
@@ -44,7 +44,7 @@ public class DictReq extends BaseReq {
      */
     @Schema(description = "名称", example = "公告类型")
     @NotBlank(message = "名称不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
+    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
     private String name;
 
     /**
@@ -52,7 +52,7 @@ public class DictReq extends BaseReq {
      */
     @Schema(description = "编码", example = "notice_type")
     @NotBlank(message = "编码不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
+    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
     private String code;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/RoleReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/RoleReq.java
index 41c395155..d23ede21f 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/RoleReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/RoleReq.java
@@ -48,7 +48,7 @@ public class RoleReq extends BaseReq {
      */
     @Schema(description = "名称", example = "测试人员")
     @NotBlank(message = "名称不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
+    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
     private String name;
 
     /**
@@ -56,7 +56,7 @@ public class RoleReq extends BaseReq {
      */
     @Schema(description = "编码", example = "test")
     @NotBlank(message = "编码不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
+    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
     private String code;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
index 9e6854385..2dca57b5d 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
@@ -55,7 +55,7 @@ public class StorageReq extends BaseReq {
      */
     @Schema(description = "编码", example = "local")
     @NotBlank(message = "编码不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
+    @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
     private String code;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserBasicInfoUpdateReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserBasicInfoUpdateReq.java
index c026269ba..318a43141 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserBasicInfoUpdateReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserBasicInfoUpdateReq.java
@@ -45,7 +45,7 @@ public class UserBasicInfoUpdateReq implements Serializable {
      */
     @Schema(description = "昵称", example = "张三")
     @NotBlank(message = "昵称不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
+    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
     private String nickname;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserReq.java b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserReq.java
index 602af6efc..509cfb877 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserReq.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/model/req/UserReq.java
@@ -51,7 +51,7 @@ public class UserReq extends BaseReq {
      */
     @Schema(description = "用户名", example = "zhangsan")
     @NotBlank(message = "用户名不能为空")
-    @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4 到 64 位,可以包含字母、数字,下划线,以字母开头")
+    @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
     private String username;
 
     /**
@@ -59,7 +59,7 @@ public class UserReq extends BaseReq {
      */
     @Schema(description = "昵称", example = "张三")
     @NotBlank(message = "昵称不能为空")
-    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
+    @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
     private String nickname;
 
     /**
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java
index fcc1c47df..a0aec51dd 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java
@@ -16,15 +16,14 @@
 
 package top.continew.admin.system.service;
 
-import java.util.List;
-import java.util.function.Function;
-
-import top.continew.admin.system.enums.OptionCodeEnum;
 import top.continew.admin.system.model.query.OptionQuery;
 import top.continew.admin.system.model.req.OptionReq;
 import top.continew.admin.system.model.req.OptionResetValueReq;
 import top.continew.admin.system.model.resp.OptionResp;
 
+import java.util.List;
+import java.util.function.Function;
+
 /**
  * 参数业务接口
  *
@@ -56,19 +55,19 @@ public interface OptionService {
     void resetValue(OptionResetValueReq req);
 
     /**
-     * 根据code获取int参数值
-     * 
-     * @param code code
-     * @return 参数值
+     * 根据编码查询参数值
+     *
+     * @param code 编码
+     * @return 参数值(自动转换为 int 类型)
      */
-    int getValueByCode2Int(OptionCodeEnum code);
+    int getValueByCode2Int(String code);
 
     /**
-     * 根据code获取参数值
-     * 
-     * @param code   code
-     * @param mapper 类型转换 ex:value -> Integer.parseInt(value)
+     * 根据编码查询参数值
+     *
+     * @param code   编码
+     * @param mapper 转换方法 e.g.:value -> Integer.parseInt(value)
      * @return 参数值
      */
-    <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper);
-}
\ No newline at end of file
+    <T> T getValueByCode(String code, Function<String, T> mapper);
+}
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java
index a9304f9b6..fef23e5d5 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java
@@ -17,14 +17,10 @@
 package top.continew.admin.system.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import top.continew.admin.common.constant.CacheConstants;
-import top.continew.admin.system.enums.OptionCodeEnum;
 import top.continew.admin.system.mapper.OptionMapper;
 import top.continew.admin.system.model.entity.OptionDO;
 import top.continew.admin.system.model.query.OptionQuery;
@@ -70,25 +66,24 @@ public void resetValue(OptionResetValueReq req) {
     }
 
     @Override
-    public int getValueByCode2Int(OptionCodeEnum code) {
+    public int getValueByCode2Int(String code) {
         return this.getValueByCode(code, Integer::parseInt);
     }
 
     @Override
-    public <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper) {
-        String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue());
+    public <T> T getValueByCode(String code, Function<String, T> mapper) {
+        String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code);
         if (StrUtil.isNotBlank(value)) {
             return mapper.apply(value);
         }
-        LambdaQueryWrapper<OptionDO> queryWrapper = Wrappers.<OptionDO>lambdaQuery()
-            .eq(OptionDO::getCode, code.getValue())
-            .select(OptionDO::getValue, OptionDO::getDefaultValue);
-        OptionDO optionDO = baseMapper.selectOne(queryWrapper);
-        CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code);
-        value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue());
-        CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code);
-        RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value);
+        OptionDO option = baseMapper.lambdaQuery()
+            .eq(OptionDO::getCode, code)
+            .select(OptionDO::getValue, OptionDO::getDefaultValue)
+            .one();
+        CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code);
+        value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue());
+        CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code);
+        RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value);
         return mapper.apply(value);
     }
-
 }
\ No newline at end of file
diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java
index 6c2c371fa..cfd72d0b2 100644
--- a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java
+++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java
@@ -18,7 +18,6 @@
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.io.file.FileNameUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.ReUtil;
@@ -41,6 +40,7 @@
 import top.continew.admin.auth.service.OnlineUserService;
 import top.continew.admin.common.constant.CacheConstants;
 import top.continew.admin.common.constant.RegexConstants;
+import top.continew.admin.common.constant.SysConstants;
 import top.continew.admin.common.enums.DisEnableStatusEnum;
 import top.continew.admin.common.util.helper.LoginHelper;
 import top.continew.admin.system.mapper.UserMapper;
@@ -63,10 +63,13 @@
 import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;
 
 import java.time.LocalDateTime;
-import java.util.*;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
-import static top.continew.admin.system.enums.OptionCodeEnum.*;
+import static top.continew.admin.system.enums.PasswordPolicyEnum.*;
 
 /**
  * 用户业务实现
@@ -197,7 +200,7 @@ public void updatePassword(String oldPassword, String newPassword, Long id) {
             CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误");
         }
         // 校验密码合法性
-        checkPassword(newPassword, user);
+        this.checkPassword(newPassword, user);
         // 更新密码和密码重置时间
         user.setPassword(newPassword);
         user.setPwdResetTime(LocalDateTime.now());
@@ -205,52 +208,11 @@ public void updatePassword(String oldPassword, String newPassword, Long id) {
         onlineUserService.cleanByUserId(user.getId());
     }
 
-    /**
-     * 检测修改密码合法性
-     *
-     * @param password 密码
-     * @param user     用户
-     */
-    private void checkPassword(String password, UserDO user) {
-        // 密码最小长度
-        int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH);
-        ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH
-            .getDescription() + "为 {}", passwordMinLength);
-        // 密码是否允许包含正反序用户名
-        int passwordContainName = optionService.getValueByCode2Int(PASSWORD_CONTAIN_NAME);
-        if (passwordContainName == 1) {
-            String username = user.getUsername();
-            ValidationUtils.throwIf(StrUtil.containsIgnoreCase(password, username) || StrUtil
-                .containsIgnoreCase(password, StrUtil.reverse(username)), PASSWORD_CONTAIN_NAME.getDescription());
-        }
-        // 密码是否必须包含特殊字符
-        int passwordSpecialChar = optionService.getValueByCode2Int(PASSWORD_SPECIAL_CHAR);
-        String match = RegexConstants.PASSWORD;
-        String desc = "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字";
-        if (passwordSpecialChar == 1) {
-            match = RegexConstants.PASSWORD_STRICT;
-            desc = "密码长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符";
-        }
-        ValidationUtils.throwIf(!ReUtil.isMatch(match, password), desc);
-        // 密码修改间隔
-        if (ObjectUtil.isNull(user.getPwdResetTime())) {
-            return;
-        }
-        int passwordUpdateInterval = optionService.getValueByCode2Int(PASSWORD_UPDATE_INTERVAL);
-        if (passwordUpdateInterval <= 0) {
-            return;
-        }
-        LocalDateTime lastResetTime = user.getPwdResetTime();
-        LocalDateTime limitUpdateTime = lastResetTime.plusMinutes(passwordUpdateInterval);
-        ValidationUtils.throwIf(LocalDateTime.now().isBefore(limitUpdateTime), "上次修改于:{},下次可修改时间:{}", LocalDateTimeUtil
-            .formatNormal(lastResetTime), LocalDateTimeUtil.formatNormal(limitUpdateTime));
-    }
-
     @Override
     public Boolean isPasswordExpired(LocalDateTime pwdResetTime) {
         // 永久有效
-        int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS);
-        if (passwordExpirationDays <= 0) {
+        int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name());
+        if (passwordExpirationDays <= SysConstants.NO) {
             return false;
         }
         // 初始密码也提示修改
@@ -375,6 +337,33 @@ protected void afterAdd(UserReq req, UserDO user) {
         userRoleService.add(req.getRoleIds(), userId);
     }
 
+    /**
+     * 检测密码合法性
+     *
+     * @param password 密码
+     * @param user     用户信息
+     */
+    private void checkPassword(String password, UserDO user) {
+        // 密码最小长度
+        int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH.name());
+        ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH.getDescription()
+            .formatted(passwordMinLength));
+        // 密码是否允许包含正反序账号名
+        int passwordAllowContainUsername = optionService.getValueByCode2Int(PASSWORD_ALLOW_CONTAIN_USERNAME.name());
+        if (passwordAllowContainUsername == SysConstants.NO) {
+            String username = user.getUsername();
+            ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil
+                .reverse(username)), PASSWORD_ALLOW_CONTAIN_USERNAME.getDescription());
+        }
+        int passwordMaxLength = PASSWORD_MIN_LENGTH.getMax();
+        ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE
+            .formatted(passwordMinLength, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", passwordMinLength, passwordMaxLength);
+        // 密码是否必须包含特殊字符
+        int passwordContainSpecialChar = optionService.getValueByCode2Int(PASSWORD_CONTAIN_SPECIAL_CHARACTERS.name());
+        ValidationUtils.throwIf(passwordContainSpecialChar == SysConstants.YES && !ReUtil
+            .isMatch(RegexConstants.SPECIAL_CHARACTER, password), PASSWORD_CONTAIN_SPECIAL_CHARACTERS.getDescription());
+    }
+
     /**
      * 名称是否存在
      *
diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/auth/AuthController.java b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/auth/AuthController.java
index 631f46e4f..1076c0db1 100644
--- a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/auth/AuthController.java
+++ b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/auth/AuthController.java
@@ -23,6 +23,7 @@
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.enums.ParameterIn;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -68,7 +69,7 @@ public class AuthController {
     @SaIgnore
     @Operation(summary = "账号登录", description = "根据账号和密码进行登录认证")
     @PostMapping("/account")
-    public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq) {
+    public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq, HttpServletRequest request) {
         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid();
         String captcha = RedisUtils.get(captchaKey);
         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
@@ -77,35 +78,35 @@ public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginRe
         // 用户登录
         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword()));
         ValidationUtils.throwIfBlank(rawPassword, "密码解密失败");
-        String token = loginService.accountLogin(loginReq.getUsername(), rawPassword);
+        String token = loginService.accountLogin(loginReq.getUsername(), rawPassword, request);
         return R.ok(LoginResp.builder().token(token).build());
     }
 
     @SaIgnore
-    @Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证")
-    @PostMapping("/email")
-    public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) {
-        String email = loginReq.getEmail();
-        String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
+    @Operation(summary = "手机号登录", description = "根据手机号和验证码进行登录认证")
+    @PostMapping("/phone")
+    public R<LoginResp> phoneLogin(@Validated @RequestBody PhoneLoginReq loginReq) {
+        String phone = loginReq.getPhone();
+        String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
         String captcha = RedisUtils.get(captchaKey);
         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
         ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
         RedisUtils.delete(captchaKey);
-        String token = loginService.emailLogin(email);
+        String token = loginService.phoneLogin(phone);
         return R.ok(LoginResp.builder().token(token).build());
     }
 
     @SaIgnore
-    @Operation(summary = "手机号登录", description = "根据手机号和验证码进行登录认证")
-    @PostMapping("/phone")
-    public R<LoginResp> phoneLogin(@Validated @RequestBody PhoneLoginReq loginReq) {
-        String phone = loginReq.getPhone();
-        String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
+    @Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证")
+    @PostMapping("/email")
+    public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) {
+        String email = loginReq.getEmail();
+        String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
         String captcha = RedisUtils.get(captchaKey);
         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
         ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
         RedisUtils.delete(captchaKey);
-        String token = loginService.phoneLogin(phone);
+        String token = loginService.emailLogin(email);
         return R.ok(LoginResp.builder().token(token).build());
     }
 
diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserController.java b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserController.java
index 250396a1b..092290094 100644
--- a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserController.java
+++ b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserController.java
@@ -61,7 +61,7 @@ public R<Long> add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody UserReq
         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
         ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
         ValidationUtils.throwIf(!ReUtil
-            .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字");
+            .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
         req.setPassword(rawPassword);
         return super.add(req);
     }
@@ -74,7 +74,7 @@ public R<Void> resetPassword(@Validated @RequestBody UserPasswordResetReq req, @
         String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword()));
         ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败");
         ValidationUtils.throwIf(!ReUtil
-            .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字");
+            .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
         req.setNewPassword(rawNewPassword);
         baseService.resetPassword(req, id);
         return R.ok("重置密码成功");
diff --git a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql
index e95fe2f7b..d09ab452f 100644
--- a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql
+++ b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql
@@ -1,6 +1,6 @@
 -- liquibase formatted sql
 
--- changeset Charles7c:2.5.0
+-- changeset Charles7c:1
 -- comment 初始化表数据
 -- 初始化默认菜单
 INSERT INTO `sys_menu`
@@ -109,19 +109,20 @@ VALUES
 INSERT INTO `sys_option`
 (`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`)
 VALUES
-('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
-('版权信息', 'site_copyright', NULL,
- 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>',
+('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
+('版权信息', 'SITE_COPYRIGHT', NULL,
+ 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
  '用于显示登录页面的底部版权信息。', NULL, NULL),
-('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
-('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
-('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL),
-('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL),
-('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL),
-('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL),
-('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL),
-('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL),
-('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL);
+('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
+('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
+('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL),
+('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL),
+('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL),
+('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL),
+('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
+('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
+('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
+('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
 
 -- 初始化默认字典
 INSERT INTO `sys_dict`
diff --git a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_table.sql b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_table.sql
index e9233e3b1..523a55dad 100644
--- a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_table.sql
+++ b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_table.sql
@@ -1,6 +1,6 @@
 -- liquibase formatted sql
 
--- changeset Charles7c:2.5.0
+-- changeset Charles7c:1
 -- comment 初始化表结构
 CREATE TABLE IF NOT EXISTS `sys_menu` (
     `id`          bigint(20)   NOT NULL                    COMMENT 'ID',
@@ -256,19 +256,19 @@ CREATE TABLE IF NOT EXISTS `sys_storage` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表';
 
 CREATE TABLE IF NOT EXISTS `sys_file` (
-    `id`            bigint(20)   NOT NULL                    COMMENT 'ID',
-    `name`          varchar(255) NOT NULL                    COMMENT '名称',
-    `size`          bigint(20)   NOT NULL                    COMMENT '大小(字节)',
-    `url`           varchar(512) NOT NULL                    COMMENT 'URL',
-    `extension`     varchar(100) DEFAULT NULL                COMMENT '扩展名',
-    `type`          tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)',
-    `storage_id`    bigint(20)   NOT NULL                    COMMENT '存储ID',
-    `create_user`   bigint(20)   NOT NULL                    COMMENT '创建人',
-    `create_time`   datetime     NOT NULL                    COMMENT '创建时间',
-    `update_user`   bigint(20)   NOT NULL                    COMMENT '修改人',
-    `update_time`   datetime     NOT NULL                    COMMENT '修改时间',
-    `thumbnail_size` bigint(20) NULL DEFAULT NULL COMMENT '缩略图大小(字节)',
-    `thumbnail_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图URL',
+    `id`             bigint(20)   NOT NULL                    COMMENT 'ID',
+    `name`           varchar(255) NOT NULL                    COMMENT '名称',
+    `size`           bigint(20)   NOT NULL                    COMMENT '大小(字节)',
+    `url`            varchar(512) NOT NULL                    COMMENT 'URL',
+    `extension`      varchar(100) DEFAULT NULL                COMMENT '扩展名',
+    `thumbnail_size` bigint(20)   DEFAULT NULL                COMMENT '缩略图大小(字节)',
+    `thumbnail_url`  varchar(512) DEFAULT NULL                COMMENT '缩略图URL',
+    `type`           tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)',
+    `storage_id`     bigint(20)   NOT NULL                    COMMENT '存储ID',
+    `create_user`    bigint(20)   NOT NULL                    COMMENT '创建人',
+    `create_time`    datetime     NOT NULL                    COMMENT '创建时间',
+    `update_user`    bigint(20)   NOT NULL                    COMMENT '修改人',
+    `update_time`    datetime     NOT NULL                    COMMENT '修改时间',
     PRIMARY KEY (`id`) USING BTREE,
     INDEX `idx_url`(`url`) USING BTREE,
     INDEX `idx_type`(`type`) USING BTREE,
diff --git a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql
index 1acac866c..79d04e2db 100644
--- a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql
+++ b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql
@@ -1,6 +1,6 @@
 -- liquibase formatted sql
 
--- changeset Charles7c:2.5.0
+-- changeset Charles7c:1
 -- comment 初始化表数据
 -- 初始化默认菜单
 INSERT INTO "sys_menu"
@@ -109,19 +109,20 @@ VALUES
 INSERT INTO "sys_option"
 ("name", "code", "value", "default_value", "description", "update_user", "update_time")
 VALUES
-('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
-('版权信息', 'site_copyright', NULL,
- 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>',
+('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
+('版权信息', 'SITE_COPYRIGHT', NULL,
+ 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
  '用于显示登录页面的底部版权信息。', NULL, NULL),
-('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
-('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
-('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL),
-('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL),
-('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL),
-('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL),
-('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL),
-('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL),
-('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL);
+('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
+('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
+('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL),
+('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL),
+('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL),
+('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL),
+('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
+('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
+('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
+('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
 
 -- 初始化默认字典
 INSERT INTO "sys_dict"
diff --git a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_table.sql b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_table.sql
index 388eebd9a..72ddcc536 100644
--- a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_table.sql
+++ b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_table.sql
@@ -1,6 +1,6 @@
 -- liquibase formatted sql
 
--- changeset Charles7c:2.5.0
+-- changeset Charles7c:1
 -- comment 初始化表结构
 CREATE TABLE IF NOT EXISTS "sys_menu" (
     "id"          int8         NOT NULL,
@@ -430,39 +430,39 @@ COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间';
 COMMENT ON TABLE  "sys_storage"               IS '存储表';
 
 CREATE TABLE IF NOT EXISTS "sys_file" (
-    "id"            int8         NOT NULL,
-    "name"          varchar(255) NOT NULL,
-    "size"          int8         NOT NULL,
-    "url"           varchar(512) NOT NULL,
-    "extension"     varchar(100) DEFAULT NULL,
-    "type"          int2         NOT NULL DEFAULT 1,
-    "storage_id"    int8         NOT NULL,
-    "create_user"   int8         NOT NULL,
-    "create_time"   timestamp    NOT NULL,
-    "update_user"   int8         NOT NULL,
-    "update_time"   timestamp    NOT NULL,
-    "thumbnail_size"   int8    DEFAULT NULL,
-    "thumbnail_url"   varchar(512)    DEFAULT NULL,
+    "id"             int8         NOT NULL,
+    "name"           varchar(255) NOT NULL,
+    "size"           int8         NOT NULL,
+    "url"            varchar(512) NOT NULL,
+    "extension"      varchar(100) DEFAULT NULL,
+    "thumbnail_size" int8         DEFAULT NULL,
+    "thumbnail_url"  varchar(512) DEFAULT NULL,
+    "type"           int2         NOT NULL DEFAULT 1,
+    "storage_id"     int8         NOT NULL,
+    "create_user"    int8         NOT NULL,
+    "create_time"    timestamp    NOT NULL,
+    "update_user"    int8         NOT NULL,
+    "update_time"    timestamp    NOT NULL,
     PRIMARY KEY ("id")
 );
 CREATE INDEX "idx_file_url"  ON "sys_file" ("url");
 CREATE INDEX "idx_file_type" ON "sys_file" ("type");
 CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user");
 CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user");
-COMMENT ON COLUMN "sys_file"."id"          IS 'ID';
-COMMENT ON COLUMN "sys_file"."name"        IS '名称';
-COMMENT ON COLUMN "sys_file"."size"        IS '大小(字节)';
-COMMENT ON COLUMN "sys_file"."url"         IS 'URL';
-COMMENT ON COLUMN "sys_file"."extension"   IS '扩展名';
-COMMENT ON COLUMN "sys_file"."type"        IS '类型(1:其他;2:图片;3:文档;4:视频;5:音频)';
-COMMENT ON COLUMN "sys_file"."storage_id"  IS '存储ID';
-COMMENT ON COLUMN "sys_file"."create_user" IS '创建人';
-COMMENT ON COLUMN "sys_file"."create_time" IS '创建时间';
-COMMENT ON COLUMN "sys_file"."update_user" IS '修改人';
-COMMENT ON COLUMN "sys_file"."update_time" IS '修改时间';
+COMMENT ON COLUMN "sys_file"."id"             IS 'ID';
+COMMENT ON COLUMN "sys_file"."name"           IS '名称';
+COMMENT ON COLUMN "sys_file"."size"           IS '大小(字节)';
+COMMENT ON COLUMN "sys_file"."url"            IS 'URL';
+COMMENT ON COLUMN "sys_file"."extension"      IS '扩展名';
 COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)';
-COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL';
-COMMENT ON TABLE  "sys_file"               IS '文件表';
+COMMENT ON COLUMN "sys_file"."thumbnail_url"  IS '缩略图URL';
+COMMENT ON COLUMN "sys_file"."type"           IS '类型(1:其他;2:图片;3:文档;4:视频;5:音频)';
+COMMENT ON COLUMN "sys_file"."storage_id"     IS '存储ID';
+COMMENT ON COLUMN "sys_file"."create_user"    IS '创建人';
+COMMENT ON COLUMN "sys_file"."create_time"    IS '创建时间';
+COMMENT ON COLUMN "sys_file"."update_user"    IS '修改人';
+COMMENT ON COLUMN "sys_file"."update_time"    IS '修改时间';
+COMMENT ON TABLE  "sys_file"                  IS '文件表';
 
 CREATE TABLE IF NOT EXISTS "gen_config" (
     "table_name"    varchar(64)  NOT NULL,