Ocelot網關+IdentityServer4實現API許可權認證

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 = "//localhost:7000";
option.RequireHttpsMetadata = false;
option.ApiName = "api1";
})
.AddIdentityServerAuthentication("auth2", option =>
{
option.Authority = "//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資源。

 

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

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

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

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

  第二種方式,用戶名密碼模式

 2.請求第一個API項目介面/api/values

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

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

 

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

 

 

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