ASP.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权
Tip: 此篇已加入.NET Core微服务基础系列文章索引
一、案例结构总览
这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为客户端的统一入口)先向IdentityService进行Login以进行验证并获取Token,在IdentityService的验证过程中会访问数据库以验证。然后再带上Token通过API网关去访问具体的API Service。这里我们的IdentityService基于IdentityServer4开发,它具有统一登录验证和授权的功能。
二、改写API Gateway
这里主要基于前两篇已经搭好的API Gateway进行改写,如不熟悉,可以先浏览前两篇文章:Part 1和Part 2。
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<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"]; }; #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" } }
这里的定义方式,我暂时还没想好怎么重构,不过肯定是需要重构的,不然这样一个一个写比较繁琐,且不利于配置。
三、新增IdentityService
这里我们会基于之前基于IdentityServer的两篇文章,新增一个IdentityService,不熟悉的朋友可以先浏览一下Part 1和Part 2。
3.1 准备工作
新建一个ASP.NET Core Web API项目,绑定端口5100,NuGet安装IdentityServer4。配置好证书,并设置其为“较新则复制”,以便能够在生成目录中读取到。
3.2 定义一个InMemoryConfiguration用于测试
/// <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 hour AllowAccessTokensViaBrowser = 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(), }; } }
这里使用了上一篇的内容,不再解释。实际环境中,则应该考虑从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<LoginUser, IdentityDbContext>, 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; } }
其他具体逻辑请参考示例代码。
3.4 改写StarUp类
public void ConfigureServices(IServiceCollection services) { // IoC - DbContext services.AddDbContextPool<IdentityDbContext>( options => options.UseSqlServer(Configuration["DB:Dev"])); // IoC - Service & Repository services.AddScoped<ILoginUserService, LoginUserService>(); services.AddScoped<ILoginUserRepository, LoginUserRepository>(); // 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<ResourceOwnerPasswordValidator>() .AddProfileService<ProfileService>(); ...... }
这里高亮的是新增的部分,为了实现自定义验证。关于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; } }
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<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"); } } }
这里假设客户端会传递用户名,密码以及客户端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 => 点我进入GitHub
参考资料
杨中科,《.NET Core微服务介绍课程》
作者:周旭龙
出处:http://edisonchou.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
ASP.NET Core微服务之基于IdentityServer建立授权与验证服务(2)
_Tip:_此篇已加入.NET Core微服务基础系列文章索引 上一篇我们基于IdentityServer4建立了一个AuthorizationServer,并且继承了QuickStartUI,能够成功获取Token了。这一篇我们了解下如何集成API Service和MVC Web Application。 一、集成API Service 1.1 添加ASP.NET Core WebAPI项目 新建两个WebAPI程序,假设这里取名为ApiService01(占用端口5010)和ApiService02(占用端口5020)。 为了方便快速启动,可以继续在launchSettings.json中删掉关于IISExpress的部分,由于是WebAPI,所以也不需要启动浏览器(将其设为false): { "profiles": { "Manulife.DNC.MSAD.IdentityServer4Test.ApiService01": { "commandName": "Project", "launchBrowser": false, "launchUrl": "api/values",...
- 下一篇
『创造 Cloud Toolkit』贡献排行榜——如何参与定义一款 IDE 插件?
自从我们团队在去年12月发布 Cloud Toolkit(一款让开发部署效率提速 8 倍的 IDE 插件)以来,已帮助数以万计的开发者们提高了云上的部署效率,期间,开发者们不仅积极地向 Cloud Toolkit 团队提出需求及优化建议,而且还热心地帮助别人进行答疑解惑。 为了感谢所有为 Cloud Toolkit 发展做出贡献的开发者,我们团队重磅推出 「创造 Cloud Toolkit」奖励机制,跟随插件的更新迭代,长期有效。我们将记录您对插件付出的每一份贡献,寻找 Cloud Toolkit 创始人,在此,我们盛情邀请您一起来参与创造 Cloud Toolkit,共同定义一款真正属于我们自己的 IDE 插件。 点击,了解更多,加入创造 您的贡献类别(包括但不限于以下形式)和对应分数如下: 请主动将您的贡献公布在钉钉群/微信群,我们会
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS6,CentOS7官方镜像安装Oracle11G