Ocelot閘道器+IdentityServer4實現API許可權認證

跳躍的鍵盤手發表於2020-07-06

Ocelot是一個用.NET Core實現並且開源的API閘道器,它功能強大,包括了:路由、請求聚合、服務發現、認證、鑑權、限流熔斷、並內建了負載均衡器與Service Fabric、Butterfly Tracing整合。這些功能只都只需要簡單的配置即可完成。

本文主要向大家簡單介紹一下如何結合Ocelot閘道器和IdentityServer4鑑權服務實現API介面許可權認證。關於IdentityServer4大家可以看下我之前的文章。

好了,下面開始進入正題。我們需要搭建兩個API專案+一個IdentityServer4鑑權服務+一個Ocelot閘道器服務。本文以.NetCore2.2為例。

第一步,快速搭建兩個WebAPI專案。

 1.新建第一個WebApi專案:

 

2.配置API埠:6000

  1)配置檔案appsettings.json中增加埠配置節點。

{
  "Http": {
    "Port": 6000
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

  2)主程式Program.cs中新增埠監聽:

 1 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
 2             WebHost.CreateDefaultBuilder(args)
 3                 .ConfigureKestrel(options =>
 4                 {
 5                     //監聽埠
 6                     var config = options.ApplicationServices.GetRequiredService<IConfiguration>();
 7                     var port = config.GetValue<int>("Http:Port");
 8                     options.ListenAnyIP(port);
 9                 })
10                 .UseStartup<Startup>();

3.啟動專案

 

4.新建第二個WebAPI2專案,操作步驟同上,監聽埠6002。

 

第二步,搭建IdentityServer4鑑權服務

1.新增專案Identity4

  2.新增IdentityServer4 Nuget程式包。版本大家根據實際開發環境選擇。

 3.新增IdentityServer配置類

public class Config
    {
        /// <summary>
        /// 定義API資源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1","測試API"),
                new ApiResource("api2","測試API2")
            };
        }

        /// <summary>
        /// 定義客戶端
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client{
                    ClientId="client",
                    //授權方式為客戶端驗證,型別可參考GrantTypes列舉
                    AllowedGrantTypes=GrantTypes.ClientCredentials,
                    //祕鑰
                    ClientSecrets=
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes=new []{ "api1" }
                 },
                new Client{
                    ClientId="client2",
                    //授權方式為使用者密碼驗證,型別可參考GrantTypes列舉
                    AllowedGrantTypes=GrantTypes.ResourceOwnerPassword,
                    //祕鑰
                    ClientSecrets=
                    {
                        new Secret("secret2".Sha256())
                    },
                    AllowedScopes=new []{ "api2", IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile }
                 }
            };
        }

        /// <summary>
        /// 定義身份資源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
             };
        }
    }

這裡我們定義了兩個API資源(就是我們上面建立的兩個API專案):

    a.第一個api我們授權client以客戶端模式訪問

    b.第二個api我們授權client2以使用者密碼模式訪問

 4.針對使用者密碼訪問模式,我們這裡使用了自定義使用者認證。(資料庫使用者密碼校驗)

 我們實現介面:IResourceOwnerPasswordValidator,並通過實現介面方法ValidateAsyn()完成使用者認證。

 資料庫訪問我這裡使用的SqlSugar ORM框架,在這裡不多做介紹,感興趣的同學可以去了解一下。

public class UserPasswordValidator : IResourceOwnerPasswordValidator
    {
        private readonly IDBContext dbcontext;
        public UserPasswordValidator(IDBContext _context)
        {
            dbcontext = _context;
        }
        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
//通過sqlsugar ORM框架實現資料庫訪問
var user = await dbcontext.DB.Queryable<User>().Where(x => x.USER_NAME == context.UserName && x.PASSWORD == context.Password).FirstAsync(); if (user != null) { context.Result = new GrantValidationResult(subject: context.UserName, authenticationMethod: GrantType.ResourceOwnerPassword, claims: GetUserClaims(user)); } else context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "賬號或密碼錯誤"); } /// <summary> /// 獲取使用者宣告項 /// </summary> /// <returns></returns> private List<Claim> GetUserClaims(User user) { List<Claim> list = new List<Claim>(); list.Add(new Claim(JwtClaimTypes.Name, user.USER_NAME)); list.Add(new Claim(JwtClaimTypes.Id, user.USER_ID));
return list; } }

