auth

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package auth 提供用户、角色、会话与访问令牌的存储与鉴权逻辑。

Index

Constants

View Source
const (
	StatusEnabled  = "enabled"
	StatusDisabled = "disabled"
)

用户状态筛选取值。

View Source
const (

	// CSRFHeaderName 是前端提交 CSRF 令牌的请求头。
	CSRFHeaderName = "X-CSRF-Token"
)

会话与 CSRF 的 Cookie 名。

__Host- 前缀要求 Secure + Path=/ 且不带 Domain,能防止子域写入伪造 Cookie; 但它同时要求 HTTPS,故仅在 secure 模式下启用(见 cookieName)。

View Source
const CoreKey = "auth-core"

CoreKey 是核心认证栈在 app 容器里的登记名。

放在 auth 包里而不是使用方:登记名与类型是一件事,分开放迟早会出现 「Provide 用了这个名字、Lookup 拼成了另一个」这种只在运行时暴露的错。

View Source
const (
	// SessionTTL 是会话有效期。
	SessionTTL = 7 * 24 * time.Hour
)

会话时长。

View Source
const TokenPrefix = "lumo_pat_"

TokenPrefix 是 PAT 明文的固定前缀。

有了它,令牌一旦被误贴到公开场所(issue、日志、代码库), 可以被 secret 扫描工具按前缀识别出来。

Variables

View Source
var (
	// ErrNotFound 表示实体不存在。
	ErrNotFound = errors.New("对象不存在")
	// ErrDuplicate 表示唯一约束冲突(用户名或邮箱已存在)。
	ErrDuplicate = errors.New("对象已存在")
	// ErrBuiltinRole 表示试图删除内置角色。
	//
	// 内置角色的权限自 2026-09-15 起可改,故这个哨兵现在只剩「不可删」这一层含义。
	ErrBuiltinRole = errors.New("内置角色不可删除")
	// ErrRoleLocked 表示试图修改不对站长开放的角色(超级管理员)。
	ErrRoleLocked = errors.New("超级管理员角色不对外开放")
)

错误哨兵。

View Source
var (
	// ErrUserNotFound 表示用户不存在。
	ErrUserNotFound = errors.New("用户不存在")
	// ErrRoleNotFound 表示角色不存在。
	ErrRoleNotFound = errors.New("角色不存在")
	// ErrRoleInUse 表示角色仍被用户持有,不能删除。
	ErrRoleInUse = errors.New("角色仍在使用中")
	// ErrLastAdmin 表示该操作会让站点失去最后一名管理员。
	ErrLastAdmin = errors.New("不能移除最后一名管理员")
	// ErrSelfOperation 表示该操作不允许作用在自己身上。
	ErrSelfOperation = errors.New("不能对自己执行该操作")
	// ErrUserHasContent 表示用户仍拥有内容,不能删除。
	ErrUserHasContent = errors.New("用户仍有内容")
	// ErrRoleNotBuiltin 表示对自定义角色执行了只有内置角色才有的操作(恢复默认)。
	ErrRoleNotBuiltin = errors.New("自定义角色没有可恢复的默认权限")
)

用户与角色管理补充的错误哨兵。ErrNotFound / ErrDuplicate / ErrBuiltinRole 已在 store.go 中定义,这里不重复。

View Source
var ErrAccountDisabled = errors.New("账号已被停用")

ErrAccountDisabled 表示账号已被停用。

View Source
var ErrEmailUnverified = errors.New("邮箱未验证")

ErrEmailUnverified 表示账号的邮箱尚未验证。

View Source
var ErrInvalidCredentials = errors.New("用户名或密码错误")

ErrInvalidCredentials 表示登录凭据错误。

用户不存在与密码错误都返回同一个错误,避免通过响应差异枚举账号。

View Source
var ErrInvalidSession = errors.New("会话无效")

ErrInvalidSession 表示会话不存在、已过期或所属账号已停用。

View Source
var ErrInvalidToken = errors.New("访问令牌无效")

ErrInvalidToken 表示令牌不存在、已过期或所属账号已停用。

View Source
var ErrTooManyAttempts = errTooManyAttempts{}

ErrTooManyAttempts 表示登录尝试过于频繁。

Functions

func BearerToken

func BearerToken(header string) string

BearerToken 从 Authorization 头中提取 Bearer 令牌。

func ConstantTimeEqual

func ConstantTimeEqual(a, b string) bool

ConstantTimeEqual 是定长字符串比较,避免通过响应时间推断令牌内容。

func ForbiddenProblem

func ForbiddenProblem(required ...perm.Permission) *httpx.Problem

ForbiddenProblem 构造 403 错误,扩展成员 requiredPermissions 列出所需权限,便于前端提示与排错。

