查阅了大多数相关资料,查阅到的IdentityServer4 的相关文章大可能是比较简单而且可能是翻译官网的文档编写的,我这里在
Asp.Net Core 中IdentityServer4 的应用分析中会以一个电商系统架构升级过程当中广泛会遇到的场景进行实战性讲述分析,同时最后会把个人实战性的代码放到github 上,敬请你们关注!html
这里就直接开始撸代码,概念性东西就已经不概述了,想要了解概念推荐你们查看我以前的文章和官方文档:git
最初小团队的电商系统场景以下图:
github
这张架构图缺点:数据库
大多数小电商团队对于多客户端登陆受权来讲可能已经实现了Oauth 2.0 的身份受权验证,可是是和电商业务集成在一个网关里面,这样不是很好的方式;因为公司业务横向扩大,产品经理调研了代理商业务,最终让技术开发代理商业务系统。架构师出于后续发展的各方面考虑,把代理商业务单独创建了一个独立的网关,而且把受权服务一并给独立出来,调整后的电商系统架构图以下:
api
身份受权从业务系统中拆分出来后,有了以下的优点:架构
代理商业务引入进来后,同时又增长了秒杀活动,发现成交量大大增大,支付订单集中在某一时刻翻了十几倍,这时候整个电商业务API网关已经扛不住了,负载了几台可能也有点吃力;开发人员通过跟架构师一块儿讨论,得出了扛不住的缘由:主要是秒杀活动高并发的支付,以致于整个电商业务系统受到影响,故准备把支付系统从业务系统中拆分出成独立的支付网关,并作了必定的负载,成功解决了以上问题,这时候整个电商系统架构图就演变成以下:
并发
支付网关服务抽离后的优点:app
受权中心
:单独一个服务网关,访问支付业务网关
、电商业务网关
及代理商业务网关
都须要先经过受权中心
得到受权拿到访问令牌AccessToken
才能正常的访问这些网关,这样受权模块就不会受任何的业务影响,同时各个业务网关也不须要写一样的受权业务的代码;业务网关
仅仅只需关注自己的业务便可,受权中心
仅仅只须要关注维护受权;通过这样升级改造后整个系统维护性获得很大的提升,相关的业务也能够针对具体状况进行选择性的扩容。运维
上面的电商网关演变架构图中我这里没有画出具体的请求流向,偷了个赖,这里仍是先把OAuth2.0 的受权大致的流程图单独贴出来:
dom
因为受权网关服务
以前单独抽离出来了,此次把支付业务网关拆分出来就也比较顺利,一会儿就完成了电商系统的架构升级。今天这篇文章的目的架构升级也就完成了,想要深刻后续电商系统架构升级的同窗能够关注后续给你们带来的微服务的相关教程,到时继续以这个例子来进行微服务架构上的演变升级,敬请你们关注。好了下面咱们来回归该升级的和核心主题受权网关服务
IdentityServer4
的应用。
定义资源
分资源分为身份资源(Identity resources
)和API资源(API resources
)。
咱们先建立Jlion.NetCore.Identity.Service 网关服务项目,在网关服务中添加受保护的API资源
,建立OAuthMemoryData
类代码以下:
/// <summary> /// Api资源 静态方式定义 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName), }; }
定义客户端Client
接下来OAuthMemoryData
类中定义一个客户端应用程序的Client
,咱们将使用它来访问咱们的API资源代码以下:
public static IEnumerable<Client> GetClients() { return new List<Client> { new Client() { ClientId =OAuthConfig.UserApi.ClientId, AllowedGrantTypes = new List<string>() { GrantTypes.ResourceOwnerPassword.FirstOrDefault(),//Resource Owner Password模式 }, ClientSecrets = {new Secret(OAuthConfig.UserApi.Secret.Sha256()) }, AllowedScopes= {OAuthConfig.UserApi.ApiName}, AccessTokenLifetime = OAuthConfig.ExpireIn, }, }; }
AllowedGrantTypes
:配置受权类型,能够配置多个受权类型ClientSecrets
:客户端加密方式AllowedScopes
:配置受权范围,这里指定哪些API 受此方式保护AccessTokenLifetime
:配置Token 失效时间GrantTypes
:受权类型,这里使用的是密码模式ResourceOwnerPassword
代码中能够看到有一个OAuthConfig
类,这个类是我单独建的,是用于统一管理,方便维护,代码以下:
public class OAuthConfig { /// <summary> /// 过时秒数 /// </summary> public const int ExpireIn = 36000; /// <summary> /// 用户Api相关 /// </summary> public static class UserApi { public static string ApiName = "user_api"; public static string ClientId = "user_clientid"; public static string Secret = "user_secret"; } }
若是后续架构升级,添加了其余的网关服务,则只须要在这里添加所须要保护的API 资源,也能够经过读取数据库方式读取受保护的Api资源。
接下来OAuthMemoryData
类添加测试用户,代码以下:
/// <summary> /// 测试的帐号和密码 /// </summary> /// <returns></returns> public static List<TestUser> GetTestUsers() { return new List<TestUser> { new TestUser() { SubjectId = "1", Username = "test", Password = "123456" } }; }
上面受保护的资源,和客户端以及测试帐号都已经创建好了,如今须要把IdentityServer4 注册到DI中:
Startup
中的ConfigureServices
代码以下:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); #region 内存方式 services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(OAuthMemoryData.GetApiResources()) .AddInMemoryClients(OAuthMemoryData.GetClients()) .AddTestUsers(OAuthMemoryData.GetTestUsers()); #endregion }
代码解读:
AddDeveloperSigningCredential
:添加证书加密方式,执行该方法,会先判断tempkey.rsa证书文件是否存在,若是不存在的话,就建立一个新的tempkey.rsa证书文件,若是存在的话,就使用此证书文件。AddInMemoryApiResources
:把受保护的Api资源添加到内存中AddInMemoryClients
:客户端配置添加到内存中AddTestUsers
:测试的用户添加进来最后经过UseIdentityServer()
须要把IdentityServer4 中间件添加到Http管道中,代码以下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseIdentityServer(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
好了,如今受权网关服务
代码已经完成,如今直接经过命令行方式启动,命令行启动以下,我指定5000端口,以下图:
如今我来新建一个WebApi 大的用户网关服务项目,取名为Jlion.NetCore.Identity.UserApiService
,新建后会默认有一个天气预报的api接口,代码以下:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } }
接下来在Startup
类中添加受权网关服务的配置到DI中,代码以下:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddAuthorization(); services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:5000"; //配置Identityserver的受权地址 options.RequireHttpsMetadata = false; //不须要https options.ApiName = OAuthConfig.UserApi.ApiName; //api的name,须要和config的名称相同 }); }
这里的options.ApiName
须要和网关服务中的Api 资源配置中的ApiName 一致
接下来须要把受权和认证中间件分别注册到Http 管道中,代码以下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
如今受权服务网关启用已经完成,只须要在须要保护的Controller
中添加 Authorize
过滤器便可,如今我也经过命令行把须要保护的网关服务启动,如图:
如今我经过postman 工具来单独访问 用户网关服务API,不携带任何信息的状况下,如图:
从访问结果能够看出返回401 Unauthorized
未受权。
咱们接下来再来访问受权服务网关,如图:
请求网关服务中body中携带了用户名及密码等相关信息,这是返回了access_token
及有效期等相关信息,咱们再拿access_token
来继续上面的操做,访问用户业务网关的接口,如图:
访问结果中已经返回了咱们所须要的接口数据,你们目前已经对密码模式的使用有了必定的了解,可是这时候可能会有人问我,我生产环境中可能须要经过数据库的方式进行用户信息的判断,以及客户端受权方式须要更加灵活的配置,可经过后台来配置ClientId以及受权方式等,那应该怎么办呢?下面我再来给你们带来生存环境中的实现方式。
咱们须要经过用户名和密码到数据库中验证方式则须要实现IResourceOwnerPasswordValidator
接口,并实现ValidateAsync
验证方法,简单的代码以下:
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { try { var userName = context.UserName; var password = context.Password; //验证用户,这么能够到数据库里面验证用户名和密码是否正确 var claimList = await ValidateUserAsync(userName, password); // 验证帐号 context.Result = new GrantValidationResult ( subject: userName, authenticationMethod: "custom", claims: claimList.ToArray() ); } catch (Exception ex) { //验证异常结果 context.Result = new GrantValidationResult() { IsError = true, Error = ex.Message }; } } #region Private Method /// <summary> /// 验证用户 /// </summary> /// <param name="loginName"></param> /// <param name="password"></param> /// <returns></returns> private async Task<List<Claim>> ValidateUserAsync(string loginName, string password) { //TODO 这里能够经过用户名和密码到数据库中去验证是否存在, // 以及角色相关信息,我这里仍是使用内存中已经存在的用户和密码 var user = OAuthMemoryData.GetTestUsers(); if (user == null) throw new Exception("登陆失败,用户名和密码不正确"); return new List<Claim>() { new Claim(ClaimTypes.Name, $"{loginName}"), }; } #endregion }
用户密码验证器已经实现完成,如今须要把以前的经过AddTestUsers
方式改为AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
方式,修改后的代码以下:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); #region 数据库存储方式 services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(OAuthMemoryData.GetApiResources()) .AddInMemoryClients(OAuthMemoryData.GetClients()) //.AddTestUsers(OAuthMemoryData.GetTestUsers()); .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>(); #endregion }
目前已经实现了用户名和密码数据库验证的方式,可是如今有人会考虑另一个场景,客户端的受权方式等也须要经过后台可配置的方式,这样比较灵活,不经过代码中静态配置的方式,那应该这么办呢?
官方考虑的很周到,咱们可使用IClientStore
接口,同时须要实现FindClientByIdAsync
方法,代码以下:
public class ClientStore : IClientStore { public async Task<Client> FindClientByIdAsync(string clientId) { #region 用户名密码 var memoryClients = OAuthMemoryData.GetClients(); if (memoryClients.Any(oo => oo.ClientId == clientId)) { return memoryClients.FirstOrDefault(oo => oo.ClientId == clientId); } #endregion #region 经过数据库查询Client 信息 return GetClient(clientId); #endregion } private Client GetClient(string client) { //TODO 根据数据库查询 return null; } }
Startup
中ConfigureServices
代码AddInMemoryClients
改为AddClientStore<>
代码以下:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); #region 数据库存储方式 services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(OAuthMemoryData.GetApiResources()) //.AddInMemoryClients(OAuthMemoryData.GetClients()) .AddClientStore<ClientStore>() .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>(); #endregion }
好了数据库查询匹配方式也已经改造完了,业务网关服务不须要改动如何代码,运行结果这里就不在运行演示了。Demo 代码已经上传到github 上了,github 源代码地址:https://github.com/a312586670/IdentityServerDemo
结语:经过IdentityServer4 实现的简单受权中心的思想也就完成了,后续继续学习,有错误地方还请留言指出!感谢!!!