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网关项目 http://localhost:9000
AuthServer项目生成jwt令牌服务 http://localhost:9009
CustomerAPIServices 是web api项目 http://localhost:9001
ClientApp项目 模拟客户端HttpClient
当客户想要访问web api服务时,首先访问API网关的身份验证模块。我们需要首先访问AuthServer以获取访问令牌,以便我们可以使用access_token访问受保护的api服务。开源Github地址, 架构如下图所示:
二. AuthServer项目
此服务主要用于,为用户请求受保护的api,需要的jwt令牌。生成jwt关键代码如下:
/// <summary> ///用户使用 用户名密码 来请求服务器 ///服务器进行验证用户的信息 ///服务器通过验证发送给用户一个token ///客户端存储token,并在每次请求时附送上这个token值, headers: {'Authorization': 'Bearer ' + token} ///服务端验证token值,并返回数据 /// </summary> /// <param name="name"></param> /// <param name="pwd"></param> /// <returns></returns> [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用户请求:http://localhost:9009/api/auth?name=catcher&pwd=123服务时,产生jwt令牌token,下面是换了行的Token, 如下所示:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJjYXRjaGVyIiwianRpIjoiZWJmNWIyZGItNDg5YS00OTBjLTk0NjUtODZmOTE5YWEzMDRjIiwiaWF0IjoiMjAxOS80LzI1IDE6NTc6MjAiLCJuYmYiOjE1NTYxNTc0NDAsImV4cC
I6MTU1NjE1NzU2MCwiaXNzIjoiaHR0cDovL3d3dy5jLXNoYXJwY29ybmVyLmNvbS9tZW1iZXJzL2NhdGNoZXItd29uZyIsImF1ZCI6IkNhdGNoZXIgV29uZyJ9
.O2jI7NSnothl9Agbr0VhmdoBsXhDEoxkYNOuGaSEkkg","expires_in":120}
简单了解下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中:
sub: 主题, jwt发布者名称。
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。也就是请求生成的token不一样。
iat: 签发时间
nbf: 在什么时间之前,该jwt都是不可用的,是时间戳格式。
exp:jwt的过期时间,这个过期时间必须要大于签发时间。
adu: 订阅者,接收jwt的一方。
iss: jwt的发行方。
Signature(数字签名,防止信息被篡改):
包含了:base64后的Header,Payload ,Secret,secret就是用来进行jwt的签发和jwt的验证。相当于服务端的私钥。该secret在示例中,用在AuthServer和CustomerAPIServices项目中。
三. 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<string> Get() { return new string[] { "Catcher Wong", "James Li" }; } //未加授权标记,不受保护,任何用户都可以获取 [HttpGet("{id}")] public string Get(int id) { return $"Catcher Wong - {id}"; } }
下面运行,在浏览器中直接访问http://localhost:9001/api/customers 报http 500错误,而访问http://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<IdentityServerAuthenticationOptions> 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": "http://www.c-sharpcorner.com/members/catcher-wong", "Aud": "Catcher Wong" } }
五. ClientApp项目
最后使用的客户端应用程序,来模拟API网关的一些请求。首先,我们需要添加一个方法来获取access_token。
/// <summary> /// 获取jwtToken /// </summary> /// <returns></returns> private static string GetJwt() { HttpClient client = new HttpClient(); //9000是网关,会自动转发到下游服务器, client.BaseAddress = new Uri( "http://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("http://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(); }
参考文献