参考
AuthZ
命令边界授权:security.Authorizer、AllowAll、DenyByDefault、WithAuthorizer 过滤器、Middleware、ErrDenied 与 AuthContext 信任模型。
AuthZ
包:security
go-sequent 在命令边界、Handler 运行之前检查授权。认证(你是谁)由宿主应用负责;AuthZ(你能不能做这件事)是写路径上的一个钩子。
接口
type Authorizer interface {
Authorize(ctx context.Context, cmd command.Command) error
}
返回 nil 表示允许,返回错误表示拒绝。内置 authorizer 的拒绝错误都包装了 security.ErrDenied:
err := app.Service.Execute(ctx, cmd)
if errors.Is(err, security.ErrDenied) {
// 在你的 API 层返回 403
}
security.AuthorizerFunc 可以把普通函数适配成 Authorizer。
内置 Authorizer
| Authorizer | 行为 |
|---|---|
security.AllowAll{} |
允许所有命令,用于测试与演示。 |
security.DenyByDefault{Allow: map[string]bool{...}} |
只允许列出的命令类型,其余一律返回 ErrDenied。 |
app := sequent.New(cfg, sequent.WithAuthorizer(security.DenyByDefault{
Allow: map[string]bool{
"bank.Deposit": true,
"bank.Withdraw": true,
},
}))
金融应用请从 DenyByDefault 起步。这样新增的命令类型会被拦住,直到有人有意把它加入白名单。
示例里的 bank.AllowBankCommands() 返回一个允许四个 bank 命令类型的 DenyByDefault。
过滤器还是中间件
安装 authorizer 有两种方式:
| 方式 | 运行位置 |
|---|---|
sequent.WithAuthorizer(auth) |
把 security.Filter{Auth: auth} 加入 Service.Filters。在中间件链之后、Validate 之前运行。 |
security.Middleware(auth) |
一个 command.Middleware,在链中它所在的位置运行。 |
当其他中间件可能短路时,两者的区别就很重要。Idempotency 中间件对重放的键会提前返回,所以过滤器不会在重放时运行。如果需要每次尝试都做授权,把 security.Middleware(auth) 放在 Idempotency 之前;如果 authorizer 需要租户信息,则放在 tenancy 之后:
app.Service.Middleware = append(
[]command.Middleware{tenancy.Middleware, security.Middleware(auth)},
app.Service.Middleware...,
)
Authorizer 能看到什么
Command 及其 Meta:TenantID、UserID、AuthContext、Source。它还能拿到 context.Context,所以可以读取你的 HTTP 层放进去的值。
不要信任客户端设置的 UserID。 先在应用里验证调用者,再用已验证的声明设置 Meta.UserID 与 Meta.AuthContext。go-sequent 把 AuthContext 当作不透明数据,含义由你的 authorizer 决定。
auth := security.AuthorizerFunc(func(ctx context.Context, cmd command.Command) error {
m := cmd.Meta()
claims, ok := m.AuthContext.(*MyClaims)
if !ok || claims.TenantID != m.TenantID {
return fmt.Errorf("%w: %s", security.ErrDenied, cmd.CommandType())
}
if !claims.Can(cmd.CommandType()) {
return fmt.Errorf("%w: %s", security.ErrDenied, cmd.CommandType())
}
return nil
})
聚合级检查
「只有账户所有者才能取款」这类规则依赖聚合状态,应写在聚合方法里,只使用聚合状态和命令携带的声明。不要在聚合里临时发起数据库查询。
审计轨迹
每条成功的命令都会连同租户、用户、Idempotency 键、关联与因果 id 以及来源一起存储。command.AuditOf(cmd) 以结构体形式给出同样的数据。结合事件流与 hash chain(按聚合串联事件哈希,用来发现篡改),就能得到「谁做了什么」的记录。
AuthZ 不是什么
- 不是认证。
- 不对事件签名。go-sequent 没有事件签名,完整性靠 hash chain。
- 本身不强制租户隔离。tenancy 中间件会拒绝租户与 context 不一致的命令,见 Tenancy(多租户)。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。