.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权

2023-11-06 15:32

本文主要是介绍.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、案例结构总览

640?wx_fmt=png

  这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要先向IdentityService进行Login以进行验证并获取Token,在IdentityService的验证过程中会访问数据库以验证。然后再带上Token通过API网关去访问具体的API Service。这里我们的IdentityService基于IdentityServer4开发,它具有统一登录验证和授权的功能。当然,我们也可以将统一登录验证独立出来,写成一个单独的API Service,托管在API网关中,这里我不想太麻烦,便直接将其也写在了IdentityService中。

二、改写API Gateway

  这里主要基于前两篇已经搭好的API Gateway进行改写,如不熟悉,可以先浏览前两篇文章:Part 1和Part 2。

2.1 配置文件的改动

640?wx_fmt=gif

  ......  "AuthenticationOptions": {    "AuthenticationProviderKey": "ClientServiceKey",    "AllowedScopes": []}......  "AuthenticationOptions": {    "AuthenticationProviderKey": "ProductServiceKey",    "AllowedScopes": []}......  

640?wx_fmt=gif

  上面分别为两个示例API Service增加Authentication的选项,为其设置ProviderKey。下面会对不同的路由规则设置的ProviderKey设置具体的验证方式。

2.2 改写StartUp类

640?wx_fmt=gif

    public void ConfigureServices(IServiceCollection services){        // IdentityServer#region IdentityServerAuthenticationOptions => need to refactorAction<IdentityServerAuthenticationOptions> isaOptClient = option =>{option.Authority = Configuration["IdentityService:Uri"];option.ApiName = "clientservice";option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);option.SupportedTokens = SupportedTokens.Both;option.ApiSecret = Configuration["IdentityService:ApiSecrets:clientservice"];};Action<IdentityServerAuthenticationOptions> isaOptProduct = option =>{option.Authority = Configuration["IdentityService:Uri"];option.ApiName = "productservice";option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);option.SupportedTokens = SupportedTokens.Both;option.ApiSecret = Configuration["IdentityService:ApiSecrets:productservice"];}; #endregionservices.AddAuthentication().AddIdentityServerAuthentication("ClientServiceKey", isaOptClient).AddIdentityServerAuthentication("ProductServiceKey", isaOptProduct);        // Ocelot        services.AddOcelot(Configuration);......       }

640?wx_fmt=gif

  这里的ApiName主要对应于IdentityService中的ApiResource中定义的ApiName。这里用到的配置文件定义如下:

640?wx_fmt=gif View Code

  这里的定义方式,我暂时还没想好怎么重构,不过肯定是需要重构的,不然这样一个一个写比较繁琐,且不利于配置。

三、新增IdentityService

这里我们会基于之前基于IdentityServer的两篇文章,新增一个IdentityService,不熟悉的朋友可以先浏览一下Part 1和Part 2。

3.1 准备工作

  新建一个ASP.NET Core Web API项目,绑定端口5100,NuGet安装IdentityServer4。配置好证书,并设置其为“较新则复制”,以便能够在生成目录中读取到。

3.2 定义一个InMemoryConfiguration用于测试

640?wx_fmt=gif

    /// <summary>/// One In-Memory Configuration for IdentityServer => Just for Demo Use    /// </summary>public class InMemoryConfiguration{        public static IConfiguration Configuration { get; set; }        /// <summary>/// Define which APIs will use this IdentityServer        /// </summary>/// <returns></returns>public static IEnumerable<ApiResource> GetApiResources(){            return new[]{                new ApiResource("clientservice", "CAS Client Service"),                new ApiResource("productservice", "CAS Product Service"),                new ApiResource("agentservice", "CAS Agent Service")};}        /// <summary>/// Define which Apps will use thie IdentityServer        /// </summary>/// <returns></returns>public static IEnumerable<Client> GetClients(){            return new[]{                new Client{ClientId = "cas.sg.web.nb",ClientName = "CAS NB System MPA Client",ClientSecrets = new [] { new Secret("websecret".Sha256()) },AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AllowedScopes = new [] { "clientservice", "productservice",IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Profile }},                new Client{ClientId = "cas.sg.mobile.nb",ClientName = "CAS NB System Mobile App Client",ClientSecrets = new [] { new Secret("mobilesecret".Sha256()) },AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AllowedScopes = new [] { "productservice",IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Profile }},                new Client{ClientId = "cas.sg.spa.nb",ClientName = "CAS NB System SPA Client",ClientSecrets = new [] { new Secret("spasecret".Sha256()) },AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AllowedScopes = new [] { "agentservice", "clientservice", "productservice",IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Profile }},                new Client{ClientId = "cas.sg.mvc.nb.implicit",ClientName = "CAS NB System MVC App Client",AllowedGrantTypes = GrantTypes.Implicit,RedirectUris = { Configuration["Clients:MvcClient:RedirectUri"] },PostLogoutRedirectUris = { Configuration["Clients:MvcClient:PostLogoutRedirectUri"] },AllowedScopes = new [] {IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Profile,                        "agentservice", "clientservice", "productservice"},                    //AccessTokenLifetime = 3600, // one hourAllowAccessTokensViaBrowser = true // can return access_token to this client                }};}        /// <summary>/// Define which IdentityResources will use this IdentityServer        /// </summary>/// <returns></returns>public static IEnumerable<IdentityResource> GetIdentityResources(){            return new List<IdentityResource>{                new IdentityResources.OpenId(),                new IdentityResources.Profile(),};}}

640?wx_fmt=gif

  这里使用了上一篇的内容,不再解释。实际环境中,则应该考虑从NoSQL或数据库中读取。

3.3 定义一个ResourceOwnerPasswordValidator

  在IdentityServer中,要实现自定义的验证用户名和密码,需要实现一个接口:IResourceOwnerPasswordValidator

640?wx_fmt=gif

    public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator{        private ILoginUserService loginUserService;        public ResourceOwnerPasswordValidator(ILoginUserService _loginUserService){            this.loginUserService = _loginUserService;}        public Task ValidateAsync(ResourceOwnerPasswordValidationContext context){LoginUser loginUser = null;            bool isAuthenticated = loginUserService.Authenticate(context.UserName, context.Password, out loginUser);            if (!isAuthenticated){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid client credential");}            else{context.Result = new GrantValidationResult(subject : context.UserName,authenticationMethod : "custom",claims : new Claim[] {                        new Claim("Name", context.UserName),                        new Claim("Id", loginUser.Id.ToString()),                        new Claim("RealName", loginUser.RealName),                        new Claim("Email", loginUser.Email)});}            return Task.CompletedTask;}}

640?wx_fmt=gif

  这里的ValidateAsync方法中(你也可以把它写成异步的方式,这里使用的是同步的方式),会调用EF去访问数据库进行验证,数据库的定义如下(密码应该做加密,这里只做demo,没用弄):

  640?wx_fmt=png

  至于EF部分,则是一个典型的简单的Service调用Repository的逻辑,下面只贴Repository部分:

640?wx_fmt=gif View Code

  其他具体逻辑请参考示例代码。

3.4 改写StarUp类

640?wx_fmt=gif

    public void ConfigureServices(IServiceCollection services){        // IoC - DbContextservices.AddDbContextPool<IdentityDbContext>(options => options.UseSqlServer(Configuration["DB:Dev"]));        // IoC - Service & Repositoryservices.AddScoped<ILoginUserService, LoginUserService>();services.AddScoped<ILoginUserRepository, LoginUserRepository>();        // IdentityServer4string basePath = PlatformServices.Default.Application.ApplicationBasePath;InMemoryConfiguration.Configuration = this.Configuration;services.AddIdentityServer().AddSigningCredential(new X509Certificate2(Path.Combine(basePath,Configuration["Certificates:CerPath"]),Configuration["Certificates:Password"]))            //.AddTestUsers(InMemoryConfiguration.GetTestUsers().ToList())            .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources()).AddInMemoryApiResources(InMemoryConfiguration.GetApiResources()).AddInMemoryClients(InMemoryConfiguration.GetClients())            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>().AddProfileService<ProfileService>();......}

640?wx_fmt=gif

  这里高亮的是新增的部分,为了实现自定义验证。关于ProfileService的定义如下:

640?wx_fmt=gif View Code

3.5 新增统一Login入口

  这里新增一个LoginController:

640?wx_fmt=gif

    [Produces("application/json")][Route("api/Login")]    public class LoginController : Controller{        private IConfiguration configuration;        public LoginController(IConfiguration _configuration){configuration = _configuration;}[HttpPost]        public async Task<ActionResult> RequestToken([FromBody]LoginRequestParam model){Dictionary<string, string> dict = new Dictionary<string, string>();dict["client_id"] = model.ClientId;dict["client_secret"] = configuration[$"IdentityClients:{model.ClientId}:ClientSecret"];dict["grant_type"] = configuration[$"IdentityClients:{model.ClientId}:GrantType"];dict["username"] = model.UserName;dict["password"] = model.Password;            using (HttpClient http = new HttpClient())            using (var content = new FormUrlEncodedContent(dict)){                var msg = await http.PostAsync(configuration["IdentityService:TokenUri"], content);                if (!msg.IsSuccessStatusCode){                    return StatusCode(Convert.ToInt32(msg.StatusCode));}                string result = await msg.Content.ReadAsStringAsync();                return Content(result, "application/json");}}}

640?wx_fmt=gif

  这里假设客户端会传递用户名,密码以及客户端ID(ClientId,比如上面InMemoryConfiguration中的cas.sg.web.nb或cas.sg.mobile.nb)。然后构造参数再调用connect/token接口进行身份验证和获取token。这里将client_secret等机密信息封装到了服务器端,无须客户端传递(对于机密信息一般也不会让客户端知道):

640?wx_fmt=gif

  "IdentityClients": {    "cas.sg.web.nb": {      "ClientSecret": "websecret",      "GrantType": "password"},    "cas.sg.mobile.nb": {      "ClientSecret": "mobilesecret",      "GrantType": "password"}}

640?wx_fmt=gif

四、改写业务API Service

4.1 ClientService

  (1)安装IdentityServer4.AccessTokenValidation

NuGet>Install-Package IdentityServer4.AccessTokenValidation

  (2)改写StartUp类

640?wx_fmt=gif

    public IServiceProvider ConfigureServices(IServiceCollection services){......        // IdentityServerservices.AddAuthentication(Configuration["IdentityService:DefaultScheme"]).AddIdentityServerAuthentication(options =>{options.Authority = Configuration["IdentityService:Uri"];options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);});......}

640?wx_fmt=gif

  这里配置文件的定义如下:

640?wx_fmt=gif

  "IdentityService": {    "Uri": "http://localhost:5100",    "DefaultScheme":  "Bearer",    "UseHttps": false,    "ApiSecret": "clientsecret"}

640?wx_fmt=gif

4.2 ProductService

  与ClientService一致,请参考示例代码。

五、测试

5.1 测试Client: cas.sg.web.nb

  (1)统一验证&获取token

  640?wx_fmt=png

  (2)访问clientservice (by API网关)

  640?wx_fmt=png

  (3)访问productservice(by API网关)

  640?wx_fmt=png

5.2 测试Client: cas.sg.mobile.nb

  由于在IdentityService中我们定义了一个mobile的客户端,但是其访问权限只有productservice,所以我们来测试一下:

  (1)统一验证&获取token

  640?wx_fmt=png

  (2)访问ProductService(by API网关)

  640?wx_fmt=png

  (3)访问ClientService(by API网关) => 401 Unauthorized

  640?wx_fmt=png

六、小结

  本篇主要基于前面Ocelot和IdentityServer的文章的基础之上,将Ocelot和IdentityServer进行结合,通过建立IdentityService进行统一的身份验证和授权,最后演示了一个案例以说明如何实现。不过,本篇实现的Demo还存在诸多不足,比如需要重构的代码较多如网关中各个Api的验证选项的注册,没有对各个请求做用户角色和权限的验证等等,相信随着研究和深入的深入,这些都可以逐步解决。后续会探索一下数据一致性的基本知识以及框架使用,到时再做一些分享。

示例代码

  Click Here => 点我进入GitHub

参考资料

  杨中科,《.NET Core微服务介绍课程》

  640?wx_fmt=jpeg


这篇关于.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/357489

相关文章

Flutter实现文字镂空效果的详细步骤

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模... 目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建U

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

在.NET平台使用C#为PDF添加各种类型的表单域的方法

《在.NET平台使用C#为PDF添加各种类型的表单域的方法》在日常办公系统开发中,涉及PDF处理相关的开发时,生成可填写的PDF表单是一种常见需求,与静态PDF不同,带有**表单域的文档支持用户直接在... 目录引言使用 PdfTextBoxField 添加文本输入域使用 PdfComboBoxField

gradle第三方Jar包依赖统一管理方式

《gradle第三方Jar包依赖统一管理方式》:本文主要介绍gradle第三方Jar包依赖统一管理方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录背景实现1.顶层模块build.gradle添加依赖管理插件2.顶层模块build.gradle添加所有管理依赖包

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1