处理器内的所有权判定失败时直接返回它:*httpx.Problem 实现了 huma.StatusError。

func HasAdminRole

func HasAdminRole(roleNames []string) bool

HasAdminRole 报告用户的角色集合里是否含管理员角色。

func HashToken

func HashToken(token string) string

HashToken 返回令牌的 SHA-256 十六进制哈希。

会话与 PAT 均只存哈希:库泄漏时无法反推出可用凭据。 这里不需要加盐或慢哈希——令牌是 256 位随机值,不存在字典攻击面。

func NormalizeUsername

func NormalizeUsername(name string) string

NormalizeUsername 归一化用户名:去空白并转小写。

导出是给 account 模块用的:注册表单要先归一化再校验格式, 否则用户输入 "Alice" 会因为含大写而被自己的正则拒掉, 而落到库里其实是合法的 "alice"。

func RequireAuth

func RequireAuth(next http.Handler) http.Handler

RequireAuth 要求请求已认证。

func RequirePermission

func RequirePermission(permissions ...perm.Permission) func(huma.Context, func(huma.Context))

RequirePermission 是 huma 操作级中间件:要求调用者直接持有指定权限之一。

用法:

huma.Register(api, huma.Operation{
	Middlewares: huma.Middlewares{auth.RequirePermission(perm.UsersManage)},
	...
}, handler)

适用于与所有权无关的权限(如 users:manage)。涉及所有权的检查必须在处理器内 用 Principal.Allows 完成,失败时返回 ForbiddenProblem——中间件此时还不知道目标对象归属谁。

func WithPrincipal

func WithPrincipal(ctx context.Context, p *Principal) context.Context

WithPrincipal 把调用者放入 context。

Types

type AccessToken

type AccessToken struct {
	bun.BaseModel `bun:"table:access_tokens,alias:at"`

	ID        int64  `bun:"id,pk,autoincrement" json:"id"`
	TokenHash string `bun:"token_hash,notnull"  json:"-"`
	// TokenHint 是令牌前缀,仅用于 UI 区分,不足以用于认证。
	TokenHint string `bun:"token_hint" json:"tokenHint"`
	UserID    int64  `bun:"user_id,notnull" json:"userId"`
	Name      string `bun:"name,notnull"    json:"name"`
	// Scopes 为空表示继承用户的全部权限。
	Scopes     []perm.Permission `bun:"scopes,type:jsonb" json:"scopes"`
	ExpiresAt  *time.Time        `bun:"expires_at"        json:"expiresAt,omitempty"`
	LastUsedAt *time.Time        `bun:"last_used_at"      json:"lastUsedAt,omitempty"`
	CreatedAt  time.Time         `bun:"created_at,nullzero" json:"createdAt"`
}

AccessToken 是 Personal Access Token(仅存哈希)。

func (*AccessToken) Expired

func (t *AccessToken) Expired(now time.Time) bool

Expired 报告令牌是否已过期。ExpiresAt 为 nil 表示永不过期。

type AdminHandler

type AdminHandler struct {
	// contains filtered or unexported fields
}

AdminHandler 提供 Console 的用户与角色管理端点。

与 Handler 分开是因为二者的依赖与关注点都不同:认证端点只需要会话与令牌, 管理端点需要用户存储与密码校验,且权限要求完全不同。

func NewAdminHandler

func NewAdminHandler(store *Store, service *Service, perms func() []PermissionInfo) *AdminHandler

NewAdminHandler 构造 AdminHandler;perms 可为 nil,此时权限清单只有权限串本身。

func (*AdminHandler) Register

func (h *AdminHandler) Register(console huma.API)

Register 挂载管理端点,全部要求 users:manage 或 roles:manage。

type AuthMethod

type AuthMethod string

AuthMethod 标识请求的认证方式。

const (
	// MethodSession 表示经由会话 Cookie 认证(Console)。
	MethodSession AuthMethod = "session"
	// MethodToken 表示经由 Personal Access Token 认证(无头调用)。
	MethodToken AuthMethod = "token"
)

type Authenticator

type Authenticator struct {
	// contains filtered or unexported fields
}

Authenticator 把请求解析为调用者。

func NewAuthenticator

func NewAuthenticator(users *Store, sessions *SessionStore, tokens *TokenStore, logger *slog.Logger) *Authenticator

NewAuthenticator 构造 Authenticator。

func (*Authenticator) CSRF

func (a *Authenticator) CSRF(next http.Handler) http.Handler

CSRF 校验非安全方法的 CSRF 令牌(双提交模式)。

仅对会话认证生效:PAT 调用不带 Cookie,天然无 CSRF 风险, 若一并要求 CSRF 头会让无头客户端无法使用。

func (*Authenticator) Middleware

func (a *Authenticator) Middleware(next http.Handler) http.Handler

