auth

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: 10 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrReplayed = fmt.Errorf("%w: refresh 重放(旧令牌二次使用)", ErrUnauthorized)

ErrReplayed 是 refresh 重放哨兵(#120:旧 refresh 二次使用):被轮换换出、 被强踢、被登出的 refresh 再次出现即此错。链上同时含 ErrUnauthorized (errors.Is 双判),下游可按需分流「重放 → 告警」而不影响 401 映射。

View Source
var ErrUnauthorized = bizerrors.NewUnauthorizedError("凭据无效")

ErrUnauthorized 是 401 语义总集(BizError 401——middleware.Fail 自动映射 HTTP 401):凭据无效、令牌已吊销、会话不在、查无此人,统一从此外出(具体 缘由看外层 %w 文本,客户端响应不泄露区分——防用户枚举)。

与 500 的分型契约(#120 错误分型不变式):本哨兵**只**承载「凭据不行」; 存储/仓储故障原样上抛(非 BizError,handler 层归 500)——数据面故障既不 放行也不放大成 401。

View Source
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 改密陈旧吊销钩子;风控「封禁下线」同口):

  1. authz.Revoker.RevokeUser 截断点(at 之前签发的令牌判陈旧,落 #119 用户级撤销黑名单)——覆盖未登记/跨实例的存量令牌;
  2. 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

func (f *Flow) IssuePair(ctx context.Context, u *User, meta SessionMeta) (*TokenPair, error)

IssuePair 签发尾段(#120):成对签发 access+refresh(kitx/jwt Manager IssuePair,token_type 分型 + 独立 jti)并登记在线会话(设备/会话维度)。

不变式:**签发与登记一次落地**——会话登记失败时把刚签出的双令牌直接拉黑 再上抛(宁可失败重试,不放行一对「签了名但不可吊销」的无主令牌)。

func (*Flow) Logout

func (f *Flow) Logout(ctx context.Context, access, refresh string) error

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

func (f *Flow) RevokeUser(ctx context.Context, userID string, at time.Time) error

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 核心不变式):

  1. 分型校验旧 refresh(ParseTyped,ParseOption 透传);
  2. 双层撤销判定:用户级截断命中 ⇒ ErrUnauthorized(改密/风控的账号级 吊销,覆盖重放分型——账号级事件优先于令牌级事件);否则 token 级黑名 单命中 ⇒ ErrReplayed(已换出/已登出/已强踢的 refresh 二次使用,重放 信号);
  3. 会话认领前置(GetByRefreshHash:旧 refresh 必须仍挂在活会话上);
  4. GetByID 错误分型(ErrUserNotFound ⇒ 401;其余 ⇒ 500),RoleIDs 重取 (角色变更随轮换生效);
  5. **原子认领旧会话**(Kick,先拉黑双 hash 后删键):恰一个轮换胜出, 败方/被强踢者 ⇒ ErrReplayed——旧 refresh 单次使用不靠时序运气;
  6. 签发新对并登记新会话(issueTail);
  7. 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):下游只留本接口的实现,编排语义单点在 库层。

Jump to

Keyboard shortcuts

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