Go aah 框架的安全设计理解及应用实例

更新

  • 20220302 - 增加编辑用户功能
  • 20220312 - 实现动态权限更新

前言

Go aah 框架的安全设计借鉴了 Apache Shiro 安全库,其安全包(aahframe.work/security)相当于是 Shiro 的一个 Golang 实现,且自带 4 种现成的 Auth Scheme(身份验证方案):表单认证、基本认证、OAuth2 认证和通用认证。这 4 种方案可单用可组合,基本能覆盖多数应用场景的安全要求和需要。要知道,Apache Shiro 是一个开源的企业级 Java 安全框架,在 Java 生态应用广泛、颇具影响力。而在 Go 生态的框架或库中则较少看到这种设计。Apache Shiro 的应用,丰富了 aah 的企业级功能特性。

这是 Go aah 框架介绍的第 4 篇,我将结合一个官方实例(Example - Form based Auth),来介绍如何使用 aah 框架来开发安全的 Web 应用;并实现将这个例子的后端存储迁移到 MySQL 数据库,以寻求在 aah 应用中更好地集成数据库的实践方法。例子现有的功能及动态权限更新功能已在代码中实现,代码的 GitHub repo 是:https://github.com/vulcangz/aah-form-based-auth进一步的功能开发计划中,也欢迎您参与其中欢迎提出问题或贡献代码。实验性项目,请勿用于生产环境。

需求分析与设计目标

要使用 aah 框架的安全功能,阅读和理解相关的安全术语是必要的,这些术语在 aah 的安全文档中随处可见,对术语的理解有助于对 aah 安全的深入了解。建议去官网看下相关的文档,术语的官网链接是 https://docs.aahframework.org/v0.12/security-termi...。在文末我会附一个简单的术语介绍,这里就不多说了。

这个表单认证的例子用 aah 框架演示了基于表单的 auth。它包括认证,通过路由配置的路由授权,视图文件的访问权限和在视图文件中的会话访问。这个例子的设计是花了心思的,可以让你很直观的了解到:Subject(主体)的角色、权限配置和网页访问权限的对应关系。有一小点遗憾的是,与 aah 的其他例子一样,该案例同样是用内存 "store" 的方式实现的,而不是数据库!不是数据库!算是 "鸡蛋里挑骨头" 的话,另有几点可优化或完善的地方,总结如下:

  • 内存 "store" 的方式有其优点,但存在局限性!数据库集成(Database Access Layer)的问题,可以通过 aah 的扩展功能来实现。但深度集成不易!官方 repo 有一个相关的 issue #161,最近的回复停留在2018年12月,一直也没啥进展。
  • “我是谁?我是什么角色?我有什么权限”——在每个页面加上这些信息,便于 Subject 自行对照,找到这些问题的答案。
  • “aah 支持在运行时通过 `Subject` 实例动态添加/删除角色和权限。这意味着,你不必强迫应用程序的用户退出并重新登录,以获得他们的新角色/权限。”——我对这个功能点感兴趣,但我不确定 aah 是否已经实现了这个功能?实例中有一个 "Edit User" 的菜单,但是只是个空菜单而已,具体功能并未实现。

好吧,这就是我写这篇文章的原因和编写代码的主要目标了,具体而言就是:

  • 将用户模型迁移到 MySQL 数据库,选用适合的 ORM(目前是 SQLBoiler)实现对数据库的访问。要做到深度集成不易,目前还只能是 "no-context" 的。
  • 在各网页增加当前 "Subject" 的信息——"用户看板(kanban)"。这个通过 session 传递给网页,实现起来简单些。但从目前来看,这对第4点的实现有影响。
  • (ok)(todo)实现为 `Subject` 实例动态添加/删除角色和权限的功能。这需要一个 "Update" 用户的页面。表面上看是补充缺失的 "Edit User"(编辑用户)功能,实际上是涉及到完整的 "CRUD" 功能。这个等有时间开发好了再更新吧。
  • (ok)(todo)实现在为 `Subject` 实例动态添加/删除角色和权限后,不必强迫应用程序的用户退出并重新登录,以获得他们的新角色/权限。”从框架源代码来看,应该没有现成可用的接口。目前已实现约50%,难点在于 session 的更新!还有 "用户看板" 的更新(我增加的功能,这不是难为自己吗 )。session 的更新通过自定义中间件来实现,而用户更新数据通过 RabbitMQ 传递。参见:补充说明。

