Documentation
¶
Overview ¶
Package authz 提供双 token 的**状态面**原语(#119):撤销黑名单(token 级 / 用户级)与在线会话注册表(设备/会话维度登记、枚举、强踢),与 kitx/jwt 的 校验面(ParseTyped 等)组合消费——jwt 只管「令牌是不是我签的、过没过期」, 本包回答「这枚令牌还能不能用」「某用户在哪些设备在线」「如何把他踢下线」。
#119 的缺口:kitx/jwt pair.go 明文把撤销黑名单等有状态部分留在平台层,下游 (zeus 4 服务)只能手搓两份拷贝靠注释对齐哈希口径,漂移即强踢失效。本包把 「必须单点」的键空间/哈希约定收进库层:
- TokenHash 是键空间唯一产出口(sha256hex),黑名单/会话注册表的键全部 由此派生,跨服务不再各写一份;
- Revoker / SessionRegistry 接口化存储面(#119 建议形态):内存实现 MemRevoker / MemRegistry 随包交付(单实例进程内部署与测试替身双用), Redis 等持久实现由下游按同一契约落地(键前缀可配、TTL=refresh 剩余 寿命的约定写在各方法契约里)。
撤销语义两层:
- token 级:Revoke(hash, remaining)——单枚令牌拉黑,条目 TTL=令牌剩余 寿命(过期自然出清,过期令牌本就被 exp 拒绝);
- 用户级:RevokeUser(userID, at)——截断点,at 之前签发的全部令牌判陈旧 (#121 改密陈旧吊销的落地口)。判定口径单点在 pwd.TokenStaleBefore (秒截断比较),本包复用之,两处比较不各写一份。
会话语义(SessionRegistry):Register 登记(TTL=refresh 剩余寿命)、 ListByUser 管理端枚举、Kick 单会话强踢(**双 hash 拉黑 + 删键**,原子认领 语义供 refresh 轮换防重放拍板)、KickUser 全会话强踢。编排层(kitx/auth) 在此之上交付签发尾段/轮换/登出/改密吊销的单点语义。
并发契约:MemRevoker / MemRegistry 全方法并发安全(共享状态 mutex 保护), 不启动后台 goroutine——过期条目读判即视为不在,Sweep 可手动清扫。
Index ¶
- Variables
- func RevokeToken(ctx context.Context, r Revoker, token string, remaining time.Duration) error
- func RevokedToken(ctx context.Context, r Revoker, token string) (bool, error)
- func TokenHash(token string) string
- type MemRegistry
- func (m *MemRegistry) GetByRefreshHash(ctx context.Context, refreshHash string) (Session, error)
- func (m *MemRegistry) Kick(ctx context.Context, id string) (bool, error)
- func (m *MemRegistry) KickUser(ctx context.Context, userID string) (int, error)
- func (m *MemRegistry) ListByUser(ctx context.Context, userID string) ([]Session, error)
- func (m *MemRegistry) Register(ctx context.Context, s Session, ttl time.Duration) error
- func (m *MemRegistry) Sweep()
- type MemRegistryOption
- type MemRevoker
- func (m *MemRevoker) Revoke(ctx context.Context, hash string, remaining time.Duration) error
- func (m *MemRevoker) RevokeUser(ctx context.Context, userID string, at time.Time) error
- func (m *MemRevoker) Revoked(ctx context.Context, hash string) (bool, error)
- func (m *MemRevoker) RevokedUser(ctx context.Context, userID string, iat time.Time) (bool, error)
- func (m *MemRevoker) Sweep()
- type MemRevokerOption
- type Revoker
- type Session
- type SessionRegistry
Constants ¶
This section is empty.
Variables ¶
var ErrSessionNotFound = errors.New("authz: 会话不存在")
ErrSessionNotFound 表示会话不在登记表(未登记 / 已过期 / 已下线)。
Functions ¶
func RevokeToken ¶
RevokeToken 以原始令牌串登记撤销(Revoker.Revoke 的薄助手:内部经 TokenHash 落到键空间)。remaining 语义同 Revoker.Revoke。
func RevokedToken ¶
RevokedToken 以原始令牌串判定撤销(Revoker.Revoked 的薄助手)。
Types ¶
type MemRegistry ¶
type MemRegistry struct {
// contains filtered or unexported fields
}
MemRegistry 是 SessionRegistry 的内存实现:并发安全(mutex 保护), 与 MemRevoker 配对使用(Kick 的双 hash 拉黑经构造注入的 Revoker 落地)。
定位与 MemRevoker 一致:单实例进程内形态 + 契约测试替身。不启动后台 goroutine——过期会话读判即视为不在,Sweep 可手动回收。
func NewMemRegistry ¶
func NewMemRegistry(rev Revoker, opts ...MemRegistryOption) *MemRegistry
NewMemRegistry 构造内存会话注册表。rev 是 Kick/KickUser 双 hash 拉黑的 落地口,必须非 nil——强踢无拉黑等于假强踢(#65 同款零值透传纪律,构造期 fail-fast panic)。
func (*MemRegistry) GetByRefreshHash ¶
GetByRefreshHash 按 refresh 哈希查会话(契约见 SessionRegistry)。
func (*MemRegistry) Kick ¶
Kick 强踢单会话(契约见 SessionRegistry 的三态返回)。
时序:先拉黑(登记保留、失败可重试)后原子删键(claim 拍板)——拉黑失败 时不动登记,避免「登记没了、令牌还活着」的假下线。
func (*MemRegistry) ListByUser ¶
ListByUser 枚举某用户的在线会话(契约见 SessionRegistry)。
func (*MemRegistry) Sweep ¶
func (m *MemRegistry) Sweep()
Sweep 回收过期登记(读路径已把过期会话视为不在,Sweep 只影响内存占用)。
type MemRegistryOption ¶
type MemRegistryOption func(*MemRegistry)
MemRegistryOption 是 NewMemRegistry 的函数选项。
func WithRegistryClock ¶
func WithRegistryClock(f func() time.Time) MemRegistryOption
WithRegistryClock 注入 MemRegistry 时钟(f 为 nil 直接 panic,WithRevokerClock 同款纪律)。
type MemRevoker ¶
type MemRevoker struct {
// contains filtered or unexported fields
}
MemRevoker 是 Revoker 的内存实现:并发安全(mutex 保护),零第三方依赖。
定位:单实例进程内部署的内存形态,兼作契约测试替身(#119 建议形态的 MapRevoker 角色)。持久形态(Redis 等)由下游按 Revoker 契约落地——键前缀 可配、条目 TTL 语义与本实现一致。
生命周期:不启动后台 goroutine(本仓禁止裸 go func)。token 级条目过期后 读判即视为不在;写路径不自动清扫,Sweep 可手动回收过期条目(pkg/cache NoBackgroundSweep 同款哲学:清扫时机交给部署形态)。
func NewMemRevoker ¶
func NewMemRevoker(opts ...MemRevokerOption) *MemRevoker
NewMemRevoker 构造内存撤销黑名单(缺省时钟 time.Now)。
func (*MemRevoker) RevokeUser ¶
RevokeUser 设置用户级截断点(单调不回退,取既有值与 at 的较大者——回退 会让已判陈旧的令牌复活)。
func (*MemRevoker) RevokedUser ¶
RevokedUser 用户级判定:iat 早于截断点(秒截断)即陈旧。
比较口径单点委托 pwd.TokenStaleBefore(#121 改密陈旧判定的同一份实现—— 两处各写一份秒截断比较正是 #119/#121 要消灭的漂移形态)。
func (*MemRevoker) Sweep ¶
func (m *MemRevoker) Sweep()
Sweep 回收 token 级过期条目(用户级截断点不设 TTL,不回收)。 读路径本就把过期条目视为不在,Sweep 只影响内存占用,不影响判定语义。
type MemRevokerOption ¶
type MemRevokerOption func(*MemRevoker)
MemRevokerOption 是 NewMemRevoker 的函数选项。
func WithRevokerClock ¶
func WithRevokerClock(f func() time.Time) MemRevokerOption
WithRevokerClock 注入 MemRevoker 时钟(测试确定性 TTL 边界;f 为 nil 直接 panic——静默回落 time.Now() 会让时钟无声漂移,WithNow(#64)同款纪律)。
type Revoker ¶
type Revoker interface {
// Revoke 吊销单枚令牌:以 hash 记黑名单,条目 TTL=remaining。
// remaining 必须为正(已过期令牌无需登记,非正 TTL 是配置零值透传的
// 常见形态,#65 同款纪律)。
Revoke(ctx context.Context, hash string, remaining time.Duration) error
// Revoked 判定单枚令牌是否已吊销(token 级)。
Revoked(ctx context.Context, hash string) (bool, error)
// RevokeUser 用户级吊销:at 之前签发的该用户令牌全部判陈旧(截断点单调不回退)。
RevokeUser(ctx context.Context, userID string, at time.Time) error
// RevokedUser 用户级判定:iat 早于截断点(秒截断,pwd.TokenStaleBefore
// 口径)即陈旧。无截断点 ⇒ false。
RevokedUser(ctx context.Context, userID string, iat time.Time) (bool, error)
}
Revoker 撤销黑名单存储面(#119):token 级 + 用户级两层,接口化存储。
键纪律:hash 参数必须出自 TokenHash(键空间唯一产出口),实现按「键即 TokenHash 产物」理解与建索引,不接受任意字符串键。原始令牌串入口见 RevokeToken / RevokedToken 助手。raw token 与 hash 的分型是有意为之: 会话注册表(Kick 双 hash 拉黑)只持有 hash,轮换/登出路径未必持有原始串, 签名收窄到 hash 级可杜绝「随手传非 TokenHash 键污染键空间」。
两层语义:
- token 级(Revoke/Revoked):单枚令牌拉黑。remaining 是该令牌的**剩余 寿命**(条目 TTL,过期自然出清——过期令牌本就被 exp 拒绝,条目无需 长生);已过期令牌不需要登记。
- 用户级(RevokeUser/RevokedUser):截断点。at 之前签发的全部令牌判陈旧 (#121 改密陈旧吊销的落地口)。截断点**单调不回退**(同用户重复设置取 时间较大者——回退会让已判陈旧的令牌复活,是 fail-open);不设 TTL(陈旧 判定随 iat 天然自限,条目随用户数线性)。
陈旧判定口径单点在 pwd.TokenStaleBefore(秒截断比较:iat 与截断点同秒不判 陈旧)——实现 RevokedUser 必须复用该口径,禁止另写一份比较。
存储故障契约:实现出错必须返回 error,**不得**把故障折叠成「未吊销」放行 (fail-open 是黑名单最危险的缺省)。消费侧(kitx/auth)把查询故障归 500: 既不放行、也不放大成 401 全站踢下线。
type Session ¶
type Session struct {
// ID 会话 ID(登记表主键,编排层生成;同一次登录一对令牌一枚会话 ID)。
ID string
// UserID 会话归属用户。
UserID string
// Device 设备标识(自由文本,管理端枚举/按设备辨识用;可空)。
Device string
// IP 登录来源 IP(审计留痕;可空)。
IP string
// UA 登录客户端 UA(审计留痕;可空)。
UA string
// RefreshJTI 会话对应的 refresh 令牌 jti(管理端展示与对账;可空)。
RefreshJTI string
// AccessHash access 令牌的 TokenHash(强踢拉黑口径;可空——空则 Kick 跳过该路)。
AccessHash string
// RefreshHash refresh 令牌的 TokenHash(轮换认领键;必填)。
RefreshHash string
// LoginAt 登录/换发时刻。
LoginAt time.Time
}
Session 是一条在线会话登记项(设备/会话维度,#119)。
登记表回答「某用户在哪些设备在线」(ListByUser → Device/IP/UA/LoginAt), 强踢回答「如何把他踢下线」(Kick/KickUser → 双 hash 拉黑 + 删键)。
哈希字段(AccessHash/RefreshHash)是 TokenHash 产出口:登记期由编排层 (kitx/auth)算好写入,拉黑期由注册表直接消费——原始令牌串不进登记表 (存储面不落明文凭据)。
type SessionRegistry ¶
type SessionRegistry interface {
// Register 登记会话(ttl=refresh 剩余寿命,必须为正)。
// ID 已被未过期会话占用、或 RefreshHash 已挂在另一会话上 ⇒ 报错
// (同一 refresh 不得挂两枚会话——登记面本身也是防重放面)。
Register(ctx context.Context, s Session, ttl time.Duration) error
// GetByRefreshHash 按 refresh 哈希查会话(refresh 轮换的认领前置)。
// 不在(未登记/已过期/已下线)⇒ ErrSessionNotFound。
GetByRefreshHash(ctx context.Context, refreshHash string) (Session, error)
// ListByUser 枚举某用户的在线会话(管理端;按 LoginAt、ID 稳定排序,
// 过期会话不出现)。返回值是快照副本,改写不影响登记表。
ListByUser(ctx context.Context, userID string) ([]Session, error)
// Kick 强踢单会话:双 hash 拉黑 + 删键,**原子认领**。三态契约:
// - (true, nil):完整下线(拉黑与删键都已落地);
// - (false, nil):登记不存在(幂等强踢的常态 / 轮换竞态的败方);
// - (false, err):拉黑落地失败,登记**保留**(可重试)。
//
// 原子认领(claim)是 refresh 轮换防重放的拍板点:并发轮换/强踢竞争同一
// 会话时恰有一个调用拿到 true,其余按败方拒绝——「旧 refresh 二次使用被拒」
// 不靠时序运气。
Kick(ctx context.Context, id string) (bool, error)
// KickUser 强踢某用户全部在线会话(#119 强踢全会话;改密吊销钩子的落地
// 一半)。返回完整下线的会话数;部分失败时已踢的不回滚,错误聚合上抛
// (errors.Join)。
KickUser(ctx context.Context, userID string) (int, error)
}
SessionRegistry 在线会话注册表存储面(#119):接口化存储。
生命周期契约:登记 TTL 是 refresh 令牌的**剩余寿命**(编排层以 ttlRefresh 登记)——会话过期即两令牌都已自然失效(编排层保证 ttlRefresh ≥ ttlAccess, 登记 TTL 是两令牌寿命的上界)。持久实现(Redis)按键前缀可配 + TTL=登记 寿命落地即可。
强踢契约(Kick):**双 hash 拉黑 + 删键**一次落地——只删登记不拉黑等于假 强踢(令牌还活着),只拉黑不删键会让会话继续被枚举为在线。拉黑落地失败时 登记必须保留(可重试),见 Kick 的三态返回。