博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权
阅读量:6840 次
发布时间:2019-06-26

本文共 14679 字,大约阅读时间需要 48 分钟。

Tip: 此篇已加入

一、案例结构总览

  这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为客户端的统一入口)先向IdentityService进行Login以进行验证并获取Token,在IdentityService的验证过程中会访问数据库以验证。然后再带上Token通过API网关去访问具体的API Service。这里我们的IdentityService基于IdentityServer4开发,它具有统一登录验证和授权的功能。

二、改写API Gateway

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

2.1 配置文件的改动

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

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

2.2 改写StartUp类

public void ConfigureServices(IServiceCollection services)    {        // IdentityServer        #region IdentityServerAuthenticationOptions => need to refactor        Action
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
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"]; }; #endregion services.AddAuthentication() .AddIdentityServerAuthentication("ClientServiceKey", isaOptClient) .AddIdentityServerAuthentication("ProductServiceKey", isaOptProduct); // Ocelot services.AddOcelot(Configuration); ...... }

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

"IdentityService": {    "Uri": "http://localhost:5100",    "UseHttps": false,    "ApiSecrets": {      "clientservice": "clientsecret",      "productservice": "productsecret"    }  }
View Code

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

三、新增IdentityService

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

3.1 准备工作

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

3.2 定义一个InMemoryConfiguration用于测试

///     /// One In-Memory Configuration for IdentityServer => Just for Demo Use    ///     public class InMemoryConfiguration    {        public static IConfiguration Configuration { get; set; }        ///         /// Define which APIs will use this IdentityServer        ///         /// 
public static IEnumerable
GetApiResources() { return new[] { new ApiResource("clientservice", "CAS Client Service"), new ApiResource("productservice", "CAS Product Service"), new ApiResource("agentservice", "CAS Agent Service") }; } ///
/// Define which Apps will use thie IdentityServer /// ///
public static IEnumerable
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 hour AllowAccessTokensViaBrowser = true // can return access_token to this client } }; } ///
/// Define which IdentityResources will use this IdentityServer /// ///
public static IEnumerable
GetIdentityResources() { return new List
{ new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } }

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

3.3 定义一个ResourceOwnerPasswordValidator

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

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;        }    }

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

  

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

public class LoginUserRepository : RepositoryBase
, ILoginUserRepository { public LoginUserRepository(IdentityDbContext dbContext) : base(dbContext) { } public LoginUser Authenticate(string _userName, string _userPassword) { var entity = DbContext.LoginUsers.FirstOrDefault(p => p.UserName == _userName && p.Password == _userPassword); return entity; } }
View Code

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

3.4 改写StarUp类

public void ConfigureServices(IServiceCollection services)    {        // IoC - DbContext        services.AddDbContextPool
( options => options.UseSqlServer(Configuration["DB:Dev"])); // IoC - Service & Repository services.AddScoped
(); services.AddScoped
(); // IdentityServer4 string 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
() .AddProfileService
(); ...... }

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

public class ProfileService : IProfileService    {        public async Task GetProfileDataAsync(ProfileDataRequestContext context)        {            var claims = context.Subject.Claims.ToList();            context.IssuedClaims = claims.ToList();        }        public async Task IsActiveAsync(IsActiveContext context)        {            context.IsActive = true;        }    }
View Code

3.5 新增统一Login入口

  这里新增一个LoginController:

[Produces("application/json")]    [Route("api/Login")]    public class LoginController : Controller    {        private IConfiguration configuration;        public LoginController(IConfiguration _configuration)        {            configuration = _configuration;        }        [HttpPost]        public async Task
RequestToken([FromBody]LoginRequestParam model) { Dictionary
dict = new Dictionary
(); 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"); } } }

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

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

3.6 加入API网关中

  在API网关的Ocelot配置文件中加入配置,配置如下(这里我是开发用,所以没有用服务发现,实际环境建议采用服务发现):

// --> Identity Service Part    {      "UseServiceDiscovery": false, // do not use Consul service discovery in DEV env      "DownstreamPathTemplate": "/api/{url}",      "DownstreamScheme": "http",      "DownstreamHostAndPorts": [        {          "Host": "localhost",          "Port": "5100"        }      ],      "ServiceName": "CAS.IdentityService",      "LoadBalancerOptions": {        "Type": "RoundRobin"      },      "UpstreamPathTemplate": "/api/identityservice/{url}",      "UpstreamHttpMethod": [ "Get", "Post" ],      "RateLimitOptions": {        "ClientWhitelist": [ "admin" ], // 白名单        "EnableRateLimiting": true, // 是否启用限流        "Period": "1m", // 统计时间段:1s, 5m, 1h, 1d        "PeriodTimespan": 15, // 多少秒之后客户端可以重试        "Limit": 10 // 在统计时间段内允许的最大请求数量      },      "QoSOptions": {        "ExceptionsAllowedBeforeBreaking": 2, // 允许多少个异常请求        "DurationOfBreak": 5000, // 熔断的时间,单位为秒        "TimeoutValue": 3000 // 如果下游请求的处理时间超过多少则视如该请求超时      },      "HttpHandlerOptions": {        "UseTracing": false // use butterfly to tracing request chain      },      "ReRoutesCaseSensitive": false // non case sensitive    }

四、改写业务API Service

4.1 ClientService

  (1)安装IdentityServer4.AccessTokenValidation

NuGet>Install-Package IdentityServer4.AccessTokenValidation

  (2)改写StartUp类

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

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

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

4.2 ProductService

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

五、测试

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

  (1)统一验证&获取token (by API网关)

  

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

  

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

  

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

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

  (1)统一验证&获取token

  

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

  

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

  

六、小结

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

示例代码

  Click Here =>

参考资料

  杨中科,《》

  

 

你可能感兴趣的文章
第四课-第二讲04_02_权限及权限管理
查看>>
Python入门小程序(一)
查看>>
Spring Batch 介绍
查看>>
高德地图入门(一)——工程配置
查看>>
手机上把PDF转换成Word文档的方法,很实用幺
查看>>
学习JVM-运行时数据区
查看>>
NSOperation 简介和应用
查看>>
必读的Python入门书籍,你都看过吗?(内有福利)
查看>>
嵌入式arm产品相关知识及应用
查看>>
python select模块详解
查看>>
mac 系统下 php生成目录,移动保存文件问题
查看>>
Hibernate中update()和merge()的区别
查看>>
jmeter学习笔记之二——创建一个简单的压测脚本
查看>>
我的友情链接
查看>>
如何 Scale Up/Down Deployment?- 每天5分钟玩转 Docker 容器技术(126)
查看>>
页面$未定义的问题
查看>>
nginx
查看>>
关于Java的相关基础信息
查看>>
50款漂亮的国外婚礼邀请函设计(上篇)
查看>>
Java调用DotNet WebService为什么那么难?
查看>>