作者: markjiang7m2
源码地址: https://gitee.com/Sevenm2/OcelotDemo
本文是我关于 Ocelot 系列文章的第四篇, 认证与授权. 在前面的系列文章中, 我们的下游服务接口都是公开的, 没有经过任何的认证, 只要知道接口的调用方法, 任何人都可以随意调用, 因此, 很容易就造成信息泄露或者服务被攻击.
正如, 我要找 Willing 干活之前, 我得先到 HR 部门那里登记并且拿到属于我自己的工卡, 然后我带着我的工卡去找 Willing, 亮出我是公司员工的身份, 并且有权利要求他帮我完成一个任务.
在这里集成一套 .net core 的服务认证框架 IdentityServer4, 以及如何在 Ocelot 中接入 IdentityServer4 的认证与授权.
跟上一篇 Ocelot(三)- 服务发现文章中的 Consul 类似, 这一个是关于 Ocelot 的系列文章, 我暂时也不打算详细展开说明 IdentityServer4, 在本文中也是使用 IdentityServer4 最简单的 Client 认证模式.
关于更多的 Ocelot 功能介绍, 可以查看我的系列文章
Ocelot - .Net Core 开源网关
Ocelot(二)- 请求聚合与负载均衡
Ocelot(三)- 服务发现
本文中涉及案例的完整代码都可以从我的代码仓库进行下载.
仓库地址: https://gitee.com/Sevenm2/OcelotDemo
IdentityServer4 使用
IdentityServer4 有多种认证模式, 包括用户密码, 客户端等等, 我这里只需要实现 IdentityServer4 的验证过程即可, 因此, 我选择了使用最简单的客户端模式.
首先我们来看, 当没有 Ocelot 网关时系统是如何使用 IdentityServer4 进行认证的.
客户端需要先想 IdentityServer 请求认证, 获得一个 Token, 然后再带着这个 Token 向下游服务发出请求.
我尝试根据流程图搭建出这样的认证服务.
创建 IdentityServer 服务端
新建一个空的 ASP.NET Core Web API 项目, 因为这个项目只做 IdentityServer 服务端, 因此, 我将 Controller 也直接删除掉.
使用 NuGet 添加 IdentityServer4, 可以直接使用 NuGet 包管理器搜索 IdentityServer4 进行安装, 或者通过 VS 中内置的 PowerShell 执行下面的命令行
Install-Package IdentityServer4
在 appsettings.JSON 中添加 IdentityServer4 的配置
- {
- "Logging": {
- "LogLevel": {
- "Default": "Warning"
- }
- },
- "SSOConfig": {
- "ApiResources": [
- {
- "Name": "identityAPIService",
- "DisplayName": "identityAPIServiceName"
- }
- ],
- "Clients": [
- {
- "ClientId": "mark",
- "ClientSecrets": [ "markjiang7m2" ],
- "AllowedGrantTypes": "ClientCredentials",
- "AllowedScopes": [ "identityAPIService" ]
- }
- ]
- },
- "AllowedHosts": "*"
- }
ApiResources 为数组类型, 表示 IdentityServer 管理的所有的下游服务列表
Name: 下游服务名称
DisplayName: 下游服务别名
Clients 为数组类型, 表示 IdentityServer 管理的所有的上游客户端列表
ClientId: 客户端 ID
ClientSecrets: 客户端对应的密钥
AllowedGrantTypes: 该客户端支持的认证模式, 目前支持如下:
- Implicit
- ImplicitAndClientCredentials
- Code
- CodeAndClientCredentials
- Hybrid
- HybridAndClientCredentials
- ClientCredentials
- ResourceOwnerPassword
- ResourceOwnerPasswordAndClientCredentials
- DeviceFlow
AllowedScopes: 该客户端支持访问的下游服务列表, 必须是在 ApiResources 列表中登记的
新建一个类用于读取 IdentityServer4 的配置
- using IdentityServer4.Models;
- using Microsoft.Extensions.Configuration;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- namespace IdentityServer
- {
- public class SSOConfig
- {
- public static IEnumerable<ApiResource> GetApiResources(IConfigurationSection section)
- {
- List<ApiResource> resource = new List<ApiResource>();
- if (section != null)
- {
- List<ApiConfig> configs = new List<ApiConfig>();
- section.Bind("ApiResources", configs);
- foreach (var config in configs)
- {
- resource.Add(new ApiResource(config.Name, config.DisplayName));
- }
- }
- return resource.ToArray();
- }
- /// <summary>
- /// 定义受信任的客户端 Client
- /// </summary>
- /// <returns></returns>
- public static IEnumerable<Client> GetClients(IConfigurationSection section)
- {
- List<Client> clients = new List<Client>();
- if (section != null)
- {
- List<ClientConfig> configs = new List<ClientConfig>();
- section.Bind("Clients", configs);
- foreach (var config in configs)
- {
- Client client = new Client();
- client.ClientId = config.ClientId;
- List<Secret> clientSecrets = new List<Secret>();
- foreach (var secret in config.ClientSecrets)
- {
- clientSecrets.Add(new Secret(secret.Sha256()));
- }
- client.ClientSecrets = clientSecrets.ToArray();
- GrantTypes grantTypes = new GrantTypes();
- var allowedGrantTypes = grantTypes.GetType().GetProperty(config.AllowedGrantTypes);
- client.AllowedGrantTypes = allowedGrantTypes == null ?
- GrantTypes.ClientCredentials : (ICollection<string>)allowedGrantTypes.GetValue(grantTypes, null);
- client.AllowedScopes = config.AllowedScopes.ToArray();
- clients.Add(client);
- }
- }
- return clients.ToArray();
- }
- }
- public class ApiConfig
- {
- public string Name { get; set; }
- public string DisplayName { get; set; }
- }
- public class ClientConfig
- {
- public string ClientId { get; set; }
- public List<string> ClientSecrets { get; set; }
- public string AllowedGrantTypes { get; set; }
- public List<string> AllowedScopes { get; set; }
- }
- }
在 Startup.cs 中注入 IdentityServer 服务
- public void ConfigureServices(IServiceCollection services)
- {
- var section = Configuration.GetSection("SSOConfig");
- services.AddIdentityServer()
- .AddDeveloperSigningCredential()
- .AddInMemoryApiResources(SSOConfig.GetApiResources(section))
- .AddInMemoryClients(SSOConfig.GetClients(section));
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
- }
使用 IdentityServer 中间件
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- App.UseIdentityServer();
- App.UseMvc();
- }
配置完成, 接下来用 Debug 模式看看 IdentityServer 是否可用, 尝试向 IdentityServer 进行认证. 因为需要使用 post 方式, 而且在认证请求的 body 中加入认证信息, 所以我这里借助 Postman https://www.getpostman.com/downloads/ 工具完成.
请求路径:<host>+/connect/token
如果认证正确, 会得到如下结果:
如果认证失败, 则会返回如下:
这样, 最简单的 IdentityServer 服务就配置完成了. 当然, 我刚刚为了快速验证 IdentityServer 服务是否搭建成功, 所以使用的是 Debug 模式, 接下来要使用的话, 还是要通过 IIS 部署使用的, 我这里就把 IdentityServer 服务部署到 8005 端口.
下游服务加入认证
在 OcelotDownAPI 项目中, 使用 NuGet 添加 AccessTokenValidation 包, 可以直接使用 NuGet 包管理器搜索 IdentityServer4.AccessTokenValidation 进行安装, 或者通过 VS 中内置的 PowerShell 执行下面的命令行
Install-Package IdentityServer4.AccessTokenValidation
在 appsettings.JSON 中加入 IdentityServer 服务信息
- "IdentityServerConfig": {
- "ServerIP": "localhost",
- "ServerPort": 8005,
- "IdentityScheme": "Bearer",
- "ResourceName": "identityAPIService"
- }
这里的 identityAPIService 就是在 IdentityServer 服务端配置 ApiResources 列表中登记的其中一个下游服务.
在 Startup.cs 中读取 IdentityServer 服务信息, 加入 IdentityServer 验证
- public void ConfigureServices(IServiceCollection services)
- {
- IdentityServerConfig identityServerConfig = new IdentityServerConfig();
- Configuration.Bind("IdentityServerConfig", identityServerConfig);
- services.AddAuthentication(identityServerConfig.IdentityScheme)
- .AddIdentityServerAuthentication(options =>
- {
- options.RequireHttpsMetadata = false;
- options.Authority = $"http://{identityServerConfig.IP}:{identityServerConfig.Port}";
- options.ApiName = identityServerConfig.ResourceName;
- }
- );
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
- }
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- App.UseAuthentication();
- App.UseMvc();
- }
根据前面的配置, 我们添加一个需要授权的下游服务 API
注意添加属性[Authorize]
因为我这里只是为了演示 IdentityServer 的认证流程, 所以我只是在其中一个 API 接口中添加该属性, 如果还有其他接口需要整个认证, 就需要在其他接口中添加该属性, 如果是这个 Controller 所有的接口都需要 IdentityServer 认证, 那就直接在类名前添加该属性.
- using Microsoft.AspNetCore.Authorization;
- // GET API/ocelot/identityWilling
- [HttpGet("identityWilling")]
- [Authorize]
- public async Task<IActionResult> IdentityWilling(int id)
- {
- var result = await Task.Run(() =>
- {
- ResponseResult response = new ResponseResult()
- { Comment = $"我是 Willing, 既然你是我公司员工, 那我就帮你干活吧, host: {HttpContext.Request.Host.Value}, path: {HttpContext.Request.Path}" };
- return response;
- });
- return Ok(result);
- }
重新打包 OcelotDownAPI 项目, 并发布到 8001 端口.
首先, 像之前那样直接请求 API, 得到如下结果:
得到了 401 的状态码, 即未经授权.
因此, 我必须先向 IdentityServer 请求认证并授权
然后将得到的 Token 以 Bearer 的方式加入到向下游服务的请求当中, 这样我们就可以得到了正确的结果
可能有些朋友在这里会有点疑惑, 在 Postman 中我们在 Authorization 中加入这个 Token, 但是在我们实际调用中该怎么加入 Token?
其实熟悉 Postman 的朋友可能就知道怎么一回事, Postman 为了我们在使用过程中更加方便填入 Token 信息而单独列出了 Authorization, 实际上, 最终还是会转换加入到请求头当中
这个请求头的 Key 就是 Authorization, 对应的值是 Bearer + (空格) + Token.
以上就是没有 Ocelot 网关时, IdentityServer 的认证流程.
案例五 Ocelot 集成 IdentityServer 服务
在上面的例子中, 我是直接将下游服务暴露给客户端调用, 当接入 Ocelot 网关时, 我们要达到内外互隔的特性, 于是就把 IdentityServer 服务也托管到 Ocelot 网关中, 这样我们就能统一认证和服务请求时的入口.
于是, 我们可以形成下面这个流程图:
根据流程图, 我在 Ocelot ReRoutes 中添加两组路由
- {
- "DownstreamPathTemplate": "/connect/token",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": 8005
- }
- ],
- "UpstreamPathTemplate": "/token",
- "UpstreamHttpMethod": [ "Post" ],
- "Priority": 2
- },
- {
- "DownstreamPathTemplate": "/api/ocelot/identityWilling",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": 8001
- }
- ],
- "UpstreamPathTemplate": "/ocelot/identityWilling",
- "UpstreamHttpMethod": [ "Get" ],
- "Priority": 2
- }
第一组是将 IdentityServer 服务进行托管, 这样客户端就可以直接通过 Ocelot 网关访问 / token 就可以进行认证, 第二组是将下游服务进行托管
然后, 也是按照之前例子的步骤, 先通过 http://localhost:4727/token 认证, 然后将得到的 Token 以 Bearer 的方式加入到向下游服务的请求当中
结果也是跟我预想的是一致的, 可以按照这样的流程进行身份认证.
但是!!! 但是!!! 但是!!!
当外面随便来一个人, 跟前台说他要找我做一件事情, 然后前台直接告诉他我的具体位置, 就让他进公司找我了, 然后当我接待他的时候, 我才发现这个人根本就是来搞事的, 拒绝他的请求. 如果一天来这么几十号人, 我还要不要正常干活了?
这明显就不符合实际应用场景, 外面的人 (客户端) 在前台 (Ocelot) 的时候, 就需要进行身份认证(IdentityServer), 只有通过认证的人才能进公司(路由), 我才会接触到这个人(响应), 这才叫专人做专事.
于是, 认证流程改为下图:
准备下游服务
为了保证我的案例与上面这个认证流程是一致的, 我就把前面在下游服务中的认证配置去掉. 而且在实际生产环境中, 客户端与下游服务的网络是隔断的, 客户端只能通过网关的转发才能向下游服务发出请求.
OcelotDownAPI 项目
- public void ConfigureServices(IServiceCollection services)
- {
- //IdentityServerConfig identityServerConfig = new IdentityServerConfig();
- //Configuration.Bind("IdentityServerConfig", identityServerConfig);
- //services.AddAuthentication(identityServerConfig.IdentityScheme)
- // .AddIdentityServerAuthentication(options =>
- // {
- // options.RequireHttpsMetadata = false;
- // options.Authority = $"http://{identityServerConfig.IP}:{identityServerConfig.Port}";
- // options.ApiName = identityServerConfig.ResourceName;
- // }
- // );
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
- }
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- //App.UseAuthentication();
- App.UseMvc();
- }
同时也把 API 接口中的 [Authorize] 属性去除.
然后将 OcelotDownAPI 项目重新打包, 部署在 8001,8002 端口, 作为两个独立的下游服务.
配置 IdentityServer
回到 IdentityServer 项目的 appsettings.JSON, 在 ApiResources 中另外添加两个服务
- {
- "Name": "identityAPIService8001",
- "DisplayName": "identityAPIService8001Name"
- },
- {
- "Name": "identityAPIService8002",
- "DisplayName": "identityAPIService8002Name"
- }
在 Clients 中添加两个 Client
- {
- "ClientId": "markfull",
- "ClientSecrets": [ "markjiang7m2" ],
- "AllowedGrantTypes": "ClientCredentials",
- "AllowedScopes": [ "identityAPIService8001", "identityAPIService8002" ]
- },
- {
- "ClientId": "marklimit",
- "ClientSecrets": [ "123456" ],
- "AllowedGrantTypes": "ClientCredentials",
- "AllowedScopes": [ "identityAPIService8001" ]
- }
这里我为了能让大家看出允许访问范围的效果, 特意分配了两个不同的 AllowedScopes.
使用 markfull 登录的客户端可以同时请求 identityAPIService8001 和 identityAPIService8002 两个下游服务, 而使用 marklimit 登录的客户端只允许请求 identityAPIService8001 服务.
Ocelot 集成 IdentityServer 认证
跟前面的例子一样, 要支持 IdentityServer 认证, OcelotDemo 项目就需要安装 IdentityServer4.AccessTokenValidation 包.
OcelotDemo 项目的 appsettings.JSON 添加 IdentityServer 信息
- "IdentityServerConfig": {
- "IP": "localhost",
- "Port": 8005,
- "IdentityScheme": "Bearer",
- "Resources": [
- {
- "Key": "APIService8001",
- "Name": "identityAPIService8001"
- },
- {
- "Key": "APIService8002",
- "Name": "identityAPIService8002"
- }
- ]
- }
当然这个配置项的结构是任意的, 我这里的 Resources 数组配置的就是 Ocelot 网关支持哪些服务的认证, Name 就是服务的名称, 同时会唯一对应一个 Key.
为了能更加方便读取 IdentityServerConfig 的信息, 我定义了一个跟它同结构的类
- public class IdentityServerConfig
- {
- public string IP { get; set; }
- public string Port { get; set; }
- public string IdentityScheme { get; set; }
- public List<APIResource> Resources { get; set; }
- }
- public class APIResource
- {
- public string Key { get; set; }
- public string Name { get; set; }
- }
然后来到 Startup.cs 的 ConfigureServices 方法, 就能很快地将 IdentityServer 信息进行注册
- var identityBuilder = services.AddAuthentication();
- IdentityServerConfig identityServerConfig = new IdentityServerConfig();
- Configuration.Bind("IdentityServerConfig", identityServerConfig);
- if (identityServerConfig != null && identityServerConfig.Resources != null)
- {
- foreach (var resource in identityServerConfig.Resources)
- {
- identityBuilder.AddIdentityServerAuthentication(resource.Key, options =>
- {
- options.Authority = $"http://{identityServerConfig.IP}:{identityServerConfig.Port}";
- options.RequireHttpsMetadata = false;
- options.ApiName = resource.Name;
- options.SupportedTokens = SupportedTokens.Both;
- });
- }
- }
Configure 方法中添加
App.UseAuthentication();
最后, 就是配置 Ocelot.JSON 文件.
在 ReRoutes 中添加两组路由
- {
- "DownstreamPathTemplate": "/api/ocelot/identityWilling",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": 8001
- }
- ],
- "UpstreamPathTemplate": "/ocelot/8001/identityWilling",
- "UpstreamHttpMethod": [ "Get" ],
- "Priority": 2,
- "AuthenticationOptions": {
- "AuthenticationProviderKey": "APIService8001",
- "AllowedScopes": []
- }
- },
- {
- "DownstreamPathTemplate": "/api/ocelot/identityWilling",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": 8002
- }
- ],
- "UpstreamPathTemplate": "/ocelot/8002/identityWilling",
- "UpstreamHttpMethod": [ "Get" ],
- "Priority": 2,
- "AuthenticationOptions": {
- "AuthenticationProviderKey": "APIService8002",
- "AllowedScopes": []
- }
- }
跟其他普通路由相比, 这两组路由都多了一个 AuthenticationOptions 属性, 它里面的 AuthenticationProviderKey 就是我们在前面 ConfigureServices 方法中登记过的 Key.
我们来捋顺一下这个路由跟认证授权过程. 以 markfull 的 ID 和这里的第一组路由为例.
客户端拿着 markfull 的 clientID 向 IdentityServer(
http://localhost:4727/token
)进行认证, 得到了一个的 Token
客户端带着这个 Token, 因此有了 markfull 的身份, 请求 Url 地址
http://localhost:4727/ocelot/8001/identityWilling
Ocelot 网关接收到请求, 根据路由表找到了认证支持关键字为 APIService8001, 从而得到了对应的 IdentityServer 服务信息: IdentityServer 服务地址为
http://localhost:8005
, 下游服务名称为
identityAPIService8001
Ocelot 带着 Token 向 IdentityServer 服务(
http://localhost:8005
)进行配对, 即查看 markfull 身份的访问范围是否包含了
identityAPIService8001
服务
Ocelot 认证过 markfull 是允许访问的, 将请求转发到下游服务中, 根据路由配置, 下游服务地址为
http://localhost:8001/API/ocelot/identityWilling
下面我将 Ocelot 运行起来, 并通过 Postman 进行验证.
markfull 身份认证
使用 markfullClientId 向 IdentityServer 进行认证
向 8001 请求
将得到的 Token 加入到请求中, 请求 Url 地址 http://localhost:4727/ocelot/8001/identityWilling, 得到下游服务返回的响应结果
向 8002 请求
将得到的 Token 加入到请求中, 请求 Url 地址 http://localhost:4727/ocelot/8002/identityWilling, 得到下游服务返回的响应结果
然后, 更换 marklimit 身份再验证一遍
marklimit 身份认证
使用 marklimitClientId 向 IdentityServer 进行认证
向 8001 请求
将得到的 Token 加入到请求中, 请求 Url 地址 http://localhost:4727/ocelot/8001/identityWilling, 得到下游服务返回的响应结果
向 8002 请求
将得到的 Token 加入到请求中, 请求 Url 地址 http://localhost:4727/ocelot/8002/identityWilling, 此时, 我们得到了 401 的状态码, 即未授权.
总结
在这篇文章中就跟大家介绍了基于 IdentityServer4 为认证服务器的 Ocelot 认证与授权, 主要是通过一些案例的实践, 让大家理解 Ocelot 对客户端身份的验证过程, 使用了 IdentityServer 中最简单的客户端认证模式, 因为这种模式下 IdentityServer 的认证没有复杂的层级关系. 但通常在我们实际开发时, 更多的可能是通过用户密码等方式进行身份认证的, 之后我会尽快给大家分享关于 IdentityServer 如何使用其它模式进行认证. 今天就先跟大家介绍到这里, 希望大家能持续关注我们.
来源: https://www.cnblogs.com/markjiang7m2/p/10932805.html