Middleware 解析凭据并注入 context,但**不**强制要求已认证。

凭据无效时(过期会话、被撤销的令牌)直接返回 401,而不是当作匿名继续—— 否则客户端会拿到令人困惑的 403 而不知道该重新登录。

func (*Authenticator) Optional

func (a *Authenticator) Optional(next http.Handler) http.Handler

Optional 解析会话但**不**强制认证,供访客前台的 HTML 页面使用。

与 Middleware 的区别只有一条,但很要紧:凭据无效时 Middleware 返回 401 (API 客户端必须明确知道该重新登录),而这里清掉失效 Cookie 后按匿名继续—— 访客带着一枚过期 Cookie 打开首页,该看到首页,而不是一段 JSON 错误。 解析过程本身出错(数据库故障)同样按匿名继续并记一条 Warn: 一次会话查询失败不该让整站不可访问。

只对「会话本身失效」清 Cookie,对无效的 PAT 不清:两者可能同时存在 (浏览器里既有会话 Cookie,某个扩展又带了 Authorization 头), 为一枚坏令牌把好好的会话 Cookie 抹掉,会把用户从已登录状态踢出去。

func (*Authenticator) Resolve

func (a *Authenticator) Resolve(r *http.Request) (*Principal, error)

Resolve 解析请求中的凭据。

优先 Authorization: Bearer(无头调用),其次会话 Cookie(Console)。 无凭据时返回 (nil, nil):区分「未认证」与「凭据无效」, 前者对公共端点是合法状态。

type Core

type Core struct {
	Users         *Store
	Sessions      *SessionStore
	Tokens        *TokenStore
	Service       *Service
	Authenticator *Authenticator
}

Core 是核心自带的认证栈。

抽成一个类型是为了让功能模块能经 app.Lookup 取到**同一批实例**,而不是各自 new 一套: LoginLimiter 是进程内存计数,模块各建一份等于把限流额度翻倍——前台登录 就成了绕过后台登录限流的旁路。同理,会话存储的 Secure 开关、 argon2 的并发额度、改密踢下线的删除范围,都必须是同一份。

注意本包**不得 import internal/app**:app 已被 theme 等模块依赖, 反向引用会绕成导入环。因此取值函数(From)写在消费方,不写在这里。

func NewCore

func NewCore(db bun.IDB, secureCookies bool, logger *slog.Logger) *Core

NewCore 构造认证栈。只做装配、不访问数据库,可在迁移之前调用。

type CreateTokenParams

type CreateTokenParams struct {
	UserID int64
	Name   string
	// Scopes 是令牌的权限清单,**必须是已经解析好的显式列表**:
	// 空列表表示这枚令牌没有任何权限,而不是「继承全部」。
	// 「继承账号全部权限」由 handlers 的 resolveScopes 展开后再传进来。
	Scopes []perm.Permission
	// ExpiresAt 为 nil 表示永不过期。
	ExpiresAt *time.Time
}

CreateTokenParams 是创建令牌的入参。

type CreateUserParams

type CreateUserParams struct {
	Username    string
	Email       string
	Password    string
	DisplayName string
	Roles       []string
	// EmailVerified 为真时把 email_verified_at 一并写上。
	//
	// 后台建号(Console 用户管理、`lumo admin create-user`、Bootstrap)一律传 true:
	// 管理员当面给的账号再要求对方去收信毫无意义,而且站点没配 SMTP 时那封信根本发不出去。
	// 只有前台自助注册传 false —— 那条路径上的账号必须自己证明邮箱可达。
	EmailVerified bool
}

CreateUserParams 是创建用户的入参。

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler 提供 Console 认证端点。

func NewHandler

func NewHandler(service *Service, sessions *SessionStore, tokens *TokenStore, logger *slog.Logger) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(consolePublic, console huma.API)

Register 把认证端点挂到 Console 平面:登录走免认证注册面,其余走强制认证注册面。

type IssuedSession

type IssuedSession struct {
	Session   *Session
	Token     string
	CSRFToken string
}

IssuedSession 是新建会话的结果,含仅此一次可见的令牌明文。

type IssuedToken

type IssuedToken struct {
	Token *AccessToken
	// Plaintext 是令牌明文,**只在创建时返回一次**,此后无法找回。
	Plaintext string
}

IssuedToken 是新建令牌的结果,Plaintext 仅在此刻可见。

type LoginLimiter

type LoginLimiter struct {
	// contains filtered or unexported fields
}

LoginLimiter 按账号与客户端 IP 两个维度限制登录失败次数。

内存实现,单进程有效:多实例部署时每个实例各自计数,防护会被摊薄 (限流阈值随实例数放大)。真正的修复要在网关或共享存储上做, 这里先把单实例的洞堵住,并在文档里写明这一限制。

