跳到正文

参考

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 不一致,以源码为准。