在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。
回顾《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》
《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(二)》
《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(三)》
用户授权简介在继续分析我们的应用程序之前,我们简单回顾一下用户授权。在用户登录的过程中,系统首先确定当前试图登录的用户是否为合法用户,也就是该用户是否被允许访问应用程序,在这个过程中,登录流程并不负责检查用户对哪些资源具有访问权限,反正系统中存在用户的合法记录,就认证通过。接下来,该用户账户就需要访问系统中的各个功能模块,并查看或者修改系统中的业务数据,此时,授权机制就会发挥作用,以便检查当前登录用户是否被允许访问某些功能模块或者某些数据,以及该用户对这些数据是否具有读写权限。这种决定用户是否被允许以某种方式访问系统中的某些资源的机制,称为授权。
最常见的授权可以基于用户组,也可以基于用户角色,还可以组合用户组与角色,实现基于角色的授权(Role Based Access Control,RBAC)。比如:某个“用户”属于“管理员组”,而“管理员组”的所有“用户”都具有“管理员角色”,对于“管理员角色”,系统允许它可以管理和组织系统中的业务数据,但不能对用户账户进行管理,系统希望只有超级管理员才可以管理用户账户。于是,当某个用户账户被添加到“管理员组”之后,该用户账户就自动被赋予了“管理员角色”,它可以管理系统中的业务数据,但仍然无法对系统中的用户账户进行管理,因为那是“超级管理员”的事情。
从应用程序的架构角度来看,不难得出这样的结论:用户认证可以通过第三方的框架或者解决方案来完成,但用户授权一般都是在应用程序内部完成的,因为它的业务性很强。不同系统可以有不同的授权方式,但认证方式还是相对统一的,比如让用户提供用户名密码,或者通过第三方身份供应商(Identity Provider,IdP)完成单点登录等等。纵观当下流行的认证服务供应商(例如Auth0),它们在认证这部分的功能非常强大,但仅提供一些相对简单基础的授权服务,帮助应用程序完成一些简单的授权需求,虽然应用程序也可以依赖第三方服务供应商来统一完成认证与授权,但这并不是一个很好的架构实践,因为对第三方服务的依赖性太强。
回顾我们的案例,至今为止,我们仅仅完成了用户认证的部分,接下来,一起看看在Ocelot API网关中如何做用户授权。
用户授权的实现在系统架构中引入API网关之后,实现用户授权可以有以下两种方式:
在API网关处完成用户授权。这种方式不需要后台的服务各自实现自己的授权体系,用户授权由API网关代为完成,如果授权失败,API网关会直接返回授权失败,不会将客户端请求进一步转发给后端的服务。优点是可以实现统一的授权机制,并且减少后端服务的处理压力,后端服务无需关注和处理授权相关的逻辑;缺点是API网关本身需要知道系统的用户授权策略
API网关将用户账户信息传递给后端服务,由服务各自实现授权。这种做法优点是API网关无需关心由应用程序业务所驱动的授权机制,缺点是每个服务要各自管理自己的授权逻辑
后端服务授权先来看看第二种方式,也就是API网关将用户账户信息传递给后端服务,由后端服务完成授权。在前文中,我们可以看到,Access Token中已经包含了如下四个User Claims:
nameidentifier
name
emailaddress
role
Ocelot允许将Token中所包含的Claims通过HTTP Header的形式传递到后端服务上去,做法非常简单,只需要修改Ocelot的配置文件即可,例如:
{ "ReRoutes": [ { "DownstreamPathTemplate": "/weatherforecast", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5000 } ], "UpstreamPathTemplate": "/api/weather", "UpstreamHttpMethod": [ "Get" ], "AuthenticationOptions": { "AuthenticationProviderKey": "AuthKey", "AllowedScopes": [] }, "AddHeadersToRequest": { "X-CLAIMS-NAME-IDENTIFIER": "Claims[] > value > |", "X-CLAIMS-NAME": "Claims[] > value > |", "X-CLAIMS-EMAIL": "Claims[] > value > |", "X-CLAIMS-ROLE": "Claims[] > value > |" } } ] }然后重新运行服务,并在后端服务的API Controller中设置断点,可以看到,这四个Claims的数据都可以通过Request.Headers得到: