客戶端模式(Client Credentials Grant)


Tips:本篇已加入,.Net core 3.1 使用IdentityServer4 實現 OAuth2.0 --閱讀目錄  可點擊查看更多相關文章。

前言

 本篇會詳細刨析客戶端模式的流程圖,羅列一下此模式的利弊,適用場景。最后會用代碼進行實現,此外還會額外參雜一些相關知識的彩蛋。 


 

流程圖

客戶端模式(Client Credentials Grant) 是所有模式中最簡單的模式,所以我會盡可能的把 一些細小的,可能后面文章也會涉及到的點,都歸納在此篇中。

 

 

對於一些名詞解釋,忘記的同學可以看一下之前一篇文章,打開傳送門 

業務場景:用戶使用客戶端模式去訪問一個資源(我們可以理解成這個資源是查看數據)

上圖是用戶訪問資源的整體流程,其實除了灰色的部分,認證的流程不一樣,用戶訪問資源,資源服務器是否授權客戶端資源都是一樣的,所以之后的文章中我會只畫出灰色的認證流程部分大家要注意一下。

用文字描述一下 客戶端模式的流程就是:

1.客戶端 訪問授權服務器,提交clientid,或者clientSecret(密碼可以提交也可以不提交,具體由代碼控制)

2.授權服務器 校驗clientid (和 clientSecret)是否可以通過身份認證,通過后返回 訪問令牌(此令牌可以訪問資源服務器

 

 

 


 

 

代碼篇

我們按照官方文檔的實現步驟,實現一遍代碼。

官方實現的步驟如下:

  • Defining an API Resource   定義api資源
  • Defining the client                 定義客戶端
  • Configuring IdentityServer     配置ID4 服務端
  • Adding an API                       新增一個api
  • Creating the client                 創建一個客戶端
  • Calling the API                      調用api

 

 

 

 

 

 

如果是 使用IdentityServer4   4.x版本的話,就算按照以上步驟實現完,你會發現請求訪問總是401。

要注意一下!!!

ID4 4.x版本需要定義api的作用域,之前的版本作用域不定義的話都是默認與資源名稱相同。

在這里我額外的加了一個步驟:

  • Defining the scope  定義作用域

 

 

 

 

 

 

接下來 我們上手操作一遍:

先看一下目錄結構:

 

 前面步驟說到的 定義api資源,定義作用域,定義客戶端,我們全部寫在 5000站點項目里的 Config文件。

 大家想一下,我們要保護資源總要告訴認證授權服務器,保護資源的名稱,哪些客戶端可以看到(而客戶端權限是作用在作用域上的,作用域又將api資源分組歸類,那么這樣也就定義了哪些客戶端可以看到哪些api資源了)

 

    public static class Config
    {
        /// <summary>
        /// Defining an API Resource
        /// </summary>
        public static IEnumerable<ApiResource> Apis =>
            new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };

        /// <summary>
        /// Defining the scope
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiScope> ApiScopes =>
           new ApiScope[]
           {
                new ApiScope("api1"),
           };

        /// <summary>
        /// Defining the client
        /// </summary>
        public static IEnumerable<Client> Clients =>
            new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    // no interactive user, use the clientid/secret for authentication
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    // secret for authentication
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    // scopes that client has access to
                    AllowedScopes = { "api1" }
                }
            };
    }

接下來要完成的步驟就是:   Configuring IdentityServer  配置ID4 服務端

修改一下 IdentityServer項目中的 Startup 文件,為了讓認證授權服務啟動的時候能知道 資源相關的定義,並且啟用一下 ID4中間件。

    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            //程序啟動的時候加載 受保護的api資源,api作用域,客戶端定義
            var builder = services.AddIdentityServer() 
.AddDeveloperSigningCredential() // .AddInMemoryApiResources(Config.Apis) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients); }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseIdentityServer();//使用ID4中間件 app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } }

ID4 提供了 內存級別讀取資源,作用域和客戶端的方法 .AddInMemoryXXXXX()   上面案例中我們都是讀取的 Config類中的寫死的資源,當然也可以從配置文件中讀取,

 

 至此,認證授權的服務器全部完工。接下去我們寫一個最基本的api,在ResourceApi中我們新增一個 IdentityController。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ResourceApi.Controllers
{ 
    [Authorize]
    public class IdentityController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }
    }
}

我們給 IdentityController  加一個 用戶身份驗證 標簽  [Authorize]   這樣進來的請求就需要經過 .netcore的 身份認證。

我們修改一下 ResourceApi的 Startup文件 :

  • AddAuthentication adds the authentication services to DI and configures Bearer as the default scheme.
  • UseAuthentication adds the authentication middleware to the pipeline so authentication will be performed automatically on every call into the host.
  • UseAuthorization adds the authorization middleware to make sure, our API endpoint cannot be accessed by anonymous clients.

 

 

 

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddNewtonsoftJson(options =>
            {
                // 忽略循環引用
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                // 不使用駝峰
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
                // 設置時間格式
                options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
                // 如字段為null值,該字段不會返回到前端
                // options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
            });

            //將認證服務注冊到容器中,並且配置默認token的方案名為 Bearer
            services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = "http://localhost:5000";//授權服務器地址
                options.RequireHttpsMetadata = false; 
                options.Audience = "api1";//token能訪問的 受眾群體(這個定義要和授權服務器定義的資源名稱一樣)
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseAuthentication();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "api/{controller}/{action}");
            });
        }
    }

到這里,授權服務器代碼也好了,資源api服務也好了。剩下的就是 客戶端代碼了,我們看一下  VisitorClient這個控制台應用:

class Program
    {
        static void Main(string[] args)
        {
            // discover endpoints from metadata
            var client = new HttpClient();
            var disco = client.GetDiscoveryDocumentAsync("http://localhost:5000").Result;//IdentityModel.Client 中對client的擴展
            if (disco.IsError)
            {
                Console.WriteLine(disco.Error);
                return;
            }

            // request token  client的擴展方法 使用 客戶端模式 請求token
            var tokenResponse = client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = disco.TokenEndpoint,

                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            }).Result;

            if (tokenResponse.IsError)
            {
                Console.WriteLine(tokenResponse.Error);
                return;
            }

            Console.WriteLine(tokenResponse.Json);

            // call api  
            client.SetBearerToken(tokenResponse.AccessToken);

            var response = client.GetAsync("http://localhost:5001/api/Identity/Get").Result;
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
            }
            else
            {
                var content = response.Content.ReadAsStringAsync().Result;
                Console.WriteLine(JArray.Parse(content));
            }

            Console.ReadLine();
        }
    }

 運行下來獲取token 沒問題,但是訪問 5001 api報錯了

 

 我們雖然是按照 文檔去實現的,卻總還是磕磕盼盼,這其實是好事情,我們看一下錯誤原因,有助於我們更加了解ID4

{StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
Date: Wed, 23 Sep 2020 06:47:44 GMT
Server: Kestrel
WWW-Authenticate: Bearer error="invalid_token", error_description="The audience 'empty' is invalid"
Content-Length: 0
}}

報錯原因是這樣的,經過分析token是沒問題的,但是無效,最后想了好久,The audience 'empty' is invalid 還記不記得之前我說過 ID4 4.x版本多了一個 scope的定義,

其實我們定義api資源的時候也要告訴授權服務器,這個api的scope,我們需要改一下 5000站點里 Config文件里的定義api字段那段代碼。

/// <summary>
/// Defining an API Resource
/// </summary>
public static IEnumerable<ApiResource> Apis =>
  new List<ApiResource>
    {
      new ApiResource("api1", "My API")
      {
        Scopes = {
          new string("api1")
        }
      }
    };

把作用域給加上,然后我們再運行一下 VisitorClient控制台程序。

 

大功告成 !!

我們成功訪問了被保護的api並輸出了  User.Claims 的信息。


總結

這篇文章 從授權的流程到代碼,完整的輸出了一個 客戶端模式的驗證案例,

代碼之后會完整的上傳 github,如果我忘記了 可以留言里提醒我一下,

那么就這么結束了嗎?並沒有,里面還有一寫概念,比如最終輸出的 User.Claims 是個啥,token是怎么獲取到的(我們現在只知道 配了一點東西,然后用了client的擴展方法)等等等。

我會再寫一篇 補充篇 在那里完善,敬請期待~(●'◡'●)

 如果覺得本篇隨筆對你有幫助,請點擊右下方的 【推薦👍】,或者給作者 【打賞】,感謝大家的支持,這將成為作者繼續寫作的動力。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM