认证
VEF 的认证发生在 API 操作层。每个操作都有自己的 auth 配置,API 中间件会在 handler 执行前先解析出当前 principal。
默认行为
如果你不做额外配置:
- 操作默认使用 Bearer 认证
- 显式
Public的操作则不要求认证
这个默认值来自 API 引擎,而不是来自你的应用配置文件。
内置认证策略
公开的 api 包暴露了这些策略 helper:
api.Public()api.BearerAuth()api.SignatureAuth()api.IPAuth(...)(白名单解析方式见下文的 Signature helpers)
实际使用中,你通常通过操作配置来控制:
api.OperationSpec{
Action: "login",
Public: true,
}
或者通过资源级别的 auth 默认值来设置。
Bearer 认证
Bearer 认证支持两种 token 来源:
Authorization: Bearer <token>- 查询参数
__accessToken
真正的 token 校验逻辑由安全模块中的 auth manager 负责。
Signature 认证
Signature 认证主要用于外部应用和请求签名场景。
它要求这些 header:
X-App-IDX-TimestampX-NonceX-Signature
校验逻辑由安全模块的 signature authenticator 执行。
公开操作
公开操作会得到一个匿名 principal,而不是直接被拒绝。
适合标记为 Public 的接口包括:
- 登录
- 刷新 token
- 某些匿名健康检查或回调入口
内置认证资源
安全模块会自动注册一个内置 RPC 资源:
security/auth
主要 actions 包括:
loginrefreshlogoutresolve_challengeget_user_info
这些请求字段、公开标记和限流来源也是运行时 contract 的一部分:
| Action | Public | Rate limit | 请求字段 |
|---|---|---|---|
login | 是 | vef.security.login_rate_limit | type、principal、credentials;全部是 validate:"required" |
refresh | 是 | vef.security.refresh_rate_limit | refreshToken;validate:"required"。仅在 token_type = "jwt_token" 下挂载——opaque_token 下该操作不存在(会话自行续期) |
logout | 否 | 默认 API rate limit | 无 |
resolve_challenge | 是 | vef.security.login_rate_limit | challengeToken、type、response;全部是 validate:"required" |
get_user_info | 否 | 默认 API rate limit | 任意 params,会转发给 UserInfoLoader.LoadUserInfo(...) |
这个资源、所有已注册的 Authenticator,以及 AuthManager 聚合器,都由
框架的安全模块完成装配——同一个模块还装配了暴力破解锁定、密码强度/历
史/过期(参见登录加固)以及 opaque token 会话控制
(参见会话管理)。
内置 authenticator type 字符串是 password、jwt_token、opaque_token、
refresh 和 signature(原来的 token 字符串在 v0.38 更名为
jwt_token)。普通客户端调用里,security/auth.login 使用
type: "password" 搭配用户名和密码凭证。Bearer 保护的操作会在内部按
vef.security.token_type 分派已配置的令牌机制(jwt_token 或
opaque_token),security/auth.refresh 会在内部使用 refresh,
SignatureAuth 会把签名 headers 映射到 signature authenticator。只有
已配置机制的认证器会被注册,且 login 拒绝框架签发的令牌类型作为登录
凭据(见会话管理)。
logout 总是返回 ok 结果。在 jwt_token 下它实际上是 no-op——服务端没有
可吊销的会话,客户端需要自行删除已保存的 token。在 opaque_token 下它会
吊销当前 bearer token 背后的会话,尽力而为(会话不存在或存储出错只记录
日志)。
登录流程
内置认证资源支持两阶段模型:
- 先校验凭证
- 如有需要,再进入 challenge 流程
如果没有 challenge,login 会直接返回 token。
如果 challenge provider 已配置且当前用户需要额外挑战,login 会返回:
- challenge token
- 下一步 challenge 描述
客户端之后继续调用 resolve_challenge,直到所有挑战都完成。
Go API 层里,这个响应形状由 LoginResult 表示;当前步骤由 LoginChallenge 表示。
登录响应 DTO 使用这些精确字段:
| DTO | 字段 |
|---|---|
AuthTokens | JSON accessToken、refreshToken |
Authentication | JSON type、principal、credentials |
LoginResult | JSON tokens、challengeToken、challenge |
LoginChallenge | JSON type、data、required |
ChallengeState | Go-only state: Principal、Pending、Resolved |
应用通常还需要提供什么
这里要分场景来看:
security.UserLoader通常是用户登录和 refresh 流程的前提security.ExternalAppLoader只在你使用签名认证的外部应用场景时需要- challenge provider 是可选项,只有在你启用了挑战式登录流时才相关
security.UserInfoLoader只在你希望security/auth.get_user_info返回应用自定义用户信息时需要
框架提供的是认证流程和中间件,而不是你的身份源本身。
公开 API 一览
认证相关的完整公开接口面——principal、JWT、认证管理器、挑战提供者与令牌存储、签名认证、登录事件——连同契约说明收录在认证参考中。
一个可运行的登录模块
真实的 VEF 项目里,auth 模块通常都很小:一张用户表、一个实现 loader 接口的包,以及一个把它们提供出去的模块声明。框架已经内置了 security/auth 资源、password 和 refresh authenticator,以及默认的 bcrypt password.Encoder;应用只需要提供自己的身份源。下面这个 auth 包已经完整到可以对着一张真实的表登录。
用户模型
package auth
import (
"github.com/uptrace/bun"
"github.com/coldsmirk/vef-framework-go/orm"
)
type User struct {
bun.BaseModel `bun:"table:app_user,alias:au"`
orm.FullAuditedModel
Username string `json:"username" validate:"required,alphanum,max=32" label:"Username"`
Name string `json:"name" validate:"required,max=32" label:"Name"`
PasswordHash string `json:"-" bun:"password_hash,notnull"`
Role string `json:"role"`
IsActive bool `json:"isActive"`
}
// UserDetails becomes Principal.Details and travels inside issued access tokens.
type UserDetails struct {
Username string `json:"username"`
}
PasswordHash 必须存放与登录流程所用 password.Encoder 一致的输出——安全模块默认提供 bcrypt(password.NewBcryptEncoder)。在创建或初始化用户的地方注入 password.Encoder,存入 encoder.Encode(plaintext);内置的 password authenticator 之后会用 encoder.Matches(plaintext, storedHash) 校验登录凭证。
UserLoader
security.UserLoader 只有两个方法:LoadByUsername 支撑 type: "password" 登录,返回 principal 和已存储的密码哈希;LoadByID 支撑 token 刷新。
package auth
import (
"context"
"github.com/coldsmirk/vef-framework-go/orm"
"github.com/coldsmirk/vef-framework-go/security"
)
type userLoader struct {
db orm.DB
}
func NewUserLoader(db orm.DB) security.UserLoader {
return &userLoader{db: db}
}
func (l *userLoader) LoadByUsername(ctx context.Context, username string) (*security.Principal, string, error) {
user, err := l.findActive(ctx, "username", username)
if err != nil {
return nil, "", err
}
return toPrincipal(user), user.PasswordHash, nil
}
func (l *userLoader) LoadByID(ctx context.Context, id string) (*security.Principal, error) {
user, err := l.findActive(ctx, "id", id)
if err != nil {
return nil, err
}
return toPrincipal(user), nil
}
func (l *userLoader) findActive(ctx context.Context, column string, value any) (*User, error) {
var user User
err := l.db.NewSelect().Model(&user).
Where(func(cb orm.ConditionBuilder) {
cb.Equals(column, value).IsTrue("is_active")
}).
Scan(ctx)
if err != nil {
return nil, err
}
return &user, nil
}
func toPrincipal(user *User) *security.Principal {
principal := security.NewUser(user.ID, user.Name, user.Role)
principal.Details = &UserDetails{Username: user.Username}
return principal
}
这里的错误语义与内置 authenticator 的预期一致:
Scan已经把“无记录”映射为result.ErrRecordNotFound,所以直接原样返回错误即可。查询里过滤is_active,可以让被禁用的用户与不存在的用户表现完全一致。login期间,LoadByUsername返回的任何错误——以及nilprincipal 或空哈希——都会归并为通用的凭证无效错误(code1008),因此无法通过响应枚举用户名。record-not-found 记录 info 级日志,其他错误记录 warn 级。refresh期间,LoadByID的错误会原样返回给调用方;refresh authenticator 之所以重新加载用户,正是为了让被停用的账号无法继续刷新。
权限与用户信息
package auth
import (
"context"
"github.com/coldsmirk/vef-framework-go/security"
)
type rolePermissionsLoader struct{}
func NewRolePermissionsLoader() security.RolePermissionsLoader {
return &rolePermissionsLoader{}
}
func (*rolePermissionsLoader) LoadPermissions(_ context.Context, role string) (map[string]security.DataScope, error) {
if role == "admin" {
return map[string]security.DataScope{
"user:manage": security.NewAllDataScope(),
"order:read": security.NewAllDataScope(),
}, nil
}
return map[string]security.DataScope{
"order:read": security.NewSelfDataScope(""),
}, nil
}
type userInfoLoader struct{}
func NewUserInfoLoader() security.UserInfoLoader {
return &userInfoLoader{}
}
func (*userInfoLoader) LoadUserInfo(_ context.Context, principal *security.Principal, _ map[string]any) (*security.UserInfo, error) {
return &security.UserInfo{
ID: principal.ID,
Name: principal.Name,
Gender: security.GenderUnknown,
}, nil
}
生产环境的 RolePermissionsLoader 应该读取角色-权限表,而不是写死的 switch;安全模块会自动把你提供的 loader 包上一层由 RolePermissionsChangedEvent 触发失效的缓存。权限 token 会进入授权中描述的 RBAC 检查器。
装配
构造函数必须返回接口类型——框架从 DI 图中按 security.UserLoader、security.UserInfoLoader、security.RolePermissionsLoader 这些确切的接口类型消费可选依赖。
package auth
import (
"github.com/coldsmirk/vef-framework-go"
"github.com/coldsmirk/vef-framework-go/security"
)
func init() {
security.SetUserDetailsType[*UserDetails]()
}
var Module = vef.Module(
"app:auth",
vef.Provide(
NewUserLoader,
NewUserInfoLoader,
NewRolePermissionsLoader,
),
)
在 main 里把 auth.Module 传给 vef.Run(...),内置的 security/auth 资源就会自动拿到这些 loader——不需要任何额外注册。这样认证接入代码就能和业务资源模块保持分离。
登录验证
假设已初始化一个用户(admin / ChangeMe_123,哈希由 bcrypt encoder 生成),调用内置资源:
curl http://localhost:8080/api \
-H 'Content-Type: application/json' \
-d '{
"resource": "security/auth",
"action": "login",
"version": "v1",
"params": {
"type": "password",
"principal": "admin",
"credentials": "ChangeMe_123"
}
}'
在没有注册 challenge provider 的情况下,响应直接携带 token 对:
{
"code": 0,
"message": "成功",
"data": {
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
}
访问 token 在 30 分钟后过期(框架内固定常量);刷新 token 的有效期来自 vef.security.token_expires(默认 7 天)。用刷新 token 换取新的 token 对——注意 refresh 的 data 直接就是 token 对,没有外层的 tokens 包装:
curl http://localhost:8080/api \
-H 'Content-Type: application/json' \
-d '{
"resource": "security/auth",
"action": "refresh",
"version": "v1",
"params": { "refreshToken": "eyJhbGciOiJIUzI1NiIs..." }
}'
每个 security/auth action 的请求参数在内置资源中有完整表格。
实践建议
Public只用于明确需要匿名访问的操作- 普通用户认证优先保持在 Bearer
- Signature 更适合系统对系统集成,而不是替代普通用户会话