aah 的安全设计实现简介

一般来说,Authentication(认证)、Authorization(授权)、Hash(哈希)、Role(角色)、Permission(权限)、Session(会话)这些看起来还眼熟点,"Principal" 和 "Subject" 看起来就陌生了。

1.需要知道的术语

序号 术语 描述
1 Subject(主体) 当前与应用程序交互的实体(用户、第三方服务、cron job 等)的安全特定用户 “视图”。基本上,它是与应用程序通信的任何东西或任何人。
在程序中,可以通过 ctx.Subject() 获得当前执行的主体。
2 Principals(身份) 一个主体的识别属性。如姓名,电子邮件地址,用户名,社保号等。
// 获得所有的身份
principals := ctx.Subject().AllPrincipals()
// 获得主要身份(唯一标识)
primaryPrincipal := ctx.Subject().PrimaryPrincipal()
3 Credential(凭证) 用来验证身份的秘密数据。密码、x509 证书等。
4 Authenticator(认证器) 应用程序应实现 authc.Authenticator 接口,为认证主体/用户提供身份信息(ID、Email 或用户名等。填充到 authc.AuthenticationInfo)。
5 Authorizer(授权器) 应用程序应实现 authz.Authorizer 接口,在成功认证后提供授权信息(角色和权限。填充到 authz.AuthorizationInfo)。
6 Auth Scheme(身份验证方案) aah 支持的认证方案有 Form Auth、Basic Auth、OAuth2 和 Generic Auth。Auth 方案在 security.conf 中配置;权限属性在 routes.conf 中按路由进行映射。
7 WildcardPermission(通配符权限) 常见用法是为实例级访问控制列表建模。这种情况下,可使用三个部分:domain(域)、action(动作)和 instance(实例),三部分用冒号(`:`)分隔。各部分允许缺失,请注意:缺少的部分意味着用户可以访问该部分对应的所有值。
如 "printer:query:lp7200" 定义了查询 ID 为 lp7200 的打印机的行为;
"printer:print:epsoncolor" 定义了向 ID 为 epsoncolor 的打印机打印的行为。
8 Password Encoders(密码编码器) PasswordEncoder 接口用于实现生成密码哈希,并比较给定的哈希和密码。(这都提供了,还要啥自行车)

这些看起来比较复杂,但是,毕竟最难的部分作者已经实现了:-),开发者要做的就是选择 Auth Scheme,然后对应的去实现上表中 4 和 5 这两个接口,OOTB 框架带给你就是这样爽快的感觉!

2.安全配置

aah 安全配置包括 Authentication、Authorization、Session Management、Secure Headers、Anti-CSRF 等等。这些在官网都有很详细的介绍。这里选择 Authentication 和 Authorization 结合例子来看一下其中的安全配置(config/security.conf):

auth_schemes {
    # -----------------------------------------------------------------------------
    # Form auth scheme
    # Choose a unique key name. It gets used as route auth.
    #
    # Doc: https://docs.aahframework.org/auth-schemes/form.html
    # -----------------------------------------------------------------------------
    form_auth {
      scheme = "form"
      authenticator = "security/FormAuthenticationProvider"
      authorizer = "security/FormAuthorizationProvider"
      # Password encoder
      # Doc: https://docs.aahframework.org/password-encoders.html
      password_encoder = "bcrypt"
    }
  }