5.註冊IdentityServer4服務並新增中介軟體。這裡使用的就是我們上方定義的配置類以及自定義使用者認證類

新增授權客戶端:AddInMemoryClients(Config.GetClients())
新增API資源:AddInMemoryApiResources(Config.GetApiResources())
新增身份資源:AddInMemoryIdentityResources(Config.GetIdentityResources())
新增自定義使用者認證:AddResourceOwnerValidator<UserPasswordValidator>();
        public void ConfigureServices(IServiceCollection services)
        {
            //註冊服務
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryClients(Config.GetClients())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddResourceOwnerValidator<UserPasswordValidator>();

            //新增資料庫配置
            services.AddDBContext(Configuration.GetValue<string>("ConnectionStrings:DB"));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //新增IdentityServer中介軟體
            app.UseIdentityServer();
        }

6.配置API埠:7000

  1)配置檔案appsettings.json中增加埠配置節點。

{
  "Http": {
    "Port": 7000
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

  2)主程式Program.cs中新增埠監聽:

 1 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
 2             WebHost.CreateDefaultBuilder(args)
 3                 .ConfigureKestrel(options =>
 4                 {
 5                     //監聽埠
 6                     var config = options.ApplicationServices.GetRequiredService<IConfiguration>();
 7                     var port = config.GetValue<int>("Http:Port");
 8                     options.ListenAnyIP(port);
 9                 })
10                 .UseStartup<Startup>();

7.啟動專案

  

 第三步,搭建Ocelot閘道器服務

1.新建GateWay專案

  2.新增NuGet依賴包:Ocelot、Ocelot.Provider.Polly(服務質量與熔斷配置需引用Polly)、IdentityServer4.AccessTokenValidation

   

 

3.新增閘道器配置檔案ocelot.json(配置檔名稱可自定義)。

    路由是API閘道器最基本也是最核心的功能、ReRoutes下就是由多個路由節點組成。

{
    "ReRoutes": [
    ]
}
注意:16.0版本開始之後,路由使用
Routes,否則會提示找不到路由。

幾個主要節點說明:
  • DownstreamPathTemplate:下游服務路徑(實際介面請求url)
  • DownstreamScheme:下游服務http schema
  • DownstreamHostAndPorts:下游服務的地址(包含Host:IP地址 、 Port:埠號),如果使用LoadBalancer(負載均衡)的話這裡可以填多項
  • UpstreamPathTemplate: 上游服務路徑(客戶端輸入的請求Url)
  • UpstreamHttpMethod: 上游請求http方法,可使用陣列,如:Get,Post等
  • AuthenticationOptions:新增此節點表示改路由需要進行許可權認證
  • QosOptions:熔斷,配置什麼情況下停止將請求轉發到下游服務。
 
{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 6000
        }
      ],
      "UpstreamPathTemplate": "/Service1/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "auth",
        "AllowedScopes": []
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3,
        "DurationOfBreak": 5,
        "TimeoutValue": 10000
      }
    },
    {
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 6002
        }
      ],
      "UpstreamPathTemplate": "/Service2/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "auth2",
        "AllowedScopes": []
      },
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3,
        "DurationOfBreak": 5,
        "TimeoutValue": 10000
      }
    },
    {
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7000
        }
      ],
      "UpstreamPathTemplate": "/auth/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ]

    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "",
    "RateLimitOptions": {
      "ClientWhitelist": [],
      "EnableRateLimiting": true,
      "Period": "1s",
      "PeriodTimespan": 1,
      "Limit": 1000
    }
  }
}

 4.新增啟用ocelot.json並配置埠號5000

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                   .ConfigureAppConfiguration((context, builder) =>
                    {   
//新增啟用配置檔案
builder.SetBasePath(context.HostingEnvironment.ContentRootPath); builder.AddJsonFile(
"ocelot.json", optional: true, reloadOnChange: true); }) .UseKestrel(options => { //動態配置預設埠號5000 var config = options.ApplicationServices.GetRequiredService<IConfiguration>(); var httpPort = config["Http:Port"]; options.ListenAnyIP(Convert.ToInt32(httpPort)); }); UseStartup<Startup>();

5.註冊ocelot服務和Identity4認證

        public void ConfigureServices(IServiceCollection services)
        {
            //註冊ocelot服務 
            services.AddOcelot().AddPolly();
            //註冊Identity4認證
            services.AddAuthentication()
                .AddIdentityServerAuthentication("auth", option =>
                 {
                     option.Authority = "http://localhost:7000";
                     option.RequireHttpsMetadata = false;
                     option.ApiName = "api1";
                 })
                .AddIdentityServerAuthentication("auth2", option =>
                {
                    option.Authority = "http://localhost:7000";
                    option.RequireHttpsMetadata = false;
                    option.ApiName = "api2";
                });
            services.AddControllers();
        }

        
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //新增ocelot中介軟體
            app.UseOcelot().Wait();
        }

重點說明:

    1.這裡我們需要注意AddIdentityServerAuthentication中引數值auth和auth2分別對應了ocelot配置檔案中兩個API路由下鑑權節點AuthenticationOptions:AuthenticationProviderKey。

       這裡繫結的對應關係,實際上也就是說第一個api啟用的auth對應的許可權認證,並可以訪問api1資源;第二個api啟用auth2對應的許可權認證,並可訪問api2資源。

    2.option.ApiName = "api1"這裡指定可訪問api資源。此處配置的API資源來自我們在IdentityServer4服務中配置類中定義的API資源。

6.啟動專案

 

 

專案都搭建成功了,下面我們開始使用postman模擬請求,給大家演示一下效果。

1.首先請求token:IdentityServer4框架為我們開放了token獲取介面/connect/token

請求URL:http://locahost:5000/auth/connect/token   根據ocelot配置的路由上游模板auth/{url},此時會觸發下游服務:localhost:7000/connect/token  其實也就是我們搭建的IdentityServer4服務介面地址。

第一種方式,客戶端驗證模式。

  第二種方式,使用者名稱密碼模式

 2.請求第一個API專案介面/api/values

請求URL:http://locahost:5000/Service1/api/values   根據ocelot配置的路由上游模板Service1/{url},此時會觸發下游服務:localhost:6000/api/values  其實也就是我們搭建的第一個API服務介面地址。

此時,由於我們還沒有許可權,提示401

 

 我們在請求頭加入client獲取的token,再次發起請求,請求成功。

 

 

 試想一下,如果我們用client2獲取的token,再次發起請求,會發生什麼。。。可想而知,以失敗告終。那這是為什麼呢?

舉個例子方便大家理解:

    當我們以client身份獲取token之後,訪問service1下面的介面,觸發Service1配置的auth認證,此認證允許訪問資源api1;而剛好IdentityServer4服務允許client訪問api1資源,請求成功;誠然,如果以client身份訪問Service2則會失敗,因為Service2配置的auth2認證,此認證允許訪問資源api2,而IdentityServer4服務僅允許client訪問api1資源。

2.請求第一個API專案介面/api/values

請求URL:http://locahost:5000/Service2/api/values   根據ocelot配置的路由上游模板Service2/{url},此時會觸發下游服務:localhost:6002/api/values  其實也就是我們搭建的第二個API服務介面地址。

此時,由於我們還沒有許可權,提示401.

 

我們用client2獲取的token,再次發起請求,請求成功。

 

 

 想必到了這裡,大家一定也有所想,有所言,歡迎大家交流。另外,大家不妨自己動手操作演示一下,或許更容易理解。

相關文章