func NewLoginLimiter

func NewLoginLimiter() *LoginLimiter

NewLoginLimiter 构造使用默认阈值的限流器。

func NewLoginLimiterWith

func NewLoginLimiterWith(accountLimit, ipLimit int, window time.Duration) *LoginLimiter

NewLoginLimiterWith 用指定阈值构造限流器。

阈值可调是为了让测试能用小额度跑完整链路;运维若要放宽或收紧, 也应该走这里而不是改常量(常量只是默认值)。

func (*LoginLimiter) Allow

func (l *LoginLimiter) Allow(account, ip string) error

Allow 报告给定账号与 IP 当前是否还有尝试额度。

account 是提交的登录名(可以是用户名或邮箱,也可能是根本不存在的账号)—— 按提交值计数正是我们要的:撞库与枚举都会命中。

func (*LoginLimiter) RecordFailure

func (l *LoginLimiter) RecordFailure(account, ip string)

RecordFailure 记一次登录失败。只有失败才计入额度。

func (*LoginLimiter) ResetAccount

func (l *LoginLimiter) ResetAccount(account string)

ResetAccount 清空某账号的失败计数,供成功登录与改密码后调用。

IP 维度的计数**不**清:一次成功登录不该让同一 IP 上的其他猜测重新开始。

type LoginParams

type LoginParams struct {
	// Login 可以是用户名或邮箱。
	Login     string
	Password  string
	UserAgent string
	IP        string
}

LoginParams 是登录入参。

type PermissionInfo

type PermissionInfo struct {
	Key         string
	Label       string
	Description string
}

PermissionInfo 是一条权限的展示信息,由各模块声明、核心汇总。

type Principal

type Principal struct {
	User   *User
	Method AuthMethod
	// TokenID 仅在 Method 为 MethodToken 时有值。
	TokenID int64
	// Session 仅在 Method 为 MethodSession 时由鉴权中间件填充,
	// 供登出与 CSRF 校验直接使用,免去再次查库。
	Session *Session
	// contains filtered or unexported fields
}

Principal 是已认证的调用者。

func FromContext

func FromContext(ctx context.Context) (*Principal, bool)

FromContext 取出调用者,未认证时返回 nil 与 false。

func MustFromContext

func MustFromContext(ctx context.Context) *Principal

MustFromContext 取出调用者,未认证时返回零值 Principal。

供已在鉴权中间件之后的处理器使用,避免每处都写 ok 判断; 零值 Principal 的所有权限判定均为 false,故失败是安全的。

func NewSessionPrincipal

func NewSessionPrincipal(user *User) *Principal

NewSessionPrincipal 构造会话调用者。

func NewTokenPrincipal

func NewTokenPrincipal(user *User, token *AccessToken) *Principal

NewTokenPrincipal 构造令牌调用者。

令牌 scope 只能收窄权限、不能放大:有效权限取用户权限与 scope 的交集。 否则一个 author 的令牌若被写入 users:manage,就会凭空获得越权能力。

**空 scope 表示没有任何权限**,不表示「继承账号全部权限」:后者会让 一行被改空(或历史上按旧语义创建)的 scopes 直接变成账号全权限。 签发时表示「继承全部」的空值已在 resolveScopes 里显式展开。

func (*Principal) Allows

func (p *Principal) Allows(permission perm.Permission, ownerID int64) bool

Allows 在给定所有权前提下判定权限。

ownerID 为目标对象的所有者用户 ID;传 0 表示对象无所有者概念。

func (*Principal) Has

func (p *Principal) Has(permission perm.Permission) bool

Has 报告调用者是否直接持有该权限(不考虑所有权)。

func (*Principal) IsSuperAdmin

func (p *Principal) IsSuperAdmin() bool

IsSuperAdmin 报告调用者是否为超级管理员。

func (*Principal) Permissions

func (p *Principal) Permissions() perm.Set

Permissions 返回本次调用的有效权限集合。

func (*Principal) UserID

func (p *Principal) UserID() int64

UserID 返回调用者的用户 ID,未认证时返回 0。

type Role

type Role struct {
	bun.BaseModel `bun:"table:roles,alias:r"`

	ID          int64             `bun:"id,pk,autoincrement" json:"id"`
	Name        string            `bun:"name,notnull"        json:"name"`
	Label       string            `bun:"label"               json:"label"`
	Description string            `bun:"description"         json:"description"`
	Permissions []perm.Permission `bun:"permissions,type:jsonb" json:"permissions"`
	Builtin     bool              `bun:"builtin"             json:"builtin"`
	// Locked 为真表示该角色不对站长开放:不可改、不可删,也不在角色管理页出现
	// (仍可分配给用户)。目前只有 super-admin 是锁定的。
	Locked bool `bun:"-" json:"locked"`
	// Customized 为真表示内置角色的权限已被站长改过,与代码里的默认值不同。
	// 界面据此提示「已自定义」并给出「恢复默认」;自定义角色恒为假。
	Customized bool      `bun:"-" json:"customized"`
	CreatedAt  time.Time `bun:"created_at,nullzero" json:"createdAt"`
	UpdatedAt  time.Time `bun:"updated_at,nullzero" json:"updatedAt"`
}

