在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。

回顾

用户授权简介

在继续分析我们的应用程序之前,我们简单回顾一下用户授权。在用户登录的过程中,系统首先确定当前试图登录的用户是否为合法用户,也就是该用户是否被允许访问应用程序,在这个过程中,登录流程并不负责检查用户对哪些资源具有访问权限,反正系统中存在用户的合法记录,就认证通过。接下来,该用户账户就需要访问系统中的各个功能模块,并查看或者修改系统中的业务数据,此时,授权机制就会发挥作用,以便检查当前登录用户是否被允许访问某些功能模块或者某些数据,以及该用户对这些数据是否具有读写权限。这种决定用户是否被允许以某种方式访问系统中的某些资源的机制,称为授权。

最常见的授权可以基于用户组,也可以基于用户角色,还可以组合用户组与角色,实现基于角色的授权(Role Based Access Control,RBAC)。比如:某个“用户”属于“管理员组”,而“管理员组”的所有“用户”都具有“管理员角色”,对于“管理员角色”,系统允许它可以管理和组织系统中的业务数据,但不能对用户账户进行管理,系统希望只有超级管理员才可以管理用户账户。于是,当某个用户账户被添加到“管理员组”之后,该用户账户就自动被赋予了“管理员角色”,它可以管理系统中的业务数据,但仍然无法对系统中的用户账户进行管理,因为那是“超级管理员”的事情。

从应用程序的架构角度来看,不难得出这样的结论:用户认证可以通过第三方的框架或者解决方案来完成,但用户授权一般都是在应用程序内部完成的,因为它的业务性很强。不同系统可以有不同的授权方式,但认证方式还是相对统一的,比如让用户提供用户名密码,或者通过第三方身份供应商(Identity Provider,IdP)完成单点登录等等。纵观当下流行的认证服务供应商(例如Auth0),它们在认证这部分的功能非常强大,但仅提供一些相对简单基础的授权服务,帮助应用程序完成一些简单的授权需求,虽然应用程序也可以依赖第三方服务供应商来统一完成认证与授权,但这并不是一个很好的架构实践,因为对第三方服务的依赖性太强。

回顾我们的案例,至今为止,我们仅仅完成了用户认证的部分,接下来,一起看看在Ocelot API网关中如何做用户授权。

用户授权的实现

在系统架构中引入API网关之后,实现用户授权可以有以下两种方式:

  1. 在API网关处完成用户授权。这种方式不需要后台的服务各自实现自己的授权体系,用户授权由API网关代为完成,如果授权失败,API网关会直接返回授权失败,不会将客户端请求进一步转发给后端的服务。优点是可以实现统一的授权机制,并且减少后端服务的处理压力,后端服务无需关注和处理授权相关的逻辑;缺点是API网关本身需要知道系统的用户授权策略
  2. 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[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier] > value > |",
"X-CLAIMS-NAME": "Claims[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name] > value > |",
"X-CLAIMS-EMAIL": "Claims[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress] > value > |",
"X-CLAIMS-ROLE": "Claims[http://schemas.microsoft.com/ws/2008/06/identity/claims/role] > value > |"
}
}
]
}

然后重新运行服务,并在后端服务的API Controller中设置断点,可以看到,这四个Claims的数据都可以通过Request.Headers得到:

有了这个信息,服务端就可以得知目前是哪个用户账户在请求API调用,并且它是属于哪个角色,剩下的工作就是基于这个角色信息来决定是否允许当前用户访问当前的API。很显然,这里需要一种合理的设计,而且至少需要满足以下两个需求:

  1. 授权机制的实现应该能够被后端多个服务所重用,以便解决“每个服务要各自管理自己的授权逻辑”这一弊端
  2. API控制器不应该自己实现授权部分的代码,可以通过扩展中间件并结合C# Attribute的方式完成

在这里我们就不深入讨论如何去设计这样一套权限认证系统了,今后有机会再介绍吧。

注:Ocelot可以支持多种Claims的转换形式,这里介绍的AddHeadersToRequest只是其中的一种,更多方式可以参考:https://ocelot.readthedocs.io/en/latest/features/claimstransformation.html

Ocelot API网关授权

