前言
上一篇【.Net Core微服务入门全纪录(六)——EventBus-事件总线】中使用CAP完成了一个简单的Eventbus,实现了服务之间的解耦和异步调用,并且做到数据的最终一致性。这一篇将使用IdentityServer4来搭建一个鉴权中心,来完成授权认证相关的功能。
IdentityServer4官方文档:https://identityserver4.readthedocs.io/
鉴权中心
创建ids4项目
关于IdentityServer4的基本介绍和模板安装可以看一下我的另一篇博客【IdentityServer4 4.x版本 配置Scope的正确姿势】,下面直接从创建项目开始。
来到我的项目目录下执行:dotnet new is4inmem --name IDS4.AuthCenter
执行完成后会生成以下文件:
用vs2019打开之前的解决方案,把刚刚创建的ids项目添加进来:
将此项目设为启动项,先运行看一下效果:
项目正常运行,下面需要结合我们的业务稍微修改一下默认代码。
鉴权中心配置
修改Startup的ConfigureServices方法:
// in-memory, code configbuilder.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 = { " }, FrontChannelLogoutUri = " PostLogoutRedirectUris = { " }, 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, " 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, " new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) } } }; } }}
TestUsers没有做修改,用项目模板默认生成的就行。这里定义了2个用户alice,bob,密码与用户名相同。
至此,鉴权中心的代码修改就差不多了。这个项目也不放docker了,直接用vs来启动,让他运行在9080端口。/Properties/launchSettings.json修改一下: 鉴权中心搭建完成,下面整合到之前的Ocelot.APIGateway网关项目中。 首先NuGet安装 修改Startup: 修改ocelot.json配置文件: 添加了AuthenticationOptions节点,AuthenticationProviderKey对应的是上面Startup中的定义。 既然网关是客户端访问api的统一入口,那么同样可以作为鉴权中心的入口。使用Ocelot来做代理,这样客户端也无需知道鉴权中心的地址,同样修改ocelot.json: 添加一个鉴权中心的路由,实际中鉴权中心也可以部署多个实例,也可以集成Consul服务发现,实现方式跟前面章节讲的差不多,这里就不再赘述。 让网关服务运行在9070端口,/Properties/launchSettings.json修改一下: 首先NuGet安装 修改Startup: 修改/Helper/IServiceHelper,方法定义增加accessToken参数: 修改/Helper/GatewayServiceHelper,访问接口时增加Authorization参数,传入accessToken: 最后是/Controllers/HomeController的修改。添加Authorize标记: 修改Index action,获取accessToken并传入: 至此,客户端集成也已完成。 为了方便,鉴权中心、网关、web客户端这3个项目都使用vs来启动,他们的端口分别是9080,9070,5000。之前的OrderAPI和ProductAPI还是在docker中不变。 为了让vs能同时启动多个项目,需要设置一下,解决方案右键属性: Ctor+F5启动项目。 3个项目都启动完成后,浏览器访问web客户端 src="https://gitee.com/xhznl/Blog.Images/raw/master/images/image-20200706124027549.png" alt="image-20200706124027549" loading="lazy"> 因为我还没登录,所以请求直接被重定向到了鉴权中心的登录界面。使用alice/alice这个账户登录系统。 登录成功后,进入授权同意界面,你可以同意或者拒绝,还可以选择勾选scope权限。点击Yes,Allow按钮同意授权: 同意授权后,就能正常访问客户端界面了。下面测试一下部分授权,这里没做登出功能,只能手动清理一下浏览器Cookie,ids4登出功能也很简单,可以自行百度。 清除Cookie后,刷新页面又会转到ids4的登录界面,这次使用bob/bob登录: 这次只勾选orderApiScope,点击Yes,Allow: 这次客户端就只能访问订单服务了。当然也可以在鉴权中心去限制客户端的api权限,也可以在网关层面ocelot.json中限制,相信你已经知道该怎么做了。 本文主要完成了IdentityServer4鉴权中心、Ocelot网关、web客户端之间的整合,实现了系统的统一授权认证。授权认证是几乎每个系统必备的功能,而IdentityServer4是.Net Core下优秀的授权认证方案。再次推荐一下B站@solenovex 杨老师的视频,地址:https://www.bilibili.com/video/BV16b411k7yM ,虽然视频有点老了,但还是非常受用。 需要代码的点这里:https://github.com/xiajingren/NetCoreMicroserviceDemo"applicationUrl": " id="ocelot集成ids4">Ocelot集成ids4
Ocelot保护api资源
IdentityServer4.AccessTokenValidation
public void ConfigureServices(IServiceCollection services){ services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication("orderService", options => { options.Authority = " options.ApiName = "orderApi"; options.SupportedTokens = SupportedTokens.Both; options.ApiSecret = "orderApi secret"; options.RequireHttpsMetadata = false; }) .AddIdentityServerAuthentication("productService", options => { options.Authority = " 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();}
{ "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": [] }}
Ocelot代理ids4
{ "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 9080 } ], "UpstreamPathTemplate": "/auth/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "LoadBalancerOptions": { "Type": "RoundRobin" }}
"applicationUrl": " id="客户端集成">客户端集成
Microsoft.AspNetCore.Authentication.OpenIdConnect
public void ConfigureServices(IServiceCollection services){ services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = " //options.Authority = " 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();}
/// <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);
public async Task<string> GetOrder(string accessToken){ var Client = new RestClient(" 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(" 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;}
[Authorize]public class HomeController : Controller
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();}
测试
总结
没有评论:
发表评论