Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权

在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过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网关之后,实现用户授权可以有以下两种方式:

  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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/309550.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Java基础]反射获取成员变量并使用练习

代码如下: package ClassObjectPack;public class Student {private String name;int age;public String address;public Student(String name, int age, String address) {this.name name;this.age age;this.address address;}public Student() {}private Student(String …

基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(一)

上一篇文章使用AutoMapper来处理对象与对象之间的映射关系&#xff0c;本篇主要围绕定时任务和数据抓取相关的知识点并结合实际应用&#xff0c;在定时任务中循环处理爬虫任务抓取数据。开始之前可以删掉之前测试用的几个HelloWorld&#xff0c;没有什么实际意义&#xff0c;直…

题目 2285: [蓝桥杯][2018年第九届真题]螺旋折线(数论+思维)

题目&#xff1a; 题目描述 如图所示的螺旋折线经过平面上所有整点恰好一次。 对于整点(X, Y)&#xff0c;我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度。 例如dis(0, 1)3, dis(-2, -1)9 给出整点坐标(X, Y)&#xff0c;你能计算出dis(X, Y)吗&…

[Java基础]反射获取成员方法并使用练习

代码如下: package ClassObjectPack;public class Student {private String name;int age;public String address;public Student(String name, int age, String address) {this.name name;this.age age;this.address address;}public Student() {}private Student(String …

读懂操作系统之虚拟内存(一)

由于个人对虚拟内存这块特别感兴趣&#xff0c;所以就直接暂且跳过其他&#xff0c;接下来将通过几篇文章进行详细讲解&#xff0c;当然其他基础内容后续在我进行相应整体学习后也会同步输出文章&#xff0c;比如操作系统概念、程序链接、进程管理、页面置换算法、流水线、浮点…

[Java基础]反射练习之越过泛型检查,运行配置文件制定内容

代码如下: package ReflectTest01;import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList;public class ReflectTest01 {public static void main(String[] args) throws NoSuchMethodException, InvocationTarg…

【Ids4实战】深究配置——用户信息操作篇

&#xff08;此花无日不春风&#xff09;其实IdentityServer4的小项目已经基本完结了&#xff0c;但是我总感觉还是有很多东西没有深入挖掘和研究的&#xff0c;这不&#xff0c;二群里有小伙伴问到了一个常见的问题&#xff0c;因为我去年都见到了&#xff0c;一直没有想过去解…

Sql Server之旅——第九站 看看DML操作对索引的影响

我们都知道建索引是需要谨慎的&#xff0c;当只有利大于弊的时候才适合建&#xff0c;同时也知道建索引是需要维护成本的&#xff0c;这个维护也就在于DML操作&#xff0c;下面具体看看到底DML对索引都有哪些内幕。。。。一&#xff1a;delete操作现在大家都已经知道索引是以B树…

[Java基础]反射获取成员方法并使用

代码如下: package ClassObjectPack01;import ClassObjectPack.Student;import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;public class ReflectDemo05 {public static void main(String[] args)…

副业刚需? 恐怕并不靠谱!

点击蓝字关注&#xff0c;回复“职场进阶”获取职场进阶精品资料一份上一篇文章推了我的星球&#xff0c;两天时间就有接近200位读者加入。有「热心朋友」帮我计算了下&#xff1a;你这两天收入快2万啊&#xff0c;你这副业做的挺好啊。很不客气的说&#xff0c;如果写写公号&a…

[Java基础]Junit测试

Junit测试: 代码如下: package CalculatorPack;public class Calculator {public int add(int a,int b){return ab;}public int sub(int a,int b){return a-b;}}package CalculatorPack;import org.junit.Assert; import org.junit.Test;public class CalculatorTest {Testp…

15分钟为自己架设优雅如Github的代码仓库

前言Github大家都熟悉。除了开源的项目外&#xff0c;有时候&#xff0c;大家也会把自己或团队、公司的项目传到Github的私有仓库里&#xff0c;把Github当成自己的私人Git Server。但是&#xff0c;用Github会有一些问题&#xff1a;Github从国内访问不是很稳定&#xff0c;有…

Pseudoprime numbers POJ - 3641(快速幂+判素数)

题意&#xff1a; 给你两个数&#xff0c;p和a&#xff1b;满足两个条件&#xff1a; 1.p不是素数&#xff1b; 2.apa^{p}ap %pa; 满足则输出yes&#xff0c;反之输出no。 题目&#xff1a; Fermat’s theorem states that for any prime number p and for any integer a &g…

[Java基础]反射案列

pro.properties文件(该文件与ReflectTest01同处在同一个文件夹)&#xff1b; className domain.Person methodName eat代码如下: package domain;public class Student {public void sleep(){System.out.println("sleep...");} }package domain;public class Per…

[推荐]大量 Blazor 学习资源(三)

大量 Blazor 学习资源系列文章&#xff1a;[推荐]大量 Blazor 学习资源&#xff08;一&#xff09;[推荐]大量 Blazor 学习资源&#xff08;二&#xff09;这次主要内容有 Blazor 相关视频&#xff0c;因为本身视频是英文的&#xff0c;所以就保持原样了&#xff0c;描述没有翻…

基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(二)

上一篇使用HtmlAgilityPack抓取壁纸数据成功将图片存入数据库&#xff0c;本篇继续来完成一个全网各大平台的热点新闻数据的抓取。同样的&#xff0c;可以先预览一下我个人博客中的成品&#xff1a;https://meowv.com/hot ????????????&#xff0c;和抓取壁纸的套路…

TechEmpower Web 框架性能第19轮测试结果正式发布,ASP.NET Core在主流框架中拔得头筹...

TechEmpower第19轮编程语言框架性能排行榜2020年5月28日正式发布,详见官方博客&#xff1a;https://www.techempower.com/blog/2020/05/28/framework-benchmarks-round-19/&#xff0c;TechEmpower基准测试有许多场景&#xff08;也称为测试类型&#xff09;&#xff0c;此次评…