通过Ocelot网关授权,有两种比较常用的方式,一种是在配置文件中,针对不同的downstream配置,设置其RouteClaimsRequirement配置,以便指定哪些用户角色能够被允许访问所请求的API资源。比如:

{
"ReRoutes": [
{
"DownstreamPathTemplate": "/weatherforecast",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5000
}
],
"UpstreamPathTemplate": "/api/weather",
"UpstreamHttpMethod": [ "Get" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "AuthKey",
"AllowedScopes": []
},
"RouteClaimsRequirement": {
"Role": "admin"
}
}
]
}

上面高亮部分的代码指定了只有admin角色的用户才能访问/weatherforecast API,这里的“Role”就是Claim的名称,而“admin”就是Claim的值。如果我们在此处将Role设置为superadmin,那么前端页面就无法正常访问API,而是获得403 Forbidden的状态码:

注意:理论上讲,此处的“Role”原本应该是使用标准的Role Claim的名称,即原本应该是:

但由于ASP.NET Core框架在处理JSON配置文件时存在特殊性,使得上述标准的Role Claim的名称无法被正确解析,因此,也就无法在RouteClaimsRequirement中正常使用。目前的解决方案就是用户认证后,在Access Token中带入一个自定义的Role Claim(在这里我使用最简单的名字“Role”作为这个自定义的Claim的名称,这也是为什么上面的JSON配置例子中,使用的是“Role”,而不是“http://schemas.microsoft.com/ws/2008/06/identity/claims/role”),而要做到这一点,就要修改两个地方。

首先,在IdentityService的Config.cs文件中,增加一个自定义的User Claim:

public static IEnumerable<ApiResource> GetApiResources() =>
new[]
{
new ApiResource("api.weather", "Weather API")
{
Scopes =
{
new Scope("api.weather.full_access", "Full access to Weather API")
},
UserClaims =
{
ClaimTypes.NameIdentifier,
ClaimTypes.Name,
ClaimTypes.Email,
ClaimTypes.Role,
"Role"
}
}
};

然后,在注册新用户的API中,当用户注册信息包含Role时,将“Role” Claim也添加到数据库中:

[HttpPost]
[Route("api/[controller]/register-account")]
public async Task<IActionResult> RegisterAccount([FromBody] RegisterUserRequestViewModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
} var user = new AppUser { UserName = model.UserName, DisplayName = model.DisplayName, Email = model.Email }; var result = await _userManager.CreateAsync(user, model.Password); if (!result.Succeeded) return BadRequest(result.Errors); await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.NameIdentifier, user.UserName));
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.DisplayName));
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, user.Email)); if (model.RoleNames?.Count > 0)
{
var validRoleNames = new List<string>();
foreach (var roleName in model.RoleNames)
{
var trimmedRoleName = roleName.Trim();
if (await _roleManager.RoleExistsAsync(trimmedRoleName))
{
validRoleNames.Add(trimmedRoleName);
await _userManager.AddToRoleAsync(user, trimmedRoleName);
}
} await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, string.Join(',', validRoleNames)));
await _userManager.AddClaimAsync(user, new Claim("Role", string.Join(',', validRoleNames)));
} return Ok(new RegisterUserResponseViewModel(user));
}

修改完后,重新通过调用这个register-account API来新建一个用户来进行测试,一切正常的话,就可以通过Ocelot API网关中的RouteClaimsRequirement来完成授权了。

通过Ocelot网关授权的另一种做法是使用代码实现。通过代码方式,可以实现更为复杂的授权策略,我们仍然以“角色”作为授权参照,我们可以首先定义所需的授权策略:

public void ConfigureServices(IServiceCollection services)
{
services.AddOcelot();
services.AddAuthentication()
.AddIdentityServerAuthentication("AuthKey", options =>
{
options.Authority = "http://localhost:7889";
options.RequireHttpsMetadata = false;
}); services.AddAuthorization(options =>
{
options.AddPolicy("admin", builder => builder.RequireRole("admin"));
options.AddPolicy("superadmin", builder => builder.RequireRole("superadmin"));
}); services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
}

然后使用Ocelot的AuthorisationMiddleware中间件,来定义我们的授权处理逻辑:

app.UseOcelot((b, c) =>
{
c.AuthorisationMiddleware = async (ctx, next) =>
{
if (ctx.DownstreamReRoute.DownstreamPathTemplate.Value == "/weatherforecast")
{
var authorizationService = ctx.HttpContext.RequestServices.GetService<IAuthorizationService>();
var result = await authorizationService.AuthorizeAsync(ctx.HttpContext.User, "superadmin");
if (result.Succeeded)
{
await next.Invoke();
}
else
{
ctx.Errors.Add(new UnauthorisedError($"Fail to authorize policy: admin"));
}
}
else
{
await next.Invoke();
}
}; b.BuildCustomOcelotPipeline(c).Build(); }).Wait();

当然,上面的BuildCustomOcelotPipeline方法的目的就是将一些默认的Ocelot中间件加入到管道中,否则整个Ocelot框架是不起作用的。我将这个方法定义为一个扩展方法,代码如下:

public static class Extensions
{
private static void UseIfNotNull(this IOcelotPipelineBuilder builder,
Func<DownstreamContext, Func<Task>, Task> middleware)
{
if (middleware != null)
{
builder.Use(middleware);
}
} public static IOcelotPipelineBuilder BuildCustomOcelotPipeline(this IOcelotPipelineBuilder builder,
OcelotPipelineConfiguration pipelineConfiguration)
{
builder.UseExceptionHandlerMiddleware();
builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
app =>
{
app.UseDownstreamRouteFinderMiddleware();
app.UseDownstreamRequestInitialiser();
app.UseLoadBalancingMiddleware();
app.UseDownstreamUrlCreatorMiddleware();
app.UseWebSocketsProxyMiddleware();
});
builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware);
builder.UseResponderMiddleware();
builder.UseDownstreamRouteFinderMiddleware();
builder.UseSecurityMiddleware();
if (pipelineConfiguration.MapWhenOcelotPipeline != null)
{
foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
{
builder.MapWhen(pipeline);
}
}
builder.UseHttpHeadersTransformationMiddleware();
builder.UseDownstreamRequestInitialiser();
builder.UseRateLimiting(); builder.UseRequestIdMiddleware();
builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware);
if (pipelineConfiguration.AuthenticationMiddleware == null)
{
builder.UseAuthenticationMiddleware();
}
else
{
builder.Use(pipelineConfiguration.AuthenticationMiddleware);
}
builder.UseClaimsToClaimsMiddleware();
builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware);
if (pipelineConfiguration.AuthorisationMiddleware == null)
{
builder.UseAuthorisationMiddleware();
}
else
{
builder.Use(pipelineConfiguration.AuthorisationMiddleware);
}
builder.UseClaimsToHeadersMiddleware();
builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware);
builder.UseClaimsToQueryStringMiddleware();
builder.UseLoadBalancingMiddleware();
builder.UseDownstreamUrlCreatorMiddleware();
builder.UseOutputCacheMiddleware();
builder.UseHttpRequesterMiddleware(); return builder;
}
}

与上文所提交的“后端服务授权”类似,我们需要在Ocelot API网关上定义并实现授权策略,有可能是需要设计一些框架来简化用户数据的访问并提供灵活的、可复用的授权逻辑,由于这部分内容跟每个应用程序的业务关系较为密切,所以本文也就不深入讨论了。

总结

至此,有关Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权的介绍,就告一段落了。通过四篇文章,我们从零开始,一步步搭建微服务、基于IdentityServer4的IdentityService、Ocelot API网关以及Angular单页面应用,并逐步介绍了认证与授权的实现过程。虽然没有最终实现一个可被重用的授权框架,但基本架构也算是完整了,今后有机会我可以再补充认证、授权的相关内容,欢迎阅读并提宝贵意见。

源代码

访问以下Github地址以获取源代码:

https://github.com/daxnet/identity-demo

Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(四)的更多相关文章

  1. Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(二)

    上文已经介绍了Identity Service的实现过程.今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关. 回顾 <Angular SPA基于Ocelot ...

  2. Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(三)

    在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot ...

  3. Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)

    好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名<Angular单页面应用基于Ocelot API网关与IdentityServer4+ASP.NET Iden ...

  4. .Netcore 2.0 Ocelot Api网关教程(5)- 认证和授权

    本文介绍Ocelot中的认证和授权(通过IdentityServer4),本文只使用最简单的IdentityServer,不会对IdentityServer4进行过多讲解. 1.Identity Se ...

  5. 微服务(入门三):netcore ocelot api网关结合consul服务发现

    简介 api网关是提供给外部调用的统一入口,类似于dns,所有的请求统一先到api网关,由api网关进行指定内网链接. ocelot是基于netcore开发的开源API网关项目,功能强大,使用方便,它 ...

  6. ASP.NET Core on K8S学习之旅(13)Ocelot API网关接入

    本篇已加入<.NET Core on K8S学习实践系列文章索引>,可以点击查看更多容器化技术相关系列文章. 上一篇介绍了Ingress的基本概念和Nginx Ingress的基本配置和使 ...

  7. .Netcore 2.0 Ocelot Api网关教程(7)- 限流

    本文介绍Ocelot中的限流,限流允许Api网关控制一段时间内特定api的总访问次数.限流的使用非常简单,只需要添加配置即可. 1.添加限流 修改 configuration.json 配置文件,对  ...

  8. .Netcore 2.0 Ocelot Api网关教程(6)- 配置管理

    本文介绍Ocelot中的配置管理,配置管理允许在Api网关运行时动态通过Http Api查看/修改当前配置.由于该功能权限很高,所以需要授权才能进行相关操作.有两种方式来认证,外部Identity S ...

  9. .Netcore 2.0 Ocelot Api网关教程(2)- 路由

    .Netcore 2.0 Ocelot Api网关教程(1) 路由介绍 上一篇文章搭建了一个简单的Api网关,可以实现简单的Api路由,本文介绍一下路由,即配置文件中ReRoutes,ReRoutes ...

随机推荐

  1. Rancher流水线配置文档

    2019独角兽企业重金招聘Python工程师标准>>> 一.概述 Rancher流水线从逻辑上可以分为两部分,即CI和CD. CI,可分化为克隆代码.代码打包.发布镜像三部分. CD ...

  2. openlayers3中Overlay用法

    Overlay 从名字看,是覆盖图.覆盖物的意思,主要的用途就是在地图之上再覆盖一层,用以显示额外的可见元素,可见元素一般是 HTML 元素,利用 overlay,可以将可见元素放置到地图的任意位置, ...

  3. 集合框架-day10

    day10-集合框架-对象数组的概述与引用 1 集合框架的简单介绍: A:集合的由来 数组长度是固定,当添加的元素超过了数组的长度时需要对数组重新定义,太麻烦,java内部给我们提供了集合类,能存储任 ...

  4. 详解Linux 安装 JDK、Tomcat 和 MySQL(图文并茂)

    https://www.jb51.net/article/120984.htm

  5. 再砸4.35亿美元,LG疯狂扩建太阳能电池生产线

    LG在收缩高分辨率电视和其他消费电子产品业务的同时,在太阳能面板业务上却很明显一直在进行扩张.LG公司表示,他们将斥资4.35亿美元在韩国工厂增加超过6条生产线,使其太阳能电池生产量能够在2018年达 ...

  6. resetlogs 打开数据库时新生成日志位置问题

    若系统中缺少联机日志,以resetlogs方式重建控制文件,那么当我们以alter database open resetlogs方式打开数据库时,新生成的联机日志会位于何处? 一:下面分别讨论几种情 ...

  7. #Week2 Linear Regression with One Variable

    一.Model Representation 还是以房价预测为例,一图胜千言: h表示一个从x到y的函数映射. 二.Cost Function 因为是单变量线性回归,所以假设函数是: \[h_{\th ...

  8. zabbix分布式安装全过程

    项目规划 软件 版本 IP zabbix-server 3.4.15 10.1.10.128 zabbix-proxy 3.4.15 10.1.10.129 zabbix-agent 3.4.15 1 ...

  9. 我在 IntelliJ IDEA 中必有得插件和配置

    最近在陆续写 Java 并发编程系列,好多朋私信问我的不是并发内容本身,而是我的 IDEA 主题配置.我就姑且认为好的主题配置可以写出更好的并发程序吧 即便这种可能性只有万分之一,我也要把我的 IDE ...

  10. Composition API

    介绍 Composition API的主要思想是,我们将它们定义为从新的 setup 函数返回的JavaScript变量,而不是将组件的功能(例如state.method.computed等)定义为对 ...