这其中的各配置项正如上面所说的那样:

  • scheme:为身份验证方案的名字,支持的值包括 `form`, `oauth2`, `basic` 和 `generic`。其中 `form` 即表单验证;`oauth2` 为引入的第三方认证;`basic` 包括 "File" 和 "Dynamic","File" 是在配置文件中定义的已知用户集,支持配置角色和权限。"Dynamic" 则类似于表单认证。`generic` 提供了通用的 auth 方案,作为一个可扩展的功能。这种方案下身份验证的责任由用户承担。如 JWT auth 方案的实施,用户登录成功后,不再验证凭据而是 "token"。
  • authenticator:为了在登录流程中提供主体的认证信息,实现接口`authc.Authenticator`并在此配置。这个接口包括 2 个方法:Init、GetAuthenticationInfo。
  • authorizer:为了提供主体的角色和权限,实现接口 `authz.Authorizer` 并在此配置。这个接口同样包括 2 个方法:Init、GetAuthorizationInfo。
  • password_encoder:密码编码器用于配置密码算法。 aah 用应用程序提供的凭证来验证主体凭证。

主要的配置就是这些,还有字段名(field{})、表单认证方案的 URL(url{})等配置项(块),详情可参考官方文档。

3.路由配置

例子中的路由配置(config/routes.conf),以 "manage_users" 为例如下所示:

      manage_users {
        path = "/manage/users.html"
        controller = "AppController"
        action = "ManageUsers"
        authorization {
          satisfy = "both"
          roles = [
            "hasanyrole(manager,administrator)"
          ]
          permissions = [
            "ispermitted(users:manage:view)"
          ]
        }
      }

其中,authorization {} 块中的值含义如下:

  • Satisfy 值用于评估 `roles` 和 `permissions` 属性的结果。Satisfy 有两种值:`either` => `roles` 或 `permissions` 应该满足主体的要求。`both` => `roles` 和 `permissions` 都应满足主体的要求。
  • roles 是可选配置。"hasanyrole(manager,administrator)" 中 "hasanyrole" 是角色函数,这句话表示有 "manager"、"administrator" 中任何一个角色结果都为 true。roles 中如有多个函数,各函数之间是 AND 的关系。
  • permissions 同为可选配置。各权限函数之间是同样是 AND 的关系。
  • 总的来说该 authorization{} 表示,如果主体(Subject)角色是 "manager" 或 "administrator";并且主体(Subject)被允许访问由指定权限字符串 "users:manage:view" 汇总的资源,就允许访问指定控制器 "AppController" 的动作 "ManageUsers"。

4.模板函数

aah 提供了模板/视图函数,以灵活的方式对视图文件执行授权。包括:

  • hasrole
  • hasanyrole
  • hasallroles
  • ispermitted
  • ispermittedall

比如,要检查主体是否有管理用户的权限。如果为真,则显示该链接。

<html>
<body>
  {{ if ispermitted . "users:manage" }}
    <a href="/manage-users.html">Manage Users</a>
  {{ end }}
</body>
</html>

数据库 ORM 的选型与配置

这个在《Go aah 框架集成数据库 ORM》已经分析过了。结论就是 "SQLBoiler" 能够以 "--no-context" 模式结合 aah 框架使用。由于 SQLBoiler 是采取预生成 models 代码的方式,性能表现要好一些(主观感受)。开启调试日志后来看,在复杂点的 "TO MANY" 查询中生成的 SQL 代码是正常的,不会出现在 GORM 关联查询中出现多余了条件却无对应值的情况。

代码中忘了关闭 SQLBoiler 的调试了,如果你在终端 console 看到满屏滚动的 SQL 语句那就对了,那就看看吧,输出格式看起来还蛮顺眼的呢。

数据存储库(repository)在 “OnStart” 阶段初始化,代码如下:

import "aah-form-based-auth/app/repository"
func init() {
  app := aah.App()
  // ...
  // 数据库连接
  app.OnStart(database.SConnect)
  // 数据存储库初始化
  app.OnStart(repository.InitRepo)
  // ...
}