Role 是角色实体,本质是一组权限串。

func (*Role) DisplayLabel

func (r *Role) DisplayLabel() string

DisplayLabel 返回用于展示的角色名,显示名缺失时回退到标识。

显示名可以由站长改(内置角色的名字也随代码更新),故它只能从服务端取, 前端写死一张表就会在下一次改档时漂掉。

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service 编排登录、登出与密码变更。

func NewService

func NewService(users *Store, sessions *SessionStore, tokens *TokenStore, logger *slog.Logger) *Service

NewService 构造 Service。

func (*Service) Bootstrap

func (s *Service) Bootstrap(ctx context.Context, params *CreateUserParams) (*User, error)

Bootstrap 在无任何用户时创建首个超级管理员。

返回创建的用户;若已存在用户则返回 nil。

func (*Service) ChangePassword

func (s *Service) ChangePassword(ctx context.Context, userID int64, oldPlain, newPlain string) error

ChangePassword 校验旧密码后设置新密码。

成功后销毁该用户的全部会话与令牌:改密码必须使所有既有凭据失效, 否则「改了密码却没踢下线」会成为严重的安全缺陷。

func (*Service) CleanupExpiredSessions

func (s *Service) CleanupExpiredSessions(ctx context.Context) error

CleanupExpiredSessions 清理过期会话,供定期任务调用。

func (*Service) Disable

func (s *Service) Disable(ctx context.Context, userID int64) error

Disable 停用账号并清除其全部凭据。

func (*Service) Login

func (s *Service) Login(ctx context.Context, params LoginParams) (*IssuedSession, *User, error)

Login 校验凭据并签发会话。

失败尝试按账号与客户端 IP 两个维度计数(见 LoginLimiter)。计数在 **任何数据库访问之前**判定:被限流的请求不该再去花一次 64 MiB 的 argon2 开销, 否则限流本身就成了一条放大路径。

func (*Service) Logout

func (s *Service) Logout(ctx context.Context, sessionToken string) error

Logout 销毁指定会话。

func (*Service) LogoutSession

func (s *Service) LogoutSession(ctx context.Context, session *Session) error

LogoutSession 销毁已解析的会话;session 为 nil(如令牌调用)时不做任何事。

func (*Service) ResetPassword

func (s *Service) ResetPassword(ctx context.Context, userID int64, newPlain string) error

ResetPassword 强制设置新密码并使既有凭据全部失效。

供 `lumo admin reset-password` 与管理员操作使用,不校验旧密码。

func (*Service) SetLoginLimiter

func (s *Service) SetLoginLimiter(limiter *LoginLimiter)

SetLoginLimiter 替换限流器,供测试注入可控制时间的实例。

func (*Service) StartSessionCleanup

func (s *Service) StartSessionCleanup(ctx context.Context, interval time.Duration)

StartSessionCleanup 启动后台会话清理循环,ctx 取消时退出。

func (*Service) VerifyPassword

func (s *Service) VerifyPassword(ctx context.Context, userID int64, plain string) error

VerifyPassword 校验用户当前的口令,供敏感操作的二次确认使用。

自己重新查一次用户而不是复用调用者的 User:调用链上拿到的用户来自鉴权中间件, 其字段集合是「按需加载」的,这里需要的是口令哈希,显式查库更稳。

type Session

type Session struct {
	bun.BaseModel `bun:"table:sessions,alias:s"`

	TokenHash  string    `bun:"token_hash,pk"`
	UserID     int64     `bun:"user_id,notnull"`
	CSRFToken  string    `bun:"csrf_token,notnull"`
	UserAgent  string    `bun:"user_agent"`
	IP         string    `bun:"ip"`
	ExpiresAt  time.Time `bun:"expires_at,notnull"`
	CreatedAt  time.Time `bun:"created_at,nullzero"`
	LastSeenAt time.Time `bun:"last_seen_at,nullzero"`
}

Session 是服务端会话。

TokenHash 是会话令牌的哈希;令牌明文只存在于客户端 Cookie 中。

func (*Session) Expired

func (s *Session) Expired(now time.Time) bool

Expired 报告会话是否已过期。

type SessionStore

type SessionStore struct {

	// Secure 决定是否设置 Secure 属性与 __Host- 前缀,生产环境必须为真。
	Secure bool
	// contains filtered or unexported fields
}

SessionStore 管理服务端会话。

