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

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

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

View Code

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

三、新增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 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(),};}}

  这里使用了上一篇的内容,不再解释。实际环境中,则应该考虑从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;}}

View Code

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

3.4 改写StarUp类

    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>();......}

  这里高亮的是新增的部分,为了实现自定义验证。关于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<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){......// IdentityServerservices.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

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

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

  1. .NET Core微服务之基于Ocelot实现API网关服务

    一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端实现和微服务应用程序之间的沟通方式.以前的话,客户端不得不去请 ...

  2. .NET Core微服务之基于Ocelot实现API网关服务(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.负载均衡与请求缓存 1.1 负载均衡 为了验证负载均衡,这里我们配置了两个Consul Client节点,其中ClientServic ...

  3. .NET Core微服务之基于IdentityServer建立授权与验证服务(续)

    上一篇<.NET Core微服务之基于IdentityServer建立授权与验证服务>我们基于IdentityServer4建立了一个AuthorizationServer,并且继承了Qu ...

  4. .NET Core微服务之基于Exceptionless实现分布式日志记录

    一.Exceptionless极简介绍 Exceptionless 是一个开源的实时的日志收集框架,它可以应用在基于 ASP.NET,ASP.NET Core,Web API,Web Forms,WP ...

  5. .NET Core微服务之基于Consul实现服务治理

    一.Consul基础介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分布式服务注册与发现的方案,比如 Airbnb的SmartStack等相比,Co ...

  6. .NET Core微服务之基于Consul实现服务治理(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 上一篇发布之后,很多人点赞和评论,不胜惶恐,这一篇把上一篇没有弄到的东西补一下,也算是给各位前来询问的朋友的一些回复吧. 一.Consul ...

  7. .NET Core微服务之基于MassTransit实现数据最终一致性(Part 1)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.预备知识:数据一致性 关于数据一致性的文章,园子里已经有很多了,如果你还不了解,那么可以通过以下的几篇文章去快速地了解了解,有个感性认 ...

  8. .NET Core微服务之基于EasyNetQ使用RabbitMQ消息队列

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.消息队列与RabbitMQ 1.1 消息队列 "消息"是在两台计算机间传送的数据单位.消息可以非常简单,例如只包含 ...

  9. .NET Core微服务之基于IdentityServer建立授权与验证服务

    一.IdentityServer的预备知识 要学习IdentityServer,事先得了解一下基于Token的验证体系,这是一个庞大的主题,涉及到Token,OAuth&OpenID,JWT, ...

最新文章

  1. mysql 视图 临时表模式
  2. Errors occurred during the build
  3. JVM的生命周期||JVM的发展历程
  4. 可用于企业的 7 个最佳开源 Web 服务器
  5. 在arm上运行x86动态库_苹果官宣:Arm取代英特尔
  6. hbase 监控指标项
  7. 1.4.5 动态字段
  8. 静态网页HTML知识点归纳
  9. java web实现聊天系统_java web在线聊天系统
  10. 小米游戏本0909bios_小米笔记本电脑全家族介绍
  11. RdViewer(远程控制电脑屏幕软件)官方中文版V3.3.1 | rd远程工具下载
  12. Django中使用163邮箱发送邮件
  13. android应用层学习经验
  14. macOS Big Sur 11.7.1 (20G918) 正式版 ISO、PKG、DMG、IPSW 下载
  15. node.js之async的使用(series,whilst)
  16. Unity打包生成APK文件提交应用宝踩坑记录
  17. laravel结合easyWeChat的使用
  18. 美通社企业新闻汇总 | 2019.2.1 | 迪士尼推出猪年限定产品;阿联酋航空与南航签订代码共享协议...
  19. web 流程设计器探究
  20. 可取性、适用性、可行性:内存计算技术的影响

热门文章

  1. (八)适配器模式详解
  2. 对一条常用命令(netstat结合awk统计TCP连接数)的理解
  3. 【STM32 .Net MF开发板学习-25】LED数码管显示
  4. 备份恢复文件夹NTFS安全设置
  5. 监听服务端口及邮件报警脚本
  6. ACM-ICPC 2018 焦作赛区网络预赛 J(二分+JAVA高精)
  7. psp进度(11月25号-31号)
  8. noip2016前的话[漫谈]
  9. hust1350Trie【字典树+dfs || 字典树 + LCA】
  10. [Tomcat]Tomcat6和Tomcat7的区别