控制器或安全接口中这样调用:

import (
    repo "aah-form-based-auth/app/repository"
)
// 这样调用
user, err := repo.R().GetUserByEmail(authcToken.Identity)
if err != nil {
    // No subject exists, return nil and error
    return nil, authc.ErrSubjectNotExists
}

在控制器的 before 拦截器中实例化一个数据存储库实例、然后调用。具体可参考代码。

存在的问题

测试中存在模板函数不能使用的情况,这个暂时是通过在 init.go 中添加自定义模板函数的方法解决了,代码用的是源代码中的相关部分。本机 aah 版本情况如下:

λ aah -v
aah v0.12.5
cli v0.13.6
go  v1.17.7

官方示例中的原始代码将 aah 切换到 v0.12.5 也是正常的,所以不确定是否是版本的问题。

补充说明

用户权限更新之后的数据是通过 RabbitMQ 传递的,所以你需要有一个运行的 RabbitMQ 服务器,并且默认 URI "amqp://guest:guest@localhost:5672/" 可用。

测试方法:如果是同一台机器,可以用两个浏览器:一个用作管理用户登陆(如 Chrome);另一个用于普通用户登录(如 Firefox)。在管理用户编辑了用户权限之后,在普通用户浏览器上刷新网页,如果更新成功,则可以在页面上部的 “用户看板” 看到更新后的用户名、权限等信息!目前 RabbitMQ 消息的交付确认(Ack)的处理上还有点小问题,如果在用户端刷新网页权限没有更新,那就试着再刷新一次看看。

总结

其实几年前的时候,曾经短暂地使用过 aah 框架,由于数据库的问题最终选择了 Echo + GORM 的组合,用起来感觉也不错。这次是因为测试 app 的缘故用回了这个框架,aah 框架是那种初步上手容易,想深入一点就有点难的那种。但是一旦你突破了这个阶段,对 aah 框架有了更深一层的了解之后,用起来就顺手了!OOTB 框架,名不虚传!易于使用和配置、丰富的功能集、强大的扩展能力,用起来确实省事。aah 框架如果能在数据库深度集成方面更进一步,并加强企业级应用特性,相信发展会更好。

aah 系列的文章到这里是第4篇了:

关注开源技术发展,发掘实用开源项目。秀项目,秀 code!这就是 WorldLink 资源网。关注公号 "Worldlink资源网",可及时收到文章推送。技术文章是会修改和更新的,也欢迎您有空时过来看看。


参考资料

附录

1.aah 常见安全术语

认证(Authentication)是身份验证的过程——系统试图验证一个主体/用户是他们所声称的人。

授权(Authorization,又称访问控制),是确定一个主体/用户是否被允许做某事的过程。

哈希(Hash,又称散列)函数是对输入源(有时称为消息)进行单向的、不可逆的转换,使其成为一个编码的散列值,有时称为消息摘要。

权限(Permission),权限是安全策略中最低级别的结构。它们只定义应用程序可以做什么。它们并不描述谁能够执行这些操作。权限只是一个行为声明,仅此而已。

身份(Principal)是应用程序用户(Subject)的任何识别属性。如用户名,姓名,身份证号,用户 ID等。aah 中定义有 Primary principal,是指 Subject 的主身份属性,比如表的主键等。

角色(Role),aah 倾向于将角色解释为简单的一个命名的权限集合。

会话(Session)是一个有状态的数据上下文,它与一个在一段时间内与软件系统交互的单一主体/用户相关联。当主体使用应用程序时,数据可以从会话中被添加/读取/删除。当主体/用户注销应用程序或由于不活动而超时时,会话被终止。

主体(Subject),当前与应用程序交互的实体(用户、第三方服务、cron job 等)的安全特定用户 “视图”。基本上,它是与应用程序通信的任何东西或任何人。

Like:
0
To the top