6595e94677
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见 last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。 go build / vet / test -race ./... 全绿。 安全(P0): - JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言 - JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm) - 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size - HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control) 并发/资源(主线A 闭合): - 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage / DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争 - trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown - ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron) - ratelimit failClosed atomic.Bool;Recover Written() 守卫 - cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态) 终审剩余项收口(H-A~M-H + L 系列): - 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns) - JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用 - GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU - config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady - compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold - 删 redisLimiters 死代码;CLI 输入校验 + 回滚 文档: - README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明) - docs/README 索引修正;config 注释修正(RS256 已不支持) 破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
package validation
|
||
|
||
import (
|
||
"strconv"
|
||
"unicode"
|
||
)
|
||
|
||
// PasswordConfig 密码验证配置
|
||
type PasswordConfig struct {
|
||
MinLength int // 最小长度
|
||
MaxLength int // 最大长度
|
||
RequireUpper bool // 需要大写字母
|
||
RequireLower bool // 需要小写字母
|
||
RequireDigit bool // 需要数字
|
||
RequireSpecial bool // 需要特殊字符
|
||
}
|
||
|
||
// DefaultPasswordConfig 默认密码配置。
|
||
//
|
||
// P1 #17:MaxLength 由 128 收敛到 72(字节)。bcrypt 在 72 字节处静默截断——
|
||
// 若允许更长密码,两个前 72 字节相同的不同密码会被视为同一密码通过认证。
|
||
// length 用 len()(字节)度量,与 bcrypt 的字节截断口径一致。需支持超长密码时,
|
||
// 应在哈希前先做 SHA-256 预哈希再交给 bcrypt,而非放宽此上限。
|
||
var DefaultPasswordConfig = PasswordConfig{
|
||
MinLength: 8, // 最少8位
|
||
MaxLength: 72, // 最多72字节(bcrypt 截断边界,见上)
|
||
RequireUpper: true,
|
||
RequireLower: true,
|
||
RequireDigit: true,
|
||
RequireSpecial: false,
|
||
}
|
||
|
||
// ValidatePassword 验证密码强度
|
||
// 返回:是否有效,错误信息
|
||
func ValidatePassword(password string) (bool, string) {
|
||
return ValidatePasswordWithConfig(password, DefaultPasswordConfig)
|
||
}
|
||
|
||
// ValidatePasswordWithConfig 使用指定配置验证密码强度
|
||
func ValidatePasswordWithConfig(password string, config PasswordConfig) (bool, string) {
|
||
length := len(password)
|
||
|
||
// 检查最小长度
|
||
if length < config.MinLength {
|
||
return false, "密码长度不能少于" + strconv.Itoa(config.MinLength) + "位"
|
||
}
|
||
|
||
// 检查最大长度
|
||
if length > config.MaxLength {
|
||
return false, "密码长度不能超过" + strconv.Itoa(config.MaxLength) + "位"
|
||
}
|
||
|
||
var hasUpper, hasLower, hasDigit, hasSpecial bool
|
||
|
||
for _, char := range password {
|
||
switch {
|
||
case unicode.IsUpper(char):
|
||
hasUpper = true
|
||
case unicode.IsLower(char):
|
||
hasLower = true
|
||
case unicode.IsDigit(char):
|
||
hasDigit = true
|
||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||
hasSpecial = true
|
||
}
|
||
}
|
||
|
||
// 检查大写字母
|
||
if config.RequireUpper && !hasUpper {
|
||
return false, "密码必须包含大写字母"
|
||
}
|
||
|
||
// 检查小写字母
|
||
if config.RequireLower && !hasLower {
|
||
return false, "密码必须包含小写字母"
|
||
}
|
||
|
||
// 检查数字
|
||
if config.RequireDigit && !hasDigit {
|
||
return false, "密码必须包含数字"
|
||
}
|
||
|
||
// 检查特殊字符
|
||
if config.RequireSpecial && !hasSpecial {
|
||
return false, "密码必须包含特殊字符"
|
||
}
|
||
|
||
return true, ""
|
||
}
|