feat: 密码强度多级校验 — low/medium/high/very_high

- site_settings 新增 password.strength 配置项
- 四级:low(len>=8) / medium(两类) / high(三类+无连续) / very_high(四类+无连续)
- 连续字符检查:重复/升序/降序/键盘水平/键盘垂直序列
- 注册和改密接口统一读取配置
- 前端 register.js 动态校验 + 管理后台安全设置区
This commit is contained in:
2026-06-22 00:54:21 +08:00
parent e9b7401dc9
commit 9a496d3055
8 changed files with 225 additions and 11 deletions

View File

@ -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")

View File

@ -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(),

View File

@ -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
}

View File

@ -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
}