Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v1.0.0 Latest Latest
Warning

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

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

Documentation

Overview

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

Index

Constants

View Source
const (
	StatusEnabled  = "enabled"
	StatusDisabled = "disabled"
	// EmailVerified 与 EmailUnverified 是邮箱验证状态的筛选取值。
	EmailVerified   = "verified"
	EmailUnverified = "unverified"
)

用户状态筛选取值。

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 HookUser added in v0.2.0

func HookUser(u *User) hooks.User

Name 返回用于展示的名称,显示名缺失时回退到用户名。 HookUser 把用户转成派给插件的动作数据:只有公开资料。

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 为空表示没有任何权限;签发时省略 scope 会展开成调用者当时的全部权限再落库。
	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, events app.Events) *AdminHandler

NewAdminHandler 构造 AdminHandler;perms 可为 nil,此时权限清单只有权限串本身;events 为 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
	// Limits 是共享的限流计数,前台账户模块的注册、找回密码等限流也记在这里。
	Limits *ratelimit.Counter
}

Core 是核心自带的认证栈。

抽成一个类型是为了让功能模块能经 app.Lookup 取到**同一批实例**,而不是各自 new 一套: 前台登录必须走同一个 Service,否则就成了绕过后台登录限流的旁路。同理,会话存储的 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 两个维度限制登录失败次数。

计数存在数据库里(见 ratelimit 包),多实例共享同一份额度。 读写计数出错时放行并记 warn:数据库都不可用时登录本身也会失败, 为限流把整条登录链路打成 500 不划算。

func NewLoginLimiter

func NewLoginLimiter(counter *ratelimit.Counter, logger *slog.Logger) *LoginLimiter

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

func (*LoginLimiter) Allow

func (l *LoginLimiter) Allow(ctx context.Context, account, ip string) error

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

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

func (*LoginLimiter) RecordFailure

func (l *LoginLimiter) RecordFailure(ctx context.Context, account, ip string)

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

func (*LoginLimiter) ResetAccount

func (l *LoginLimiter) ResetAccount(ctx context.Context, 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, counter *ratelimit.Counter, logger *slog.Logger) *Service

NewService 构造 Service。counter 为 nil 时不做登录限流。

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) 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) StartCleanup added in v0.1.9

func (s *Service) StartCleanup(ctx context.Context, db bun.IDB, interval time.Duration)

StartCleanup 启动后台周期清理,ctx 取消时退出。

多实例部署时每个实例的定时器都会触发,经 database.ClaimRun 认领后每轮只有一个实例真正执行。

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 需要的是「这个号能不能登录」,那是下面 Verified 那个布尔值的事。
	EmailVerifiedAt *time.Time `bun:"email_verified_at" json:"-"`
	// Verified 由 EmailVerifiedAt 推出,扫描每一行后填充(见 AfterScanRow),不是数据库列。
	Verified bool `bun:"-" json:"emailVerified"`
	// 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) AfterScanRow added in v0.1.9

func (u *User) AfterScanRow(context.Context) error

AfterScanRow 实现 bun.AfterScanRowHook:每读出一行就同步 Verified, 接口序列化 User 时这个字段因此永远与数据库一致。

func (*User) EmailVerified

func (u *User) EmailVerified() bool

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

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

func (*User) Name

func (u *User) Name() string

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
	// Email 取 verified 或 unverified,按邮箱是否已验证筛选。
	Email 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