func NewSessionStore

func NewSessionStore(db bun.IDB, secure bool) *SessionStore

NewSessionStore 构造 SessionStore。

func (*SessionStore) CSRFCookieName

func (s *SessionStore) CSRFCookieName() string

CSRFCookieName 返回当前模式下的 CSRF Cookie 名。

func (*SessionStore) ClearCookies

func (s *SessionStore) ClearCookies(w http.ResponseWriter)

ClearCookies 清除会话与 CSRF Cookie。

func (*SessionStore) ClearedCookies

func (s *SessionStore) ClearedCookies() []string

ClearedCookies 返回用于清除会话与 CSRF Cookie 的 Set-Cookie 头值。

func (*SessionStore) Cookies

func (s *SessionStore) Cookies(issued *IssuedSession) []string

Cookies 返回登录成功后应下发的会话与 CSRF Cookie,已序列化为 Set-Cookie 头值。

供 huma 操作以响应头字段输出;属性与 SetCookies 完全一致。

func (*SessionStore) Create

func (s *SessionStore) Create(ctx context.Context, userID int64, userAgent, ip string) (*IssuedSession, error)

Create 为用户签发新会话。

func (*SessionStore) Delete

func (s *SessionStore) Delete(ctx context.Context, token string) error

Delete 按令牌明文删除会话(登出)。

func (*SessionStore) DeleteAllForUser

func (s *SessionStore) DeleteAllForUser(ctx context.Context, userID int64) error

DeleteAllForUser 删除某用户的全部会话。

改密码、停用账号、撤销授权时必须调用:否则旧会话仍然有效, 「改了密码却没踢下线」是常见且严重的安全缺陷。

func (*SessionStore) DeleteByHash

func (s *SessionStore) DeleteByHash(ctx context.Context, tokenHash string) error

DeleteByHash 按令牌哈希删除会话,供已持有会话记录的调用方(如登出)使用。

func (*SessionStore) DeleteExpired

func (s *SessionStore) DeleteExpired(ctx context.Context) (int64, error)

DeleteExpired 清理过期会话,返回删除条数。

func (*SessionStore) Lookup

func (s *SessionStore) Lookup(ctx context.Context, token string) (*Session, error)

Lookup 按令牌明文查找有效会话,并返回其所属用户。

过期会话会被顺带删除,避免表无限增长。

func (*SessionStore) SessionCookieName

func (s *SessionStore) SessionCookieName() string

SessionCookieName 返回当前模式下的会话 Cookie 名。

func (*SessionStore) SetCookies

func (s *SessionStore) SetCookies(w http.ResponseWriter, issued *IssuedSession)

SetCookies 写入会话与 CSRF Cookie。

会话 Cookie 为 HttpOnly,前端 JS 读不到,防止 XSS 直接窃取会话; CSRF Cookie 必须可被 JS 读取(双提交模式),故不设 HttpOnly。 两者均为 SameSite=Lax。

func (*SessionStore) TokenFromRequest

func (s *SessionStore) TokenFromRequest(r *http.Request) string

TokenFromRequest 从请求 Cookie 中取出会话令牌明文。

func (*SessionStore) Touch

func (s *SessionStore) Touch(ctx context.Context, session *Session) error

Touch 在超过阈值时更新会话的最近活跃时间并滑动延长有效期。

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 提供用户与角色的持久化操作。

func NewStore

func NewStore(db bun.IDB) *Store

NewStore 构造 Store。

func (*Store) AssignRoles

func (s *Store) AssignRoles(ctx context.Context, userID int64, roleNames []string) error

AssignRoles 覆盖式设置用户角色。

func (*Store) CountAdmins

func (s *Store) CountAdmins(ctx context.Context) (int, error)

CountAdmins 统计拥有管理员角色的用户数。

func (*Store) CountUsers

func (s *Store) CountUsers(ctx context.Context) (int, error)

CountUsers 返回用户总数,用于判断是否需要初始化首个管理员。

func (*Store) CreateRole

func (s *Store) CreateRole(ctx context.Context, role *Role) error

CreateRole 创建自定义角色。

func (*Store) CreateUser

func (s *Store) CreateUser(ctx context.Context, params *CreateUserParams) (*User, error)

CreateUser 创建用户并授予角色。

密码在此处哈希,调用方不应接触哈希逻辑。

func (*Store) CustomRolePermissions

func (s *Store) CustomRolePermissions(ctx context.Context) (map[string][]perm.Permission, error)

CustomRolePermissions 返回自定义角色的权限映射,供权限汇总使用。

func (*Store) DeleteRole

func (s *Store) DeleteRole(ctx context.Context, roleID int64) error

DeleteRole 删除自定义角色;仍被用户持有时拒绝。

func (*Store) DeleteUser

