Documentation
¶
Overview ¶
Package auth 提供用户、角色、会话与访问令牌的存储与鉴权逻辑。
Index ¶
- Constants
- Variables
- func BearerToken(header string) string
- func ConstantTimeEqual(a, b string) bool
- func ForbiddenProblem(required ...perm.Permission) *httpx.Problem
- func HasAdminRole(roleNames []string) bool
- func HashToken(token string) string
- func HookUser(u *User) hooks.User
- func NormalizeUsername(name string) string
- func RequireAuth(next http.Handler) http.Handler
- func RequirePermission(permissions ...perm.Permission) func(huma.Context, func(huma.Context))
- func WithPrincipal(ctx context.Context, p *Principal) context.Context
- type AccessToken
- type AdminHandler
- type AuthMethod
- type Authenticator
- type Core
- type CreateTokenParams
- type CreateUserParams
- type Handler
- type IssuedSession
- type IssuedToken
- type LoginLimiter
- type LoginParams
- type PermissionInfo
- type Principal
- type Role
- type Service
- func (s *Service) Bootstrap(ctx context.Context, params *CreateUserParams) (*User, error)
- func (s *Service) ChangePassword(ctx context.Context, userID int64, oldPlain, newPlain string) error
- func (s *Service) Disable(ctx context.Context, userID int64) error
- func (s *Service) Login(ctx context.Context, params LoginParams) (*IssuedSession, *User, error)
- func (s *Service) Logout(ctx context.Context, sessionToken string) error
- func (s *Service) LogoutSession(ctx context.Context, session *Session) error
- func (s *Service) ResetPassword(ctx context.Context, userID int64, newPlain string) error
- func (s *Service) StartCleanup(ctx context.Context, db bun.IDB, interval time.Duration)
- func (s *Service) VerifyPassword(ctx context.Context, userID int64, plain string) error
- type Session
- type SessionStore
- func (s *SessionStore) CSRFCookieName() string
- func (s *SessionStore) ClearCookies(w http.ResponseWriter)
- func (s *SessionStore) ClearedCookies() []string
- func (s *SessionStore) Cookies(issued *IssuedSession) []string
- func (s *SessionStore) Create(ctx context.Context, userID int64, userAgent, ip string) (*IssuedSession, error)
- func (s *SessionStore) Delete(ctx context.Context, token string) error
- func (s *SessionStore) DeleteAllForUser(ctx context.Context, userID int64) error
- func (s *SessionStore) DeleteByHash(ctx context.Context, tokenHash string) error
- func (s *SessionStore) DeleteExpired(ctx context.Context) (int64, error)
- func (s *SessionStore) Lookup(ctx context.Context, token string) (*Session, error)
- func (s *SessionStore) SessionCookieName() string
- func (s *SessionStore) SetCookies(w http.ResponseWriter, issued *IssuedSession)
- func (s *SessionStore) TokenFromRequest(r *http.Request) string
- func (s *SessionStore) Touch(ctx context.Context, session *Session) error
- type Store
- func (s *Store) AssignRoles(ctx context.Context, userID int64, roleNames []string) error
- func (s *Store) CountAdmins(ctx context.Context) (int, error)
- func (s *Store) CountUsers(ctx context.Context) (int, error)
- func (s *Store) CreateRole(ctx context.Context, role *Role) error
- func (s *Store) CreateUser(ctx context.Context, params *CreateUserParams) (*User, error)
- func (s *Store) CustomRolePermissions(ctx context.Context) (map[string][]perm.Permission, error)
- func (s *Store) DeleteRole(ctx context.Context, roleID int64) error
- func (s *Store) DeleteUser(ctx context.Context, id int64) error
- func (s *Store) FindRoleByName(ctx context.Context, name string) (*Role, error)
- func (s *Store) FindUserByID(ctx context.Context, id int64) (*User, error)
- func (s *Store) FindUserByLogin(ctx context.Context, login string) (*User, error)
- func (s *Store) FindUsersByIDs(ctx context.Context, ids []int64) (map[int64]*User, error)
- func (s *Store) GetUser(ctx context.Context, id int64) (*User, error)
- func (s *Store) ListRoles(ctx context.Context) ([]Role, error)
- func (s *Store) LoadRoles(ctx context.Context, user *User) error
- func (s *Store) PageUsers(ctx context.Context, filter UserFilter, params api.PageParams) ([]User, int, error)
- func (s *Store) ResetRole(ctx context.Context, roleID int64) error
- func (s *Store) RoleByName(ctx context.Context, name string) (*Role, error)
- func (s *Store) SeedRoles(ctx context.Context) error
- func (s *Store) SetDisabled(ctx context.Context, userID int64, disabled bool) error
- func (s *Store) TouchLastLogin(ctx context.Context, userID int64) error
- func (s *Store) UpdatePassword(ctx context.Context, userID int64, plain string) error
- func (s *Store) UpdateRole(ctx context.Context, roleID int64, label, description string, ...) error
- func (s *Store) UpdateUserProfile(ctx context.Context, userID int64, email, displayName, avatarURL, bio string) error
- type TokenStore
- func (s *TokenStore) Create(ctx context.Context, params *CreateTokenParams) (*IssuedToken, error)
- func (s *TokenStore) ListForUser(ctx context.Context, userID int64) ([]AccessToken, error)
- func (s *TokenStore) Lookup(ctx context.Context, plaintext string) (*AccessToken, error)
- func (s *TokenStore) Revoke(ctx context.Context, userID, tokenID int64) error
- func (s *TokenStore) RevokeAllForUser(ctx context.Context, userID int64) error
- func (s *TokenStore) TouchLastUsed(ctx context.Context, id int64) error
- type User
- type UserFilter
- type UserRole
Constants ¶
const ( StatusEnabled = "enabled" StatusDisabled = "disabled" // EmailVerified 与 EmailUnverified 是邮箱验证状态的筛选取值。 EmailVerified = "verified" EmailUnverified = "unverified" )
用户状态筛选取值。
const (
// CSRFHeaderName 是前端提交 CSRF 令牌的请求头。
CSRFHeaderName = "X-CSRF-Token"
)
会话与 CSRF 的 Cookie 名。
__Host- 前缀要求 Secure + Path=/ 且不带 Domain,能防止子域写入伪造 Cookie; 但它同时要求 HTTPS,故仅在 secure 模式下启用(见 cookieName)。
const CoreKey = "auth-core"
CoreKey 是核心认证栈在 app 容器里的登记名。
放在 auth 包里而不是使用方:登记名与类型是一件事,分开放迟早会出现 「Provide 用了这个名字、Lookup 拼成了另一个」这种只在运行时暴露的错。
const ( // SessionTTL 是会话有效期。 SessionTTL = 7 * 24 * time.Hour )
会话时长。
const TokenPrefix = "lumo_pat_"
TokenPrefix 是 PAT 明文的固定前缀。
有了它,令牌一旦被误贴到公开场所(issue、日志、代码库), 可以被 secret 扫描工具按前缀识别出来。
Variables ¶
var ( // ErrNotFound 表示实体不存在。 ErrNotFound = errors.New("对象不存在") // ErrDuplicate 表示唯一约束冲突(用户名或邮箱已存在)。 ErrDuplicate = errors.New("对象已存在") // ErrBuiltinRole 表示试图删除内置角色。 // // 内置角色的权限自 2026-09-15 起可改,故这个哨兵现在只剩「不可删」这一层含义。 ErrBuiltinRole = errors.New("内置角色不可删除") // ErrRoleLocked 表示试图修改不对站长开放的角色(超级管理员)。 ErrRoleLocked = errors.New("超级管理员角色不对外开放") )
错误哨兵。
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 中定义,这里不重复。
var ErrAccountDisabled = errors.New("账号已被停用")
ErrAccountDisabled 表示账号已被停用。
var ErrEmailUnverified = errors.New("邮箱未验证")
ErrEmailUnverified 表示账号的邮箱尚未验证。
var ErrInvalidCredentials = errors.New("用户名或密码错误")
ErrInvalidCredentials 表示登录凭据错误。
用户不存在与密码错误都返回同一个错误,避免通过响应差异枚举账号。
var ErrInvalidSession = errors.New("会话无效")
ErrInvalidSession 表示会话不存在、已过期或所属账号已停用。
var ErrInvalidToken = errors.New("访问令牌无效")
ErrInvalidToken 表示令牌不存在、已过期或所属账号已停用。
var ErrTooManyAttempts = errTooManyAttempts{}
ErrTooManyAttempts 表示登录尝试过于频繁。
Functions ¶
func BearerToken ¶
BearerToken 从 Authorization 头中提取 Bearer 令牌。
func ConstantTimeEqual ¶
ConstantTimeEqual 是定长字符串比较,避免通过响应时间推断令牌内容。
func ForbiddenProblem ¶
func ForbiddenProblem(required ...perm.Permission) *httpx.Problem
ForbiddenProblem 构造 403 错误,扩展成员 requiredPermissions 列出所需权限,便于前端提示与排错。
处理器内的所有权判定失败时直接返回它:*httpx.Problem 实现了 huma.StatusError。
func HashToken ¶
HashToken 返回令牌的 SHA-256 十六进制哈希。
会话与 PAT 均只存哈希:库泄漏时无法反推出可用凭据。 这里不需要加盐或慢哈希——令牌是 256 位随机值,不存在字典攻击面。
func NormalizeUsername ¶
NormalizeUsername 归一化用户名:去空白并转小写。
导出是给 account 模块用的:注册表单要先归一化再校验格式, 否则用户输入 "Alice" 会因为含大写而被自己的正则拒掉, 而落到库里其实是合法的 "alice"。
func RequirePermission ¶
RequirePermission 是 huma 操作级中间件:要求调用者直接持有指定权限之一。
用法:
huma.Register(api, huma.Operation{
Middlewares: huma.Middlewares{auth.RequirePermission(perm.UsersManage)},
...
}, handler)
适用于与所有权无关的权限(如 users:manage)。涉及所有权的检查必须在处理器内 用 Principal.Allows 完成,失败时返回 ForbiddenProblem——中间件此时还不知道目标对象归属谁。
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(仅存哈希)。
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 抹掉,会把用户从已登录状态踢出去。
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)写在消费方,不写在这里。
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。
type IssuedSession ¶
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 ¶
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 ¶
FromContext 取出调用者,未认证时返回 nil 与 false。
func MustFromContext ¶
MustFromContext 取出调用者,未认证时返回零值 Principal。
供已在鉴权中间件之后的处理器使用,避免每处都写 ok 判断; 零值 Principal 的所有权限判定均为 false,故失败是安全的。
func NewSessionPrincipal ¶
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 ¶
IsSuperAdmin 报告调用者是否为超级管理员。
func (*Principal) Permissions ¶
Permissions 返回本次调用的有效权限集合。
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 ¶
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) ChangePassword ¶
func (s *Service) ChangePassword(ctx context.Context, userID int64, oldPlain, newPlain string) error
ChangePassword 校验旧密码后设置新密码。
成功后销毁该用户的全部会话与令牌:改密码必须使所有既有凭据失效, 否则「改了密码却没踢下线」会成为严重的安全缺陷。
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, params LoginParams) (*IssuedSession, *User, error)
Login 校验凭据并签发会话。
失败尝试按账号与客户端 IP 两个维度计数(见 LoginLimiter)。计数在 **任何数据库访问之前**判定:被限流的请求不该再去花一次 64 MiB 的 argon2 开销, 否则限流本身就成了一条放大路径。
func (*Service) LogoutSession ¶
LogoutSession 销毁已解析的会话;session 为 nil(如令牌调用)时不做任何事。
func (*Service) ResetPassword ¶
ResetPassword 强制设置新密码并使既有凭据全部失效。
供 `lumo admin reset-password` 与管理员操作使用,不校验旧密码。
func (*Service) StartCleanup ¶ added in v0.1.9
StartCleanup 启动后台周期清理,ctx 取消时退出。
多实例部署时每个实例的定时器都会触发,经 database.ClaimRun 认领后每轮只有一个实例真正执行。
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 中。
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) 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 中取出会话令牌明文。
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store 提供用户与角色的持久化操作。
func (*Store) AssignRoles ¶
AssignRoles 覆盖式设置用户角色。
func (*Store) CountAdmins ¶
CountAdmins 统计拥有管理员角色的用户数。
func (*Store) CountUsers ¶
CountUsers 返回用户总数,用于判断是否需要初始化首个管理员。
func (*Store) CreateRole ¶
CreateRole 创建自定义角色。
func (*Store) CustomRolePermissions ¶
CustomRolePermissions 返回自定义角色的权限映射,供权限汇总使用。
func (*Store) DeleteRole ¶
DeleteRole 删除自定义角色;仍被用户持有时拒绝。
func (*Store) DeleteUser ¶
DeleteUser 删除用户及其角色关联。
用户发布过的内容通过外键 RESTRICT 阻止删除:那些内容仍需要作者。 需要保留内容时正确做法是停用账号,而不是删人。
func (*Store) FindRoleByName ¶
FindRoleByName 按名称查角色。
func (*Store) FindUserByID ¶
FindUserByID 按 ID 查用户。
func (*Store) FindUserByLogin ¶
FindUserByLogin 按用户名或邮箱查用户,两者均大小写不敏感。
func (*Store) FindUsersByIDs ¶
FindUsersByIDs 按 ID 批量查用户,不加载角色;供内容模块给文章附上作者信息。
返回映射而非切片:调用方按 ID 取用,不存在的 ID 不会出现在映射中。
func (*Store) PageUsers ¶
func (s *Store) PageUsers(ctx context.Context, filter UserFilter, params api.PageParams) ([]User, int, error)
PageUsers 分页返回用户(含角色),按创建时间倒序。
func (*Store) ResetRole ¶
ResetRole 把内置角色的权限、显示名与描述恢复成代码里的默认值。
存在的理由:内置角色的权限只在首次创建时播种(见 SeedRoles),升级新增的权限 不会自动补上。有这一条,「升级后编辑角色缺了新权限」才有自助解法, 而不是要站长去翻本文档再逐条勾回来。
func (*Store) RoleByName ¶
RoleByName 按名称查角色。
func (*Store) SeedRoles ¶
SeedRoles 幂等地写入内置角色。
每次启动都执行,但只对两类字段负责:
- 显示名与描述**随代码覆盖** —— 措辞是产品的一部分,升级就该更新;
- 权限集合**只在角色首次创建时写入**。2026-09-15 起内置角色的权限归站长 (三挡角色都能在角色管理页自由勾选),每次启动覆盖回去等于无声改写站长的选择: 升级时新增一条权限本可以自动放宽,而删掉一条权限却可能让正在用的账号突然 403。
super-admin 例外(perm.Locked):它不对站长开放,权限每次都按代码覆盖。 代价是升级新增的权限不会自动落到已被改过的内置角色上,故角色管理页提供「恢复默认」。
func (*Store) SetDisabled ¶
SetDisabled 启用或停用账号。
func (*Store) TouchLastLogin ¶
TouchLastLogin 记录最近登录时间。
func (*Store) UpdatePassword ¶
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 整条锁定 —— 把它的权限改坏等于拆掉唯一的后门。
type TokenStore ¶
type TokenStore struct {
// contains filtered or unexported fields
}
TokenStore 管理 Personal Access Token。
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
AfterScanRow 实现 bun.AfterScanRowHook:每读出一行就同步 Verified, 接口序列化 User 时这个字段因此永远与数据库一致。
func (*User) EmailVerified ¶
EmailVerified 报告账号的邮箱是否已验证。
判定写在这里而不是散落各处:登录闸门、Console 视图与账户页读的必须是同一个判据。