欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-04-08 23:10:31
一.概述 在Ocelot中,为了保护下游api资源,用户访问时需要进行认证鉴权,这需要在Ocelot 网关中添加认证服务。添加认证后,ReRoutes路由会进行身份验证,并使用Ocelot的基于声明的功能。在Startup.cs中注册认证服务,为每个注册提供一个方案 (authenticationP ......

一.概述

  在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网关 - 身份验证