From 9a496d3055c5d9e49501abb776b57a92d23106a9 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Mon, 22 Jun 2026 00:54:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=86=E7=A0=81=E5=BC=BA=E5=BA=A6?= =?UTF-8?q?=E5=A4=9A=E7=BA=A7=E6=A0=A1=E9=AA=8C=20=E2=80=94=20low/medium/h?= =?UTF-8?q?igh/very=5Fhigh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - site_settings 新增 password.strength 配置项 - 四级:low(len>=8) / medium(两类) / high(三类+无连续) / very_high(四类+无连续) - 连续字符检查:重复/升序/降序/键盘水平/键盘垂直序列 - 注册和改密接口统一读取配置 - 前端 register.js 动态校验 + 管理后台安全设置区 --- internal/config/site_settings.go | 10 ++ internal/controller/auth_controller.go | 1 + internal/service/auth_password.go | 165 +++++++++++++++++- internal/service/auth_service.go | 2 +- .../MetaLab-2026/html/auth/register.html | 2 +- templates/MetaLab-2026/static/js/register.js | 21 ++- templates/admin/html/site-settings/index.html | 22 +++ templates/admin/static/js/site-settings.js | 13 +- 8 files changed, 225 insertions(+), 11 deletions(-) diff --git a/internal/config/site_settings.go b/internal/config/site_settings.go index d84124f..a6bdbff 100644 --- a/internal/config/site_settings.go +++ b/internal/config/site_settings.go @@ -166,6 +166,16 @@ func (s *SiteSettings) ForceGuidelines() bool { return s.GetBool("registration.force_guidelines", true) } +// PasswordStrength 密码强度级别 +func (s *SiteSettings) PasswordStrength() string { + level := s.Get("password.strength", "medium") + switch level { + case "low", "medium", "high", "very_high": + return level + } + return "medium" +} + // GuidelinesTimer 强制阅读倒计时秒数 func (s *SiteSettings) GuidelinesTimer() int { val := s.Get("registration.guidelines_timer", "60") diff --git a/internal/controller/auth_controller.go b/internal/controller/auth_controller.go index 7104f78..9e6ad90 100644 --- a/internal/controller/auth_controller.go +++ b/internal/controller/auth_controller.go @@ -57,6 +57,7 @@ func (ac *AuthController) RegisterPage(c *gin.Context) { "ExtraCSS": "/static/css/auth.css", "Guidelines": guidelines, "RegistrationEnabled": ac.authService.IsRegistrationEnabled(), + "PasswordStrength": ac.siteSettings.PasswordStrength(), "ShowGuidelinesModal": ac.siteSettings.ShowGuidelinesModal(), "ForceGuidelines": ac.siteSettings.ForceGuidelines(), "GuidelinesTimer": ac.siteSettings.GuidelinesTimer(), diff --git a/internal/service/auth_password.go b/internal/service/auth_password.go index bb8aad6..6e582f4 100644 --- a/internal/service/auth_password.go +++ b/internal/service/auth_password.go @@ -2,7 +2,9 @@ package service import ( "regexp" + "strings" "time" + "unicode" "metazone.cc/mce/internal/common" "metazone.cc/mce/internal/model" @@ -14,6 +16,19 @@ import ( var pwLower = regexp.MustCompile(`[a-z]`) var pwUpper = regexp.MustCompile(`[A-Z]`) var pwDigit = regexp.MustCompile(`\d`) +var pwSymbol = regexp.MustCompile(`[^a-zA-Z0-9]`) + +// 键盘水平序列(QWERTY 布局) +var keyboardHoriz = []string{ + "qwertyuiop", "asdfghjkl", "zxcvbnm", + "poiuytrewq", "lkjhgfdsa", "mnbvcxz", +} + +// 键盘垂直序列 +var keyboardVert = []string{ + "qaz", "wsx", "edc", "rfv", "tgb", "yhn", "ujm", "ik", "ol", "p", + "zaq", "xsw", "cde", "vfr", "bgt", "nhy", "mju", "ki", "lo", +} // dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对 var dummyHash []byte @@ -26,14 +41,156 @@ func init() { dummyHash = hash } -// validatePassword 密码强度:至少 8 位 + 包含大写字母 + 包含小写字母 + 包含数字 -func validatePassword(pw string) error { - if len(pw) < 8 || !pwLower.MatchString(pw) || !pwUpper.MatchString(pw) || !pwDigit.MatchString(pw) { +// validatePassword 多级密码强度校验 +func validatePassword(pw string, level string) error { + if level == "" { + level = "medium" + } + if len(pw) < 8 { return common.ErrWeakPassword } + + switch level { + case "low": + return nil + case "medium": + if !hasAtLeastN(pw, 2) { + return common.ErrWeakPassword + } + case "high": + if !hasAtLeastN(pw, 3) { + return common.ErrWeakPassword + } + if isWeakSequence(pw) { + return common.ErrWeakPassword + } + case "very_high": + if !hasAtLeastN(pw, 4) { + return common.ErrWeakPassword + } + if isWeakSequence(pw) { + return common.ErrWeakPassword + } + } return nil } +// hasAtLeastN 检查密码包含至少 n 类字符(小写/大写/数字/符号) +func hasAtLeastN(pw string, n int) bool { + count := 0 + if pwLower.MatchString(pw) { + count++ + } + if pwUpper.MatchString(pw) { + count++ + } + if pwDigit.MatchString(pw) { + count++ + } + if pwSymbol.MatchString(pw) { + count++ + } + return count >= n +} + +// isWeakSequence 检查连续字符(≥3 重复/排序/键盘序列) +func isWeakSequence(pw string) bool { + runes := []rune(strings.ToLower(pw)) + if len(runes) < 3 { + return false + } + + // 重复字符 aaa 111 + repeatCount := 1 + // 升序/降序计数 + ascCount, descCount := 1, 1 + + for i := 1; i < len(runes); i++ { + // 重复 + if runes[i] == runes[i-1] { + repeatCount++ + if repeatCount >= 3 { + return true + } + } else { + repeatCount = 1 + } + + // 升序 abc / 123 + if runes[i] == runes[i-1]+1 { + ascCount++ + if ascCount >= 3 { + return true + } + } else { + ascCount = 1 + } + + // 降序 cba / 321 + if runes[i] == runes[i-1]-1 { + descCount++ + if descCount >= 3 { + return true + } + } else { + descCount = 1 + } + } + + // 键盘序列 + lower := strings.ToLower(pw) + for _, seq := range keyboardHoriz { + if len(seq) >= 3 { + for i := 0; i <= len(seq)-3; i++ { + if strings.Contains(lower, seq[i:i+3]) { + return true + } + } + } + } + for _, seq := range keyboardVert { + if len(seq) >= 3 && strings.Contains(lower, seq) { + return true + } + } + + return false +} + +// PasswordStrengthLevel 获取当前密码强度级别 +func PasswordStrengthLevel(strengthStr string) string { + switch strengthStr { + case "low", "medium", "high", "very_high": + return strengthStr + } + return "medium" +} + +// PasswordStrengthHint 前端校验提示文案 +func PasswordStrengthHint(level string) string { + switch level { + case "low": + return "密码至少 8 位" + case "medium": + return "密码至少 8 位,需包含字母、数字或符号中的两类" + case "high": + return "密码至少 8 位,需包含大小写字母、数字和符号中的至少三类,且不含连续字符" + case "very_high": + return "密码至少 8 位,需包含大小写字母、数字和符号全部四类,且不含连续字符" + } + return "密码至少 8 位" +} + +// hasUnicode 检查是否包含非 ASCII 字符 +func hasUnicode(s string) bool { + for _, r := range s { + if r > unicode.MaxASCII { + return true + } + } + return false +} + // ChangePassword 修改密码 // 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 删除所有 session(强制重新登录) func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error { @@ -46,7 +203,7 @@ func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword s return common.ErrIncorrectPassword } - if err := validatePassword(newPassword); err != nil { + if err := validatePassword(newPassword, s.siteSettings.PasswordStrength()); err != nil { return err } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 54fb365..61302f1 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -48,7 +48,7 @@ func (s *AuthService) Register(req model.RegisterRequest, regIP string) (*model. } // 2. 密码强度 - if err := validatePassword(req.Password); err != nil { + if err := validatePassword(req.Password, s.siteSettings.PasswordStrength()); err != nil { return nil, err } diff --git a/templates/MetaLab-2026/html/auth/register.html b/templates/MetaLab-2026/html/auth/register.html index ca06536..1baeadb 100644 --- a/templates/MetaLab-2026/html/auth/register.html +++ b/templates/MetaLab-2026/html/auth/register.html @@ -23,7 +23,7 @@
- + 密码至少 8 位,需包含大小写字母与数字
diff --git a/templates/MetaLab-2026/static/js/register.js b/templates/MetaLab-2026/static/js/register.js index 5afc7c6..4cedb1c 100644 --- a/templates/MetaLab-2026/static/js/register.js +++ b/templates/MetaLab-2026/static/js/register.js @@ -23,6 +23,10 @@ var toast = document.getElementById('toast'); var emailInput = document.getElementById('email'); var passwordInput = document.getElementById('password'); + // 密码强度提示从 data 属性读取 + var pwHint = passwordInput.getAttribute('placeholder') || '密码至少 8 位'; + var pwMinLen = 8; + var pwLevel = passwordInput.getAttribute('data-pw-level') || 'medium'; var confirmPasswordInput = document.getElementById('confirmPassword'); var rememberMeCheckbox = document.getElementById('rememberMe'); var guidelinesAgreedCheckbox = document.getElementById('guidelinesAgreed'); @@ -178,7 +182,7 @@ var password = passwordInput.value; var confirm = confirmPasswordInput.value; var emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); - var passwordValid = password.length >= 8 && /[a-z]/.test(password) && /[A-Z]/.test(password) && /\d/.test(password); + var passwordValid = checkPWStrength(password); var confirmValid = passwordValid && confirm === password; submitBtn.disabled = !(emailValid && passwordValid && confirmValid); } @@ -195,9 +199,22 @@ checkFormValidity(); }); + function checkPWStrength(pw) { + if (pw.length < pwMinLen) return false; + var cats = 0; + if (/[a-z]/.test(pw)) cats++; + if (/[A-Z]/.test(pw)) cats++; + if (/\d/.test(pw)) cats++; + if (/[^a-zA-Z0-9]/.test(pw)) cats++; + if (pwLevel === 'low') return true; + if (pwLevel === 'medium') return cats >= 2; + if (pwLevel === 'high' || pwLevel === 'very_high') return cats >= (pwLevel === 'very_high' ? 4 : 3); + return cats >= 2; + } + passwordInput.addEventListener('input', function () { var v = passwordInput.value; - if (v && (v.length < 8 || !/[a-z]/.test(v) || !/[A-Z]/.test(v) || !/\d/.test(v))) { + if (v && !checkPWStrength(v)) { passwordInput.classList.add('error'); passwordError.style.display = 'block'; } else { diff --git a/templates/admin/html/site-settings/index.html b/templates/admin/html/site-settings/index.html index ed37289..5db9627 100644 --- a/templates/admin/html/site-settings/index.html +++ b/templates/admin/html/site-settings/index.html @@ -165,6 +165,28 @@
+ +
+
安全设置
+
+
+
+ 密码强度 + 注册和修改密码时的最低强度要求 +
+
+ + +
+
+
+
+
注册设置
diff --git a/templates/admin/static/js/site-settings.js b/templates/admin/static/js/site-settings.js index 55d7d7b..a364047 100644 --- a/templates/admin/static/js/site-settings.js +++ b/templates/admin/static/js/site-settings.js @@ -9,6 +9,8 @@ const TOGGLE_KEYS = [ { id: 'maintenance-enabled', key: 'maintenance.enabled' }, { id: 'registration-enabled', key: 'registration.enabled' }, + { id: 'registration-show-guidelines', key: 'registration.show_guidelines' }, + { id: 'registration-force-guidelines', key: 'registration.force_guidelines' }, { id: 'audit-enabled', key: 'audit.enabled' }, { id: 'audit-username', key: 'audit.username_audit' }, { id: 'audit-avatar', key: 'audit.avatar_audit' }, @@ -18,9 +20,15 @@ // 已有专属 UI 的设置 key,不在"其他设置"中重复显示 var DEDICATED_KEYS = { 'site.framework': true, 'site.copyright_start': true, + 'site.name': true, 'site.tagline': true, 'site.contact_email': true, + 'site.operator_name': true, 'site.icp_number': true, 'site.icp_license': true, + 'site.police_number': true, 'site.wenwangwen': true, 'site.algorithm_filing': true, + 'site.privacy_url': true, 'site.terms_url': true, 'site.guidelines': true, 'maintenance.enabled': true, - 'registration.enabled': true, + 'registration.enabled': true, 'registration.show_guidelines': true, + 'registration.force_guidelines': true, 'registration.guidelines_timer': true, + 'password.strength': true, 'audit.enabled': true, 'audit.username_audit': true, 'audit.avatar_audit': true, 'audit.bio_audit': true }; @@ -73,8 +81,7 @@ // --- 文本输入保存 --- function bindSaveButtons() { - var buttons = document.querySelectorAll('.btn-save'); - buttons.forEach(function(btn) { + document.querySelectorAll('.btn-save, .btn-save-select').forEach(function(btn) { btn.addEventListener('click', function() { var key = btn.getAttribute('data-key'); var inputId = btn.getAttribute('data-input');