func (s *Store) DeleteUser(ctx context.Context, id int64) error

DeleteUser 删除用户及其角色关联。

用户发布过的内容通过外键 RESTRICT 阻止删除:那些内容仍需要作者。 需要保留内容时正确做法是停用账号,而不是删人。

func (*Store) FindRoleByName

func (s *Store) FindRoleByName(ctx context.Context, name string) (*Role, error)

FindRoleByName 按名称查角色。

func (*Store) FindUserByID

func (s *Store) FindUserByID(ctx context.Context, id int64) (*User, error)

FindUserByID 按 ID 查用户。

func (*Store) FindUserByLogin

func (s *Store) FindUserByLogin(ctx context.Context, login string) (*User, error)

FindUserByLogin 按用户名或邮箱查用户,两者均大小写不敏感。

func (*Store) FindUsersByIDs

func (s *Store) FindUsersByIDs(ctx context.Context, ids []int64) (map[int64]*User, error)

FindUsersByIDs 按 ID 批量查用户,不加载角色;供内容模块给文章附上作者信息。

返回映射而非切片:调用方按 ID 取用,不存在的 ID 不会出现在映射中。

func (*Store) GetUser

func (s *Store) GetUser(ctx context.Context, id int64) (*User, error)

GetUser 按 ID 查用户并加载角色。

func (*Store) ListRoles

func (s *Store) ListRoles(ctx context.Context) ([]Role, error)

ListRoles 返回全部角色。

func (*Store) LoadRoles

func (s *Store) LoadRoles(ctx context.Context, user *User) error

LoadRoles 填充用户的角色列表。

func (*Store) PageUsers

func (s *Store) PageUsers(ctx context.Context, filter UserFilter, params api.PageParams) ([]User, int, error)

PageUsers 分页返回用户(含角色),按创建时间倒序。

func (*Store) ResetRole

func (s *Store) ResetRole(ctx context.Context, roleID int64) error

ResetRole 把内置角色的权限、显示名与描述恢复成代码里的默认值。

存在的理由:内置角色的权限只在首次创建时播种(见 SeedRoles),升级新增的权限 不会自动补上。有这一条,「升级后编辑角色缺了新权限」才有自助解法, 而不是要站长去翻本文档再逐条勾回来。

func (*Store) RoleByName

func (s *Store) RoleByName(ctx context.Context, name string) (*Role, error)

RoleByName 按名称查角色。

func (*Store) SeedRoles

func (s *Store) SeedRoles(ctx context.Context) error

SeedRoles 幂等地写入内置角色。

每次启动都执行,但只对两类字段负责:

  • 显示名与描述**随代码覆盖** —— 措辞是产品的一部分,升级就该更新;
  • 权限集合**只在角色首次创建时写入**。2026-09-15 起内置角色的权限归站长 (三挡角色都能在角色管理页自由勾选),每次启动覆盖回去等于无声改写站长的选择: 升级时新增一条权限本可以自动放宽,而删掉一条权限却可能让正在用的账号突然 403。

super-admin 例外(perm.Locked):它不对站长开放,权限每次都按代码覆盖。 代价是升级新增的权限不会自动落到已被改过的内置角色上,故角色管理页提供「恢复默认」。

func (*Store) SetDisabled

func (s *Store) SetDisabled(ctx context.Context, userID int64, disabled bool) error

SetDisabled 启用或停用账号。

func (*Store) TouchLastLogin

func (s *Store) TouchLastLogin(ctx context.Context, userID int64) error

TouchLastLogin 记录最近登录时间。

func (*Store) UpdatePassword

func (s *Store) UpdatePassword(ctx context.Context, userID int64, plain string) error

UpdatePassword 重设用户密码。

func (*Store) UpdateRole

func (s *Store) UpdateRole(ctx context.Context, roleID int64, label, description string, permissions []perm.Permission) error

UpdateRole 更新角色的显示名、描述与权限。

内置角色自 2026-09-15 起可改:三挡角色(用户 / 编辑 / 管理员)的权限由站长决定, 名字仍是不可改的键。super-admin 整条锁定 —— 把它的权限改坏等于拆掉唯一的后门。

func (*Store) UpdateUserProfile

func (s *Store) UpdateUserProfile(ctx context.Context, userID int64, email, displayName, avatarURL, bio string) error

UpdateUserProfile 更新用户的展示信息,不动口令与角色。

type TokenStore

type TokenStore struct {
	// contains filtered or unexported fields
}

TokenStore 管理 Personal Access Token。

func NewTokenStore

func NewTokenStore(db bun.IDB) *TokenStore

NewTokenStore 构造 TokenStore。

func (*TokenStore) Create

func (s *TokenStore) Create(ctx context.Context, params *CreateTokenParams) (*IssuedToken, error)

