pwd

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package pwd 提供密码策略与改密陈旧判定原语(#121):最小长度/复杂度/常见 弱口令拒绝面、历史防复用(fail-closed)、登录期过期轮换判定、令牌陈旧 秒截断比较——纯 Go 零依赖。

#121 的缺口:最小长度、历史防复用、过期轮换、改密后旧会话失效是账号体系 标配,下游(zeus 3 服务)同构手搓 pwdpolicy/pwdfresh 两包;其中 「fail-closed 的安全取舍」正该由库统一:

  • 历史查询失败 ≠ 放行(HistoryCheck 契约:Fetch/Match 出错原样上抛, 绝不降级成「没查到历史 ⇒ 新密码可用」);
  • 秒截断比较(TokenStaleBefore):iat 与改密时间同秒不判陈旧——JWT iat 本就是秒粒度,截断后比较避免时区/亚秒边界把「改密后新签的令牌」误判 陈旧(数据面故障也不放大成 401 全站踢下线);
  • 策略违规与服务端配置 bug 分型(#115 同口径):违规是 Violation(Unwrap 为 BizError 400,middleware.Fail 自动映射 400),策略配置非法是普通 error(归 500——配置 bug 归 400 会让排障指错方向)。

弱口令拒绝面是「内嵌常见弱口令表 + 轻量 leet 归一 + 结构性弱模式」, 非 zxcvbn 级熵评估——纯 Go 零依赖是本包的边界(哈希比对器 Matcher 由调用 方注入,典型 bcrypt.CompareHashAndPassword 包装,库层不绑哈希算法)。

改密陈旧吊销(改密后旧 token 失效)的编排钩子在 kitx/auth.RevokeUser—— 落到 kitx/authz 的用户级撤销黑名单;本包的 TokenStaleBefore 是其判定口径 的单点实现(authz.RevokedUser 复用同一份)。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyExpiryOnLogin

func ApplyExpiryOnLogin(ctx context.Context, days int, mustChange *bool, changedAt *time.Time, persist func(context.Context) error) error

ApplyExpiryOnLogin 登录期密码过期判定(#121):密码服役超过 days 天 ⇒ 置 *mustChange = true 并调用 persist 落库(登录成功路径调用,强制引导改密)。

契约:

  • days <= 0:未启用过期策略,no-op(nil);
  • mustChange 为 nil:调用方配置 bug,报普通 error(归 500,#115 同口径);
  • changedAt 为 nil:无改密记录,不判过期(创建时间口径由调用方折算后 传入——库层不猜「changedAt 缺省=注册时间」);
  • 未过期:不动 *mustChange(不清零——标记可能被其它路径(管理员强制) 置真,清零是 fail-open);
  • persist 失败:原样上抛(fail-closed:不吞错放行);*mustChange 已置位 (内存态保留,是否重试落库由调用方决定)。

判定用 time.Now()(无时钟注入:days 以天计,亚秒边界无关语义;测试以 相对时刻构造)。

func CheckMinLength

func CheckMinLength(password string, min int, msgFormat string) error

CheckMinLength 最小长度校验(#121 建议形态):rune 口径(用户感知长度, 多字节密码不被字节数误杀)。

msgFormat 是违规文案模板:为空用默认「密码至少 %d 个字符」;含 %d 占位符 时按 fmt.Sprintf(msgFormat, min) 格式化(约定 %d = 最小长度),不含 %d 时 按字面使用(避免 Sprintf 的 EXTRA 噪音)。违规返回 *Violation (Kind=KindLength,Unwrap 为 BizError 400)。min < 1 视为调用方配置 bug, 返回普通 error(归 500,#115 同口径)。

func IsViolation

func IsViolation(err error) bool

IsViolation 判定错误链里是否有密码策略违规(errors.As 链式匹配)。

func TokenStaleBefore

func TokenStaleBefore(iat time.Time, changedAt *time.Time) bool

TokenStaleBefore 令牌陈旧判定(#121 秒截断比较,本包的单点实现—— authz.RevokedUser 的用户级判定复用同一份,两处各写一份比较正是 #119/#121 要消灭的漂移形态):

iat 严格早于 changedAt ⇒ 陈旧(true)。

两侧截断到秒再比较:JWT 的 iat 本就是秒粒度(签发写 Unix 秒),改密时间 常带亚秒(DB 时间戳),截断后比较避免亚秒边界把「改密后同一秒内新签的 令牌」误判陈旧(误判即全站踢下线,数据面故障不放大成 401 的同款取舍)。 同一秒不判陈旧——秒粒度语义的既定边界,godoc 钉明。

changedAt 为 nil(无改密记录)⇒ 不陈旧。

Types

type HistoryCheck

type HistoryCheck struct {
	// Count 回溯条数(必须 ≥ 1)。
	Count int
	// Fetch 历史取数面(必须非 nil)。
	Fetch HistorySource
	// Match 哈希比对器(必须非 nil)。
	Match Matcher
}

HistoryCheck 历史防复用检查(#121):候选密码不得与最近 Count 条历史密码 重复。

fail-closed 契约(#121 的核心取舍):Fetch/Match 出错 ≠ 放行——历史查询 失败时绝不能降级成「没查到历史 ⇒ 新密码可用」,错误原样上抛(归 500), 让调用方决定重试还是失败。命中任一历史 ⇒ *Violation(Kind=KindHistory, 400)。

零值不设防,Count 必须显式 ≥ 1 且 Fetch/Match 非 nil(Check 期 fail-fast 报配置错,归 500——配置 bug 不归 400,#115 同口径)。

func (HistoryCheck) Check

func (h HistoryCheck) Check(ctx context.Context, userID, candidate string) error

Check 判定候选密码是否与历史重复(契约见 HistoryCheck)。

type HistorySource

type HistorySource func(ctx context.Context, userID string, count int) ([]string, error)

HistorySource 历史密码取数面:返回该用户最近 count 条历史密码的**存储形态** (典型 bcrypt hash,顺序不限,逐条比对)。存储形态而非明文——历史表落的 本来就是哈希。

type Kind

type Kind string

Kind 标注密码策略违规的类别(供调用方按类别定制响应文案/指标)。

const (
	// KindLength 长度违规(不足/超长)。
	KindLength Kind = "length"
	// KindComplexity 复杂度违规(缺字符类别)。
	KindComplexity Kind = "complexity"
	// KindWeak 弱口令违规(常见弱口令/结构性弱模式)。
	KindWeak Kind = "weak"
	// KindHistory 历史复用违规(与最近历史密码重复)。
	KindHistory Kind = "history"
)

type Matcher

type Matcher func(stored, candidate string) (bool, error)

Matcher 历史比对器:候选明文与存储形态比对(典型 bcrypt.CompareHashAndPassword 包装:`func(stored, candidate string) (bool, error) { return nil == bcrypt.CompareHashAndPassword([]byte(stored), []byte(candidate)) }`)。

库层不绑哈希算法(纯 Go 零依赖是本包边界):比对语义由调用方注入,错误 形态(哈希格式坏等)经 error 上抛,HistoryCheck 按 fail-closed 处理。

type Policy

type Policy struct {
	// MinLength 最小长度(rune 口径,必须 ≥ 1)。
	MinLength int
	// MaxLength 最大长度(rune 口径;0 = 不限)。防超长输入的存储/哈希层 DoS。
	MaxLength int
	// RequireUpper 要求含大写字母。
	RequireUpper bool
	// RequireLower 要求含小写字母。
	RequireLower bool
	// RequireDigit 要求含数字。
	RequireDigit bool
	// RequireSpecial 要求含特殊字符(非字母非数字,含空格)。
	RequireSpecial bool
}

Policy 是密码策略(#121):长度/复杂度/常见弱口令拒绝面,Check 全量判据。

判定顺序固定(长度 → 复杂度 → 弱口令),一次返回首个违规——密码设置场景 给用户一条可执行的整改指引即可,聚合多条反而淹没重点(与 validator 的 字段聚合场景不同域)。

零值不设防,构造时 MinLength 必须显式 ≥ 1(Check 期 fail-fast 报配置错): 「无最小长度的密码策略」是最危险的缺省(配置零值透传即全员短密码), 与 WithTTL(0) panic(jwt #65)同一纪律——此处以 error 表达(Check 有 error 出口,策略配置 bug 归 500)。

func (Policy) Check

func (p Policy) Check(password string) error

Check 校验密码是否满足策略(见 Policy 的判定顺序与配置契约)。

type Violation

type Violation struct {
	// Kind 违规类别。
	Kind Kind
	// Message 用户可读文案(msgFormat/默认文案产物)。
	Message string
}

Violation 是密码策略违规错误:Message 为用户可读文案(可直接进响应), Unwrap 为 pkg/errors BizError 400(middleware.Fail 自动映射 HTTP 400, 与 validator.ValidationError 同款分型纪律)。

func AsViolation

func AsViolation(err error) (*Violation, bool)

AsViolation 从错误链提取 Violation。

func (*Violation) Error

func (e *Violation) Error() string

Error 实现 error。

func (*Violation) Unwrap

func (e *Violation) Unwrap() error

Unwrap 返回 BizError 400(每次新值,message 与 Error() 一致——validator Unwrap 同款形态):handler 层 errors.As 取到即 400,不落 500。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL