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地址,  架构如下图所示:

asp.net core系列 60 Ocelot 构建服务认证示例

  

二. 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/中进行查看,如下所示:

asp.net core系列 60 Ocelot 构建服务认证示例

  在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();
        }

asp.net core系列 60 Ocelot 构建服务认证示例

  

  参考文献

    在ASP.NET核心中使用Ocelot构建API网关 - 身份验证

      官方文档

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

KnockOut 绑定之foreach绑定

2020-11-9 4:35:28

随笔日记

使用 ASP.NET Core MVC 创建 Web API(五)

2020-11-9 4:35:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索