Create 为用户签发新的访问令牌。

func (*TokenStore) ListForUser

func (s *TokenStore) ListForUser(ctx context.Context, userID int64) ([]AccessToken, error)

ListForUser 返回用户的令牌列表(不含明文与哈希)。

func (*TokenStore) Lookup

func (s *TokenStore) Lookup(ctx context.Context, plaintext string) (*AccessToken, error)

Lookup 按令牌明文查找有效令牌。

func (*TokenStore) Revoke

func (s *TokenStore) Revoke(ctx context.Context, userID, tokenID int64) error

Revoke 删除用户的某个令牌。

以 user_id 一并过滤,防止越权删除他人令牌。

func (*TokenStore) RevokeAllForUser

func (s *TokenStore) RevokeAllForUser(ctx context.Context, userID int64) error

RevokeAllForUser 删除用户的全部令牌,用于停用账号或重设密码。

func (*TokenStore) TouchLastUsed

func (s *TokenStore) TouchLastUsed(ctx context.Context, id int64) error

TouchLastUsed 记录令牌最近使用时间。

失败不应影响请求:这只是审计信息。

type User

type User struct {
	bun.BaseModel `bun:"table:users,alias:u"`

	ID       int64  `bun:"id,pk,autoincrement" json:"id"`
	Username string `bun:"username,notnull"    json:"username"`
	Email    string `bun:"email,notnull"       json:"email"`
	// EmailVerifiedAt 为 nil 表示邮箱未验证,该账号不能登录(见 Service.Login)。
	// 后台创建的账号在创建时即置为已验证:管理员当面给的号,再让他去收信毫无意义。
	//
	// json:"-" 而不是导出这个时间值:管理接口直接序列化 User,
	// 而「哪一秒验证的」除了拼出一条精确到秒的账号活动轨迹之外没有任何用处。
	// Console 需要的是「这个号能不能登录」,那是 userView.EmailVerified 那个布尔值的事。
	EmailVerifiedAt *time.Time `bun:"email_verified_at" json:"-"`
	// PasswordHash 绝不出现在 JSON 中:json:"-" 是防止口令哈希经 API 泄漏的第一道防线。
	PasswordHash string `bun:"password_hash,notnull" json:"-"`
	DisplayName  string `bun:"display_name"          json:"displayName"`
	AvatarURL    string `bun:"avatar_url"            json:"avatarUrl"`
	Bio          string `bun:"bio"                   json:"bio"`
	// BannerURL 是个人中心的封面图(前台自己传,后台不管)。
	// 与 AvatarURL 同源:都指向媒体库里的一条记录,都只存地址不存文件。
	BannerURL   string     `bun:"banner_url"            json:"bannerUrl"`
	Disabled    bool       `bun:"disabled"              json:"disabled"`
	LastLoginAt *time.Time `bun:"last_login_at"         json:"lastLoginAt,omitempty"`
	CreatedAt   time.Time  `bun:"created_at,nullzero"   json:"createdAt"`
	UpdatedAt   time.Time  `bun:"updated_at,nullzero"   json:"updatedAt"`

	// Roles 由 LoadRoles 填充,不是数据库列。
	Roles []Role `bun:"-" json:"roles,omitempty"`
}

User 是用户实体。

func (*User) EmailVerified

func (u *User) EmailVerified() bool

EmailVerified 报告账号的邮箱是否已验证。

判定写在这里而不是散落各处:登录闸门、Console 视图与账户页读的必须是同一个判据。

func (*User) Name

func (u *User) Name() string

Name 返回用于展示的名称,显示名缺失时回退到用户名。

func (*User) Permissions

func (u *User) Permissions() perm.Set

Permissions 汇总用户所有角色的权限并集。

func (*User) RoleNames

func (u *User) RoleNames() []string

RoleNames 返回用户的角色名列表。

type UserFilter

type UserFilter struct {
	// Role 非空时只返回拥有该角色的用户。
	Role string
	// Status 取 enabled 或 disabled。
	Status string
	// Q 非空时按用户名、邮箱或显示名模糊筛选。
	Q string
}

UserFilter 是用户列表的筛选条件。

type UserRole

type UserRole struct {
	bun.BaseModel `bun:"table:user_roles,alias:ur"`

	UserID    int64     `bun:"user_id,pk"`
	RoleID    int64     `bun:"role_id,pk"`
	GrantedAt time.Time `bun:"granted_at,nullzero"`
}

UserRole 是用户与角色的关联。

Directories

Path Synopsis
Package password 提供 argon2id 密码哈希与校验。
Package password 提供 argon2id 密码哈希与校验。
Package perm 定义权限串、内置角色与所有权规则。
Package perm 定义权限串、内置角色与所有权规则。

Jump to

Keyboard shortcuts

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