Tip: 此篇已加入.NET Core微服务基础系列文章索引

一、案例结构总览

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

二、改写API Gateway

  这里主要基于前两篇已经搭好的API Gateway进行改写,如不熟悉,可以先浏览前两篇文章:Part 1Part 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 1Part 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": ""
}
],
"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": , // 多少秒之后客户端可以重试
"Limit": // 在统计时间段内允许的最大请求数量
},
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": , // 允许多少个异常请求
"DurationOfBreak": , // 熔断的时间,单位为秒
"TimeoutValue": // 如果下游请求的处理时间超过多少则视如该请求超时
},
"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

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

.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权的更多相关文章

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

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端 ...

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

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

  3. .NET Core微服务之基于Ocelot+Butterfly实现分布式追踪

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.什么是Tracing? 微服务的特点决定了功能模块的部署是分布式的,以往在单应用环境下,所有的业务都在同一个服务器上,如果服务器出现错 ...

  4. .NET Core 微服务—API网关(Ocelot) 教程 [三]

    前言: 前一篇文章<.NET Core 微服务—API网关(Ocelot) 教程 [二]>已经让Ocelot和目录api(Api.Catalog).订单api(Api.Ordering)通 ...

  5. .NET Core微服务二:Ocelot API网关

    .NET Core微服务一:Consul服务中心 .NET Core微服务二:Ocelot API网关 .NET Core微服务三:polly熔断与降级 本文的项目代码,在文章结尾处可以下载. 本文使 ...

  6. .NET Core 微服务—API网关(Ocelot) 教程 [二]

    上篇文章(.NET Core 微服务—API网关(Ocelot) 教程 [一])介绍了Ocelot 的相关介绍. 接下来就一起来看如何使用,让它运行起来. 环境准备 为了验证Ocelot 网关效果,我 ...

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

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 上一篇我们基于IdentityServer4建立了一个AuthorizationServer,并且继承了QuickStartUI,能够成功 ...

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

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.IdentityServer的预备知识 要学习IdentityServer,事先得了解一下基于Token的验证体系,这是一个庞大的主题 ...

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

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Exceptionless极简介绍 Exceptionless 是一个开源的实时的日志收集框架,它可以应用在基于 ASP.NET,AS ...

随机推荐

  1. .Net Core版开源跨平台框架SkyMallCore

    相互学习提升,有不足之处请指教!有需要急速开发的朋友可以拿来用哦! SkyMallCore 该项目目前放在github上,功能仍在完善,已Fork的园友已给了一些建议, 我会继续完善,并将开发过程遇到 ...

  2. 深入理解Java:内省(Introspector)

    深入理解Java:内省(Introspector) 内省(Introspector) 是Java 语言对 JavaBean 类属性.事件的一种缺省处理方法. JavaBean是一种特殊的类,主要用于传 ...

  3. bzoj 4832 抵制克苏恩 概率期望dp

    考试时又翻车了..... 一定要及时调整自己的思路!!! 随从最多有7个,只有三种,所以把每一种随从多开一维 so:f[i][j][k][l]为到第i次攻击前,场上有j个1血,k个2血,l个3血随从的 ...

  4. otter代码在IDEA远程DEBUG方法

    众所周知,Otter的代码打包后,是通过Jetty启动的,Otter代码的启动脚本中自带了开启Jetty远程DEBUG的脚本,所以我们只需要在启动Otter Manager和Otter Node的时候 ...

  5. BZOJ_3555_[Ctsc2014]企鹅QQ_哈希

    BZOJ_3555_[Ctsc2014]企鹅QQ_哈希 Description PenguinQQ是中国最大.最具影响力的SNS(Social Networking Services)网站,以实名制为 ...

  6. 使用Java实现二叉树的添加,删除,获取以及遍历

    一段来自百度百科的对二叉树的解释: 在计算机科学中,二叉树是每个结点最多有两个子树的树结构.通常子树被称作“左子树”(left subtree)和“右子树”(right subtree).二叉树常被用 ...

  7. ceph 常见问题百科全书---luminous安装部署篇

    1. 执行步骤:ceph-deploy new node        机器:centos 7.5   ceph  Luminous版本     源:阿里云 问题: Traceback (most r ...

  8. 深入解读Service Mesh的数据面Envoy

    在前面的一篇文章中,详细解读了Service Mesh中的技术细节,深入解读Service Mesh背后的技术细节. 但是对于数据面的关键组件Envoy没有详细解读,这篇文章补上. 一.Envoy的工 ...

  9. 网页基础:网页设计(我所知道的所有的html和css代码(含H5和CSS3)),如有错误请批评指正

    最基础的网页设计,就是给你一个图片你做成一个网页,当然,我的工作是C#,个人网页的功底不是很高首先先认识一下网页的一些相关知识: 一般的,现在一个html网页一般包含html文件,css文件,js文件 ...

  10. HSTS 详解,让 HTTPS 更安全

    随着互联网的快速发展,人们在生活中越来越离不开互联网.无论是社交.购物还是搜索,互联网都能给人带来很多的便捷.与此同时,由于用户对网络安全的不了解和一些网站.协议的安全漏洞,让很多用户的个人信息数据“ ...