Documentation
¶
Overview ¶
Package auth 提供认证流编排原语(#120):access+refresh 签发尾段、refresh 轮换(旧令牌防重放)、登出双 token 撤销、改密陈旧吊销钩子(#121)——安全 不变式的单点实现,基于 kitx/jwt(签发/校验)与 kitx/authz(状态面)。
#120 的缺口:认证流的编排语义是安全不变式——轮换必踢旧会话、GetByID 错误 分型 401/500、TouchLogin 尽力而为、登出双 token 撤销——下游(zeus 3 认证 服务)「逐字同源」各持一份后**实际漂移**(错误映射不同、旧会话清理语义不 同),只能在自己仓内先收敛。本包把编排语义收进库层,下游只留仓储适配器 (UserRepo 实现),不再各自维护一份轮换时序。
核心不变式(godoc 契约,测试钉死):
- 轮换必踢旧会话,且旧 refresh **单次使用**:RotateRefresh 以 SessionRegistry.Kick 的原子认领拍板——并发轮换同一 refresh 恰有一方 成功,败方按 ErrReplayed 拒绝(401);旧 refresh 落黑名单,二次使用 恒被拒(ErrReplayed,可 errors.Is 分流告警)。
- 错误分型 401/500:ErrUnauthorized(BizError 401,middleware.Fail 自动 映射)承载全部 401 语义(凭据无效/已吊销/会话不在/查无此人);存储/仓储 故障原样上抛(归 500)——既不放行(fail-open),也不放大成 401 全站 踢下线。
- TouchLogin 尽力而为:失败只记日志不阻断轮换(登录活跃度是弱信号)。
- 登出撤销覆盖双 token:会话下线(双 hash 拉黑 + 删键)+ 呈现令牌直接 拉黑,幂等可重。
三件套咬合(#119/#120/#121):本包消费 #119 状态面(authz.Revoker / SessionRegistry),#121 的改密吊销钩子(RevokeUser)落到 #119 的用户级 撤销黑名单 + 全会话强踢;陈旧判定口径单点在 pwd.TokenStaleBefore。
Index ¶
- Variables
- func IsRevoked(ctx context.Context, rev authz.Revoker, token, userID string, iat time.Time) (bool, error)
- func RevokeUser(ctx context.Context, rev authz.Revoker, sessions authz.SessionRegistry, ...) error
- type Flow
- func (f *Flow) IssuePair(ctx context.Context, u *User, meta SessionMeta) (*TokenPair, error)
- func (f *Flow) Logout(ctx context.Context, access, refresh string) error
- func (f *Flow) ParseAccess(ctx context.Context, access string, opts ...jwt.ParseOption) (*jwt.Claims, error)
- func (f *Flow) RevokeUser(ctx context.Context, userID string, at time.Time) error
- func (f *Flow) RotateRefresh(ctx context.Context, repo UserRepo, refresh string, meta SessionMeta) (*TokenPair, *User, error)
- type FlowOption
- type SessionMeta
- type TokenPair
- type User
- type UserRepo
Constants ¶
This section is empty.
Variables ¶
var ErrReplayed = fmt.Errorf("%w: refresh 重放(旧令牌二次使用)", ErrUnauthorized)
ErrReplayed 是 refresh 重放哨兵(#120:旧 refresh 二次使用):被轮换换出、 被强踢、被登出的 refresh 再次出现即此错。链上同时含 ErrUnauthorized (errors.Is 双判),下游可按需分流「重放 → 告警」而不影响 401 映射。
ErrUnauthorized 是 401 语义总集(BizError 401——middleware.Fail 自动映射 HTTP 401):凭据无效、令牌已吊销、会话不在、查无此人,统一从此外出(具体 缘由看外层 %w 文本,客户端响应不泄露区分——防用户枚举)。
与 500 的分型契约(#120 错误分型不变式):本哨兵**只**承载「凭据不行」; 存储/仓储故障原样上抛(非 BizError,handler 层归 500)——数据面故障既不 放行也不放大成 401。
var ErrUserNotFound = errors.New("auth: 用户不存在")
ErrUserNotFound 是 UserRepo.GetByID 的契约哨兵(查无此人)。Flow 把它归 401(不泄露存在性);仓储适配器必须把「查无此人」映射到本哨兵,其余错误 (DB 故障等)原样上抛(归 500)——两者混同会让排障无法分型。
Functions ¶
func IsRevoked ¶
func IsRevoked(ctx context.Context, rev authz.Revoker, token, userID string, iat time.Time) (bool, error)
IsRevoked 组合撤销判定(#119 与 jwt 校验面的组合锚点):token 级黑名单 (TokenHash 键)或用户级截断(iat 陈旧,pwd.TokenStaleBefore 口径)任一 命中即真。查询故障原样上抛(消费侧归 500——不放行、不放大 401)。
func RevokeUser ¶
func RevokeUser(ctx context.Context, rev authz.Revoker, sessions authz.SessionRegistry, userID string, at time.Time) error
RevokeUser 用户级吊销编排(#121 改密陈旧吊销钩子;风控「封禁下线」同口):
- authz.Revoker.RevokeUser 截断点(at 之前签发的令牌判陈旧,落 #119 用户级撤销黑名单)——覆盖未登记/跨实例的存量令牌;
- authz.SessionRegistry.KickUser 全会话强踢(双 hash 拉黑 + 删键)—— 在线会话立即下线,不等陈旧判定逐请求拦截。
两步失败 errors.Join 聚合(部分落地也要报全,调用方决定重试口径)。 at 取改密时刻(pwd.ApplyExpiryOnLogin/改密路径同源)。
Types ¶
type Flow ¶
type Flow struct {
// contains filtered or unexported fields
}
Flow 是认证流编排器:把签发尾段(IssuePair)、refresh 轮换(RotateRefresh)、 登出撤销(Logout)、改密吊销钩子(RevokeUser)集中在库层。构造后零可变 状态,可并发使用。
func NewFlow ¶
func NewFlow(m *jwt.Manager, sessions authz.SessionRegistry, rev authz.Revoker, opts ...FlowOption) (*Flow, error)
NewFlow 构造认证流编排器。构造期 fail-fast(jwt.NewManagerRS256 同一哲学): 依赖任一缺失、TTL 未配置/非正、ttlRefresh < ttlAccess 直接报错——这些零值 透传到第一次签发/轮换才炸出来毫无收益。
func (*Flow) IssuePair ¶
IssuePair 签发尾段(#120):成对签发 access+refresh(kitx/jwt Manager IssuePair,token_type 分型 + 独立 jti)并登记在线会话(设备/会话维度)。
不变式:**签发与登记一次落地**——会话登记失败时把刚签出的双令牌直接拉黑 再上抛(宁可失败重试,不放行一对「签了名但不可吊销」的无主令牌)。
func (*Flow) Logout ¶
Logout 登出撤销(#120):双 token 一次落地——会话下线(Kick:双 hash 拉黑 + 删键)+ 呈现令牌直接拉黑(覆盖会话外/旧对的令牌,TTL 取配置寿命上界, 宁长勿短)。幂等:会话/令牌已不在照常成功。多步失败 errors.Join 聚合。
func (*Flow) ParseAccess ¶
func (f *Flow) ParseAccess(ctx context.Context, access string, opts ...jwt.ParseOption) (*jwt.Claims, error)
ParseAccess 资源侧消费面(#119「与 jwt 校验面可组合」的落地):分型校验 (ParseTyped access,ParseOption 在共享解析选项后追加、按调用项生效)+ 双层撤销判定,返回 Claims。
错误分型:校验失败/已吊销 ⇒ ErrUnauthorized 链(401);撤销查询故障 ⇒ 原样上抛(500)。
func (*Flow) RevokeUser ¶
RevokeUser 是 Flow 装配形态的改密吊销钩子(见包级 RevokeUser)。
func (*Flow) RotateRefresh ¶
func (f *Flow) RotateRefresh(ctx context.Context, repo UserRepo, refresh string, meta SessionMeta) (*TokenPair, *User, error)
RotateRefresh refresh 轮换 + 防重放(#120 核心不变式):
- 分型校验旧 refresh(ParseTyped,ParseOption 透传);
- 双层撤销判定:用户级截断命中 ⇒ ErrUnauthorized(改密/风控的账号级 吊销,覆盖重放分型——账号级事件优先于令牌级事件);否则 token 级黑名 单命中 ⇒ ErrReplayed(已换出/已登出/已强踢的 refresh 二次使用,重放 信号);
- 会话认领前置(GetByRefreshHash:旧 refresh 必须仍挂在活会话上);
- GetByID 错误分型(ErrUserNotFound ⇒ 401;其余 ⇒ 500),RoleIDs 重取 (角色变更随轮换生效);
- **原子认领旧会话**(Kick,先拉黑双 hash 后删键):恰一个轮换胜出, 败方/被强踢者 ⇒ ErrReplayed——旧 refresh 单次使用不靠时序运气;
- 签发新对并登记新会话(issueTail);
- TouchLogin 尽力而为(失败只记日志)。
时序拍板(与 Manager.Refresh 的差异):**先认领后签发**——Manager.Refresh 的 onRotate 钩子在签发之后触发,满足不了认领闸(并发轮换会双双签发成功); 认领先行的代价是「认领成功但签发失败 ⇒ 需重新登录」(fail-closed,宁可 重登也不留旧令牌复活窗口)。
错误链:401 类(ErrUnauthorized/ErrReplayed/jwt.ErrInvalidToken 三向 errors.Is);500 类原样上抛不带 ErrUnauthorized。
type FlowOption ¶
type FlowOption func(*Flow)
FlowOption 是 NewFlow 的函数选项。
func WithClock ¶
func WithClock(f func() time.Time) FlowOption
WithClock 注入 Flow 时钟(会话 LoginAt;f 为 nil 直接 panic,jwt.WithNow 同款纪律)。签发时钟(iat/exp)经 WithSignOptions 传 jwt.WithNow/WithIssuedAt。
func WithParseOptions ¶
func WithParseOptions(opts ...jwt.ParseOption) FlowOption
WithParseOptions 配置共享解析选项(WithAudience(#66 fail-closed)、 WithExpectedIssuer(#95)、WithLeeway 等),解析/轮换/登出全路径生效。 选项入参 slices.Clone 拷贝。
func WithRolesClaim ¶
func WithRolesClaim(key string) FlowOption
WithRolesClaim 配置角色声明键(空串回退默认 "roles")。
func WithSignOptions ¶
func WithSignOptions(opts ...jwt.SignOption) FlowOption
WithSignOptions 配置共享签发选项(WithIssuer 钉签发者、WithAudienceClaims 钉受众、WithNow/WithIssuedAt 注入签发时钟等),两令牌共享。**不得**含 WithJTI(Manager.IssuePair 的显式 jti 会盖到两枚令牌上,破坏 jti 唯一性; jti 由库层自动生成)。选项入参 slices.Clone 拷贝(jwt #117 同款)。
func WithTTL ¶
func WithTTL(access, refresh time.Duration) FlowOption
WithTTL 配置双令牌寿命(必须显式配置为正——配置零值透传签出永不过期令牌 是最危险的缺省,jwt #65 同款纪律,NewFlow 以 error 表达)。ttlRefresh 必须 ≥ ttlAccess:会话登记/拉黑 TTL 以 refresh 寿命为两令牌寿命的上界,短于 access 会让强踢漏拉黑尚能活的 access。
type SessionMeta ¶
type SessionMeta struct {
// IP 登录来源 IP(可空)。
IP string
// Device 设备标识(可空;同账号多端登录的辨识键)。
Device string
// UA 登录客户端 UA(可空)。
UA string
}
SessionMeta 是会话登记的登录上下文(设备/网络维度,管理端枚举与审计用)。 收拢成结构体而非位置参数(ip, ua string):字段随审计需求增长时签名不漂 (#120 的漂移教训在 API 形状上同样适用)。
type TokenPair ¶
type TokenPair struct {
// Access access 令牌(短 ttl,面向资源请求)。
Access string
// Refresh refresh 令牌(长 ttl,只用于换发新令牌对)。
Refresh string
// Session 已登记的会话(ID/哈希/登录时刻——登记失败则整个签发失败)。
Session authz.Session
}
TokenPair 是签发产物:双令牌 + 已登记的会话快照。
type User ¶
type User struct {
// ID 用户标识(签发为 sub)。
ID string
// Roles 角色集(注入 rolesClaim 声明;空集不注入)。
Roles []string
// Claims 业务会话声明(随令牌签发并随轮换继承——jwt 业务声明口径)。
// 不得占用 rolesClaim 键(Roles 是角色的唯一入口)与 jwt 保留声明
// (iss/sub/aud/exp/nbf/iat/jti/token_type,#71 注入面防护在 jwt 层)。
Claims map[string]any
}
User 是认证流的最小用户快照:登录方备好(IssuePair 消费),轮换期由 UserRepo 重新拉取(角色变更随轮换生效)。
type UserRepo ¶
type UserRepo interface {
// GetByID 拉取用户快照。错误分型契约(#120 不变式):查无此人必须返回
// ErrUserNotFound(Flow 归 401,不泄露存在性);其余错误原样上抛(归
// 500)。返回 (nil, nil) 视为违约(归 500)。
GetByID(ctx context.Context, id string) (*User, error)
// RoleIDs 拉取角色集(轮换期单独拉——角色变更随轮换生效)。出错原样
// 上抛(归 500)。
RoleIDs(ctx context.Context, id string) ([]string, error)
// TouchLogin 登录活跃度回写。**尽力而为**(#120 不变式):失败不阻断
// 轮换,Flow 只记日志。
TouchLogin(ctx context.Context, id string) error
}
UserRepo 是用户仓储适配面(#120):下游只留本接口的实现,编排语义单点在 库层。