ASP.NET core 系列 60 Ocelot 构建服务认证示例 一. 概述
在 Ocelot 中, 为了保护下游 API 资源, 用户访问时需要进行认证鉴权, 这需要在 Ocelot 网关中添加认证服务. 添加认证后, ReRoutes 路由会进行身份验证, 并使用 Ocelot 的基于声明的功能. 在 Startup.cs 中注册认证服务, 为每个注册提供一个方案 (authenticationProviderKey 身份验证提供者密钥).
- // 下面是在网关项目中, 添加认证服务
- public void ConfigureServices(IServiceCollection services)
- {
- var authenticationProviderKey = "TestKey";
- services.AddAuthentication()
- .AddJwtBearer(authenticationProviderKey, x =>
- {
- //..
- });
- }
其中 TestKey 是此提供程序已注册的方案, 将映射到 ReRoute 的配置中
- "AuthenticationOptions": {
- "AuthenticationProviderKey": "TestKey",
- "AllowedScopes": []
- }
当 Ocelot 运行时, 会查看此 configuration.JSON 中的 AuthenticationProviderKey 节点, 并检查是否使用给定密钥, 该密钥是否已注册身份验证提供程序. 如果没有, 那么 Ocelot 将无法启动. 如果有, 则 ReRoute 将在执行时使用该提供程序.
本次示例有四个项目:
APIGateway 网关项目 https://localhost:9000
AuthServer 项目生成 jwt 令牌服务 https://localhost:9009
CustomerAPIServices 是 web API 项目 https://localhost:9001
ClientApp 项目 模拟客户端 HttpClient
当客户想要访问 Web API 服务时, 首先访问 API 网关的身份验证模块. 我们需要首先访问 AuthServer 以获取访问令牌, 以便我们可以使用 access_token 访问受保护的 API 服务. 开源 GitHub 地址, 架构如下图所示:
二. AuthServer 项目
此服务主要用于, 为用户请求受保护的 API, 需要的 jwt 令牌. 生成 jwt 关键代码如下:
- ///
- /// 用户使用 用户名密码 来请求服务器
- /// 服务器进行验证用户的信息
- /// 服务器通过验证发送给用户一个 token
- /// 客户端存储 token, 并在每次请求时附送上这个 token 值, headers: {'Authorization': 'Bearer' + token}
- /// 服务端验证 token 值, 并返回数据
- ///
- ///
- ///
- ///
- [HttpGet]
- public IActionResult Get(string name, string pwd)
- {
- // 验证用户, 通过后发送一个 token
- if (name == "catcher" && pwd == "123")
- {
- var now = DateTime.UtcNow;
- // 添加用户的信息, 转成一组声明, 还可以写入更多用户信息声明
- var claims = new Claim[]
- {
- // 声明主题
- new Claim(JwtRegisteredClaimNames.Sub, name),
- //JWT ID 唯一标识符
- new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
- // 发布时间戳 issued timestamp
- new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
- };
- // 下面使用 Microsoft.IdentityModel.Tokens 帮助库下的类来创建 JwtToken
- // 安全秘钥
- var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_settings.Value.Secret));
- // 生成 jwt 令牌(JSON Web token)
- var jwt = new JwtSecurityToken(
- //jwt 发行方
- issuer: _settings.Value.Iss,
- //jwt 订阅者
- audience: _settings.Value.Aud,
- //jwt 一组声明
- claims: claims,
- notBefore: now,
- //jwt 令牌过期时间
- expires: now.Add(TimeSpan.FromMinutes(2)),
- // 签名凭证: 安全密钥, 签名算法
- signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
- );
- // 序列化 jwt 对象, 写入一个字符串 encodedJwt
- var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
- var responseJson = new
- {
- access_token = encodedJwt,
- expires_in = (int)TimeSpan.FromMinutes(2).TotalSeconds
- };
- // 以 JSON 形式返回
- return JSON(responseJson);
- }
- else
- {
- return JSON("");
- }
- }
- }
在之前讲 IS4 的第 55 篇中, 讲 ResourceOwnerPasswords 项目, 获取 token 也是要发送用户名和密码, 那是由 is4 来完成, 包括自动: 验证用户, 生成 jwtToken. 这里由 System.IdentityModel.Tokens 类库来生成 jwtToken. 最后返回 jwt 令牌 token 给用户.
当 catcher 用户请求: https://localhost:9009/API/auth?name=catcher&pwd=123 服务时, 产生 jwt 令牌 token, 下面是换了行的 Token, 如下所示:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
简单了解下 JWT(JSON Web Token), 它是在 Web 上以 JSON 格式传输的 Token. 该 Token 被设计为紧凑声明表示格式, 意味着字节少, 它可以在 GET URL 中, Header 中, Post Parameter 中进行传输.
JWT 一般由三段构成(Header.Payload.Signature), 用 "." 号分隔开, 是 base64 编码的, 可以把该字符串放到 https://jwt.io / 中进行查看, 如下所示:
在 Header 中: alg: 声明加密的算法, 这里为 HS256.typ: 声明类型, 这里为 JWT.
在 Payload 中: <喎"/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+oaGhoaGhoaFzdWI6INb3zOKjrCBqd3S3orK81d/D+7PGoaM8L3A+CjxwPqGhoaGhoaGhanRpOiBqd3S1xM6o0rvJ7bfdserKtqOs1vfSqtPDwLTX986q0ru0ztDUdG9rZW4stNO2+LvYsdzW2LfFuaW796Gj0rK+zcrHx+vH88n6s8m1xHRva2VusrvSu9H5oaM8L3A+CjxwPqGhoaGhoaGhaWF0OiDHqbeiyrG85DwvcD4KPHA+oaGhoaGhoaFuYmY6INTayrLDtMqxvOTWrsewo6y4w2p3dLa8yseyu7/J08O1xCzKx8qxvOS0wbjxyr2hozwvcD4KPHA+oaGhoaGhoaFleHCjump3dLXEuf3G2sqxvOSjrNXiuPa5/cbayrG85LHY0OvSqrTz09rHqbeiyrG85KGjPC9wPgo8cD6hoaGhoaGhoWFkdTogtqnUxNXfLL3TytVqd3S1xNK7t72hozwvcD4KPHA+oaGhoaGhoaFpc3M6IGp3dLXEt6LQ0Le9oaM8L3A+CjxwPqGhoaFTaWduYXR1cmUoyv3X1sepw/ujrLfA1rnQxc+isbu027jEKaO6PC9wPgo8cD6hoaGhoaGhobD8uqzBy6O6YmFzZTY0uvO1xEhlYWRlcqOsUGF5bG9hZKOsU2VjcmV0o6xzZWNyZXS+zcrH08PAtL340NBqd3S1xMept6K6zWp3dLXE0enWpKGjz+C1sdPat/7O8bbLtcTLvdS/oaO4w3NlY3JldNTayr7A/dbQo6zTw9TaQXV0aFNlcnZlcrrNQ3VzdG9tZXJBUElTZXJ2aWNlc8/uxL/W0KGjPC9wPgo8cD48L3A+CjxoMyBhbGlnbj0="left">三. CustomerAPIServices 项目
在该 Web API 项目中启用身份验证来保护 API 服务, 使用 JwtBearer, 将默认的身份验证方案设置为 TestKey. 添加身份验证代码如下:
- public void ConfigureServices(IServiceCollection services)
- {
- // 获取当前用户 (订阅者) 信息
- var audienceConfig = Configuration.GetSection("Audience");
- // 获取安全秘钥
- var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
- //token 要验证的参数集合
- var tokenValidationParameters = new TokenValidationParameters
- {
- // 必须验证安全秘钥
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = signingKey,
- // 必须验证发行方
- ValidateIssuer = true,
- ValidIssuer = audienceConfig["Iss"],
- // 必须验证订阅者
- ValidateAudience = true,
- ValidAudience = audienceConfig["Aud"],
- // 是否验证 Token 有效期, 使用当前时间与 Token 的 Claims 中的 NotBefore 和 Expires 对比
- ValidateLifetime = true,
- // 允许的服务器时间偏移量
- ClockSkew = TimeSpan.Zero,
- // 是否要求 Token 的 Claims 中必须包含 Expires
- RequireExpirationTime = true,
- };
- // 添加服务验证, 方案为 TestKey
- services.AddAuthentication(o =>
- {
- o.DefaultAuthenticateScheme = "TestKey";
- })
- .AddJwtBearer("TestKey", x =>
- {
- x.RequireHttpsMetadata = false;
- //// 在 JwtBearerOptions 配置中, IssuerSigningKey(签名秘钥),ValidIssuer(Token 颁发机构),ValidAudience(颁发给谁)三个参数是必须的.
- x.TokenValidationParameters = tokenValidationParameters;
- });
- services.AddMvc();
- }
新建一个 CustomersController 类, 在 API 方法中使用 Authorize 属性.
- [Route("api/[controller]")]
- public class CustomersController : Controller
- {
- //Authorize]: 加了该标记, 当用户请求时, 需要发送有效的 jwt
- [Authorize]
- [HttpGet]
- public IEnumerable Get()
- {
- return new string[] { "Catcher Wong", "James Li" };
- }
- // 未加授权标记, 不受保护, 任何用户都可以获取
- [HttpGet("{id}")]
- public string Get(int id)
- {
- return $"Catcher Wong - {id}";
- }
- }
下面运行, 在浏览器中直接访问 https://localhost:9001/API/customers 报 http 500 错误, 而访问 https://localhost:9001/API/customers/1 则成功 http 200, 显示 "Catcher Wong - 1"
四. APIGateway 网关
添加认证服务, 基本与 CustomerAPIServices 项目中的认证服务一样. 代码如下:
- public void ConfigureServices(IServiceCollection services)
- {
- // 获取当前用户 (订阅者) 信息
- var audienceConfig = Configuration.GetSection("Audience");
- // 获取安全秘钥
- var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
- //token 要验证的参数集合
- var tokenValidationParameters = new TokenValidationParameters
- {
- // 必须验证安全秘钥
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = signingKey,
- // 必须验证发行方
- ValidateIssuer = true,
- ValidIssuer = audienceConfig["Iss"],
- // 必须验证订阅者
- ValidateAudience = true,
- ValidAudience = audienceConfig["Aud"],
- // 是否验证 Token 有效期, 使用当前时间与 Token 的 Claims 中的 NotBefore 和 Expires 对比
- ValidateLifetime = true,
- // 允许的服务器时间偏移量
- ClockSkew = TimeSpan.Zero,
- // 是否要求 Token 的 Claims 中必须包含 Expires
- RequireExpirationTime = true,
- };
- // 添加服务验证, 方案为 TestKey
- services.AddAuthentication(o =>
- {
- o.DefaultAuthenticateScheme = "TestKey";
- })
- .AddJwtBearer("TestKey", x =>
- {
- x.RequireHttpsMetadata = false;
- // 在 JwtBearerOptions 配置中, IssuerSigningKey(签名秘钥),ValidIssuer(Token 颁发机构),ValidAudience(颁发给谁)三个参数是必须的.
- x.TokenValidationParameters = tokenValidationParameters;
- });
- // 这里也可以使用 IS4 承载令牌
- /*
- var authenticationProviderKey = "TestKey";
- Action options = o =>
- {
- o.Authority = "https://whereyouridentityserverlives.com";
- o.ApiName = "api";
- o.SupportedTokens = SupportedTokens.Both;
- o.ApiSecret = "secret";
- };
- services.AddAuthentication()
- .AddIdentityServerAuthentication(authenticationProviderKey, options);
- */
- // 添加 Ocelot 网关服务时, 包括 Secret 秘钥, Iss 发布者, Aud 订阅者
- services.AddOcelot(Configuration);
- }
在 IS4 中是由 Authority 参数指定 OIDC 服务地址, OIDC 可以自动发现 Issuer,IssuerSigningKey 等配置, 而 o.Audience 与 x.TokenValidationParameters = new TokenValidationParameters { ValidAudience = "api" }是等效的.
下面应该修改 configuration.JSON 文件. 添加一个名为 AuthenticationOptions 的新节点, 并使 AuthenticationProviderKey 与我们在 Startup 类中定义的相同.
- "ReRoutes": [
- {
- "DownstreamPathTemplate": "/api/customers",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": 9001
- }
- ],
- "UpstreamPathTemplate": "/customers",
- "UpstreamHttpMethod": [ "Get" ],
- "AuthenticationOptions": {
- "AuthenticationProviderKey": "TestKey",
- "AllowedScopes": []
- }
- }
APIGateway 网关项目和 CustomerAPIServices 项目的 appsettings.JSON 文件, 都配置了订阅者信息如下:
- {
- "Audience": {
- "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
- "Iss": "https://www.c-sharpcorner.com/members/catcher-wong",
- "Aud": "Catcher Wong"
- }
- }
五. ClientApp 项目
最后使用的客户端应用程序, 来模拟 API 网关的一些请求. 首先, 我们需要添加一个方法来获取 access_token.
- ///
- /// 获取 jwtToken
- ///
- ///
- private static string GetJwt()
- {
- HttpClient client = new HttpClient();
- //9000 是网关, 会自动转发到下游服务器,
- client.BaseAddress = new Uri( "https://localhost:9000");
- client.DefaultRequestHeaders.Clear();
- // 转发到 AuthServer 的 9009
- var res2 = client.GetAsync("/api/auth?name=catcher&pwd=123").Result;
- dynamic jwt = JsonConvert.DeserializeObject(res2.Content.ReadAsStringAsync().Result);
- return jwt.access_token;
- }
接着, 编写了三段代码 , 通过 API Gateway 网关, 来访问 CustomerAPIServices 项目中的 API 服务:
- static void Main(string[] args)
- {
- HttpClient client = new HttpClient();
- client.DefaultRequestHeaders.Clear();
- client.BaseAddress = new Uri("https://localhost:9000");
- // 1. 需要授权的 API 访问, 没有 token 时, 返回 http 状态 401
- var resWithoutToken = client.GetAsync("/customers").Result;
- Console.WriteLine($"Sending Request to /customers , without token.");
- Console.WriteLine($"Result : {resWithoutToken.StatusCode}");
- //2. 需要授权的 API 访问, 获取令牌请求 API, 返回 http 状态 200 正常
- client.DefaultRequestHeaders.Clear();
- Console.WriteLine("\nBegin Auth....");
- var jwt = GetJwt();
- Console.WriteLine("End Auth....");
- Console.WriteLine($"\nToken={jwt}");
- client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
- var resWithToken = client.GetAsync("/customers").Result;
- Console.WriteLine($"\nSend Request to /customers , with token.");
- Console.WriteLine($"Result : {resWithToken.StatusCode}");
- Console.WriteLine(resWithToken.Content.ReadAsStringAsync().Result);
- //3. 不需要授权的 API 访问, 返回 http 状态 200 正常
- Console.WriteLine("\nNo Auth Service Here");
- client.DefaultRequestHeaders.Clear();
- var res = client.GetAsync("/customers/1").Result;
- Console.WriteLine($"Send Request to /customers/1");
- Console.WriteLine($"Result : {res.StatusCode}");
- Console.WriteLine(res.Content.ReadAsStringAsync().Result);
- Console.Read();
- }
参考文献
在 ASP.NET 核心中使用 Ocelot 构建 API 网关 - 身份验证
官方文档
来源: https://www.2cto.com/kf/201904/805466.html