• .Net Core微服务入门全纪录(七)——IdentityServer4-授权认证


    Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章。

    前言

    上一篇【.Net Core微服务入门全纪录(六)——EventBus-事件总线】中使用CAP完成了一个简单的Eventbus,实现了服务之间的解耦和异步调用,并且做到数据的最终一致性。这一篇将使用IdentityServer4来搭建一个鉴权中心,来完成授权认证相关的功能。

    IdentityServer4官方文档:https://identityserver4.readthedocs.io/

    鉴权中心

    创建ids4项目

    关于IdentityServer4的基本介绍和模板安装可以看一下我的另一篇博客【IdentityServer4 4.x版本 配置Scope的正确姿势】,下面直接从创建项目开始。

    来到我的项目目录下执行:dotnet new is4inmem --name IDS4.AuthCenter

    image-20200629210341489

    执行完成后会生成以下文件:

    image-20200629210446718

    用vs2019打开之前的解决方案,把刚刚创建的ids项目添加进来:

    image-20200629210933318

    将此项目设为启动项,先运行看一下效果:

    image-20200629211848802

    image-20200629212102283

    项目正常运行,下面需要结合我们的业务稍微修改一下默认代码。

    鉴权中心配置

    修改Startup的ConfigureServices方法:

    // in-memory, code config
    builder.AddInMemoryIdentityResources(Config.IdentityResources);
    builder.AddInMemoryApiScopes(Config.ApiScopes);
    builder.AddInMemoryApiResources(Config.ApiResources);
    builder.AddInMemoryClients(Config.Clients);
    

    Config类:

    public static class Config
    {
        public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
    
        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {
                new ApiResource("orderApi","订单服务")
                {
                    ApiSecrets ={ new Secret("orderApi secret".Sha256()) },
                    Scopes = { "orderApiScope" }
                },
                new ApiResource("productApi","产品服务")
                {
                    ApiSecrets ={ new Secret("productApi secret".Sha256()) },
                    Scopes = { "productApiScope" }
                }
            };
    
        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("orderApiScope"),
                new ApiScope("productApiScope"),
            };
    
        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                new Client
                {
                    ClientId = "web client",
                    ClientName = "Web Client",
    
                    AllowedGrantTypes = GrantTypes.Code,
                    ClientSecrets = { new Secret("web client secret".Sha256()) },
    
                    RedirectUris = { "http://localhost:5000/signin-oidc" },
                    FrontChannelLogoutUri = "http://localhost:5000/signout-oidc",
                    PostLogoutRedirectUris = { "http://localhost:5000/signout-callback-oidc" },
    
                    AllowedScopes = new [] {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "orderApiScope", "productApiScope"
                    },
                    AllowAccessTokensViaBrowser = true,
    
                    RequireConsent = true,//是否显示同意界面
                    AllowRememberConsent = false,//是否记住同意选项
                }
            };
    }
    

    Config中定义了2个api资源:orderApi,productApi。2个Scope:orderApiScope,productApiScope。1个客户端:web client,使用Code授权码模式,拥有openid,profile,orderApiScope,productApiScope 4个scope。

    TestUsers类:

    public class TestUsers
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };
                
                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "818727",
                        Username = "alice",
                        Password = "alice",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "Alice Smith"),
                            new Claim(JwtClaimTypes.GivenName, "Alice"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    },
                    new TestUser
                    {
                        SubjectId = "88421113",
                        Username = "bob",
                        Password = "bob",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "Bob Smith"),
                            new Claim(JwtClaimTypes.GivenName, "Bob"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    }
                };
            }
        }
    }
    

    TestUsers没有做修改,用项目模板默认生成的就行。这里定义了2个用户alice,bob,密码与用户名相同。

    至此,鉴权中心的代码修改就差不多了。这个项目也不放docker了,直接用vs来启动,让他运行在9080端口。/Properties/launchSettings.json修改一下:"applicationUrl": "http://localhost:9080"

    Ocelot集成ids4

    Ocelot保护api资源

    鉴权中心搭建完成,下面整合到之前的Ocelot.APIGateway网关项目中。

    首先NuGet安装IdentityServer4.AccessTokenValidation

    image-20200706100658900

    修改Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication("orderService", options =>
            {
                options.Authority = "http://localhost:9080";//鉴权中心地址
                options.ApiName = "orderApi";
                options.SupportedTokens = SupportedTokens.Both;
                options.ApiSecret = "orderApi secret";
                options.RequireHttpsMetadata = false;
            })
            .AddIdentityServerAuthentication("productService", options =>
            {
                options.Authority = "http://localhost:9080";//鉴权中心地址
                options.ApiName = "productApi";
                options.SupportedTokens = SupportedTokens.Both;
                options.ApiSecret = "productApi secret";
                options.RequireHttpsMetadata = false;
            });
    
        //添加ocelot服务
        services.AddOcelot()
            //添加consul支持
            .AddConsul()
            //添加缓存
            .AddCacheManager(x =>
            {
                x.WithDictionaryHandle();
            })
            //添加Polly
            .AddPolly();
    }
    

    修改ocelot.json配置文件:

    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      ......
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "productService",
        "AllowScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      ......
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "orderService",
        "AllowScopes": []
      }
    }
    

    添加了AuthenticationOptions节点,AuthenticationProviderKey对应的是上面Startup中的定义。

    Ocelot代理ids4

    既然网关是客户端访问api的统一入口,那么同样可以作为鉴权中心的入口。使用Ocelot来做代理,这样客户端也无需知道鉴权中心的地址,同样修改ocelot.json:

    {
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 9080
        }
      ],
      "UpstreamPathTemplate": "/auth/{url}",
      "UpstreamHttpMethod": [
        "Get",
        "Post"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
    

    添加一个鉴权中心的路由,实际中鉴权中心也可以部署多个实例,也可以集成Consul服务发现,实现方式跟前面章节讲的差不多,这里就不再赘述。

    让网关服务运行在9070端口,/Properties/launchSettings.json修改一下:"applicationUrl": "http://localhost:9070"

    客户端集成

    首先NuGet安装Microsoft.AspNetCore.Authentication.OpenIdConnect

    image-20200706121544645

    修改Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost:9070/auth";//通过网关访问鉴权中心
                //options.Authority = "http://localhost:9080";
    
                options.ClientId = "web client";
                options.ClientSecret = "web client secret";
                options.ResponseType = "code";
    
                options.RequireHttpsMetadata = false;
    
                options.SaveTokens = true;
    
                options.Scope.Add("orderApiScope");
                options.Scope.Add("productApiScope");
            });
    
        services.AddControllersWithViews();
        
        //注入IServiceHelper
        //services.AddSingleton<IServiceHelper, ServiceHelper>();
        
        //注入IServiceHelper
        services.AddSingleton<IServiceHelper, GatewayServiceHelper>();
    }
    
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceHelper serviceHelper)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseStaticFiles();
    
        app.UseRouting();
    
        app.UseAuthentication();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    
        //程序启动时 获取服务列表
        //serviceHelper.GetServices();
    }
    

    修改/Helper/IServiceHelper,方法定义增加accessToken参数:

    /// <summary>
    /// 获取产品数据
    /// </summary>
    /// <param name="accessToken"></param>
    /// <returns></returns>
    Task<string> GetProduct(string accessToken);
    
    /// <summary>
    /// 获取订单数据
    /// </summary>
    /// <param name="accessToken"></param>
    /// <returns></returns>
    Task<string> GetOrder(string accessToken);
    

    修改/Helper/GatewayServiceHelper,访问接口时增加Authorization参数,传入accessToken:

    public async Task<string> GetOrder(string accessToken)
    {
        var Client = new RestClient("http://localhost:9070");
        var request = new RestRequest("/orders", Method.GET);
        request.AddHeader("Authorization", "Bearer " + accessToken);
    
        var response = await Client.ExecuteAsync(request);
        if (response.StatusCode != HttpStatusCode.OK)
        {
            return response.StatusCode + " " + response.Content;
        }
        return response.Content;
    }
    
    public async Task<string> GetProduct(string accessToken)
    {
        var Client = new RestClient("http://localhost:9070");
        var request = new RestRequest("/products", Method.GET);
        request.AddHeader("Authorization", "Bearer " + accessToken);
    
        var response = await Client.ExecuteAsync(request);
        if (response.StatusCode != HttpStatusCode.OK)
        {
            return response.StatusCode + " " + response.Content;
        }
        return response.Content;
    }
    

    最后是/Controllers/HomeController的修改。添加Authorize标记:

    [Authorize]
    public class HomeController : Controller
    

    修改Index action,获取accessToken并传入:

    public async Task<IActionResult> Index()
    {
        var accessToken = await HttpContext.GetTokenAsync("access_token");
    
        ViewBag.OrderData = await _serviceHelper.GetOrder(accessToken);
        ViewBag.ProductData = await _serviceHelper.GetProduct(accessToken);
    
        return View();
    }
    

    至此,客户端集成也已完成。

    测试

    为了方便,鉴权中心、网关、web客户端这3个项目都使用vs来启动,他们的端口分别是9080,9070,5000。之前的OrderAPI和ProductAPI还是在docker中不变。

    为了让vs能同时启动多个项目,需要设置一下,解决方案右键属性:

    image-20200706123144511

    Ctor+F5启动项目。

    3个项目都启动完成后,浏览器访问web客户端:http://localhost:5000/

    image-20200706124027549

    因为我还没登录,所以请求直接被重定向到了鉴权中心的登录界面。使用alice/alice这个账户登录系统。

    image-20200706124523974

    登录成功后,进入授权同意界面,你可以同意或者拒绝,还可以选择勾选scope权限。点击Yes,Allow按钮同意授权:

    image-20200706124924213

    同意授权后,就能正常访问客户端界面了。下面测试一下部分授权,这里没做登出功能,只能手动清理一下浏览器Cookie,ids4登出功能也很简单,可以自行百度。

    image-20200706125257382

    清除Cookie后,刷新页面又会转到ids4的登录界面,这次使用bob/bob登录:

    image-20200706125759968

    这次只勾选orderApiScope,点击Yes,Allow:

    image-20200706130140730

    这次客户端就只能访问订单服务了。当然也可以在鉴权中心去限制客户端的api权限,也可以在网关层面ocelot.json中限制,相信你已经知道该怎么做了。

    总结

    本文主要完成了IdentityServer4鉴权中心、Ocelot网关、web客户端之间的整合,实现了系统的统一授权认证。授权认证是几乎每个系统必备的功能,而IdentityServer4是.Net Core下优秀的授权认证方案。再次推荐一下B站@solenovex 杨老师的视频,地址:https://www.bilibili.com/video/BV16b411k7yM ,虽然视频有点老了,但还是非常受用。

    需要代码的点这里:https://github.com/xiajingren/NetCoreMicroserviceDemo

  • 相关阅读:
    ListView滑动位置精准记忆
    2 GPS utility methods
    PingUtil in Android
    Android客户端的图形化拖放操作的设计实现
    腾讯视频去广告代码
    ie9不支持line-height_ie9不支持字体垂直居中兼容问题解决篇_IE9中字体不能垂直居中解决方法
    Web App开发的七个错误
    邮件页面的设计与网页页面设计的区别
    如何在ecshop商品页显示累计销售量
    如何让iframe的背景透明
  • 原文地址:https://www.cnblogs.com/xhznl/p/13132260.html
Copyright © 2020-2023  润新知