authz

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

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

Constants

This section is empty.

Variables

View Source
var ErrSessionNotFound = errors.New("authz: 会话不存在")

ErrSessionNotFound 表示会话不在登记表(未登记 / 已过期 / 已下线)。

Functions

func RevokeToken

func RevokeToken(ctx context.Context, r Revoker, token string, remaining time.Duration) error

RevokeToken 以原始令牌串登记撤销(Revoker.Revoke 的薄助手:内部经 TokenHash 落到键空间)。remaining 语义同 Revoker.Revoke。

func RevokedToken

func RevokedToken(ctx context.Context, r Revoker, token string) (bool, error)

RevokedToken 以原始令牌串判定撤销(Revoker.Revoked 的薄助手)。

func TokenHash

func TokenHash(token string) string

TokenHash 计算令牌哈希键:sha256(token) 的小写 hex(#119 建议形态)。

这是撤销黑名单/会话注册表键空间的**唯一产出口**:所有按令牌定位的键 (黑名单条目、会话 AccessHash/RefreshHash)都必须经本函数派生,下游不再 各写一份 sha256 口径——两份拷贝靠注释对齐、漂移即强踢失效(#119 下游实证)。 入参是完整令牌串而非 jti:绑定到具体令牌形态(同一 jti 的两个令牌串不同、 哈希不同,可分别吊销)。

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

func (m *MemRegistry) GetByRefreshHash(ctx context.Context, refreshHash string) (Session, error)

GetByRefreshHash 按 refresh 哈希查会话(契约见 SessionRegistry)。

func (*MemRegistry) Kick

func (m *MemRegistry) Kick(ctx context.Context, id string) (bool, error)

Kick 强踢单会话(契约见 SessionRegistry 的三态返回)。

时序:先拉黑(登记保留、失败可重试)后原子删键(claim 拍板)——拉黑失败 时不动登记,避免「登记没了、令牌还活着」的假下线。

func (*MemRegistry) KickUser

func (m *MemRegistry) KickUser(ctx context.Context, userID string) (int, error)

KickUser 强踢某用户全部在线会话(契约见 SessionRegistry)。

func (*MemRegistry) ListByUser

func (m *MemRegistry) ListByUser(ctx context.Context, userID string) ([]Session, error)

ListByUser 枚举某用户的在线会话(契约见 SessionRegistry)。

func (*MemRegistry) Register

func (m *MemRegistry) Register(ctx context.Context, s Session, ttl time.Duration) error

Register 登记会话(契约见 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) Revoke

func (m *MemRevoker) Revoke(ctx context.Context, hash string, remaining time.Duration) error

Revoke 登记一条 token 级撤销(TTL=remaining)。

func (*MemRevoker) RevokeUser

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

RevokeUser 设置用户级截断点(单调不回退,取既有值与 at 的较大者——回退 会让已判陈旧的令牌复活)。

func (*MemRevoker) Revoked

func (m *MemRevoker) Revoked(ctx context.Context, hash string) (bool, error)

Revoked 判定 token 级撤销(过期条目视为不在)。

func (*MemRevoker) RevokedUser

func (m *MemRevoker) RevokedUser(ctx context.Context, userID string, iat time.Time) (bool, error)

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 的三态返回。

Jump to

Keyboard shortcuts

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