IdentityServer4 (1) 客戶端授權模式(Client Credentials)

朝閒發表於2020-08-03

寫在前面

1、原始碼(.Net Core 2.2)

  git地址:https://github.com/yizhaoxian/CoreIdentityServer4Demo.git

2、相關章節

  2.1、《IdentityServer4 (1) 客戶端授權模式(Client Credentials)
  2.2、《IdentityServer4 (2) 密碼授權(Resource Owner Password)
  2.3、《IdentityServer4 (3) 授權碼模式(Authorization Code)》
  2.4、《IdentityServer4 (4) 靜默重新整理(Implicit)》
  2.5、《IdentityServer4 (5) 混合模式(Hybrid)》

3、參考資料

  IdentityServer4 中文文件 http://www.identityserver.com.cn/
  IdentityServer4 英文文件 https://identityserver4.readthedocs.io/en/latest/

4、流程圖

  客戶端授權模式是最基本的使用場景,我們需要做一個API(受保護的資源),一個客戶端(訪問的應用),一個IdentityServer(用來授權)

  

一、建立IdentityServer

1、用VS建立一個Web 專案

  

2、新增引用 IdentityServer4 包,下圖是我已經安裝好了的截圖

3、新增一個配置檔案(這裡也可以使用json檔案)

    public class IdpConfig
    {
        /// <summary>
        /// 使用者認證資訊
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<IdentityResource> GetApiResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address(),
                new IdentityResources.Email(),
                new IdentityResources.Phone()
            };
        }
        /// <summary>
        /// API 資源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApis()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API") 
            };
        }

        /// <summary>
        /// 客戶端應用
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    // 客戶端ID 這個很重要
                    ClientId = "client",
                    //AccessToken 過期時間,預設3600秒,注意這裡直接設定5秒過期是不管用的,解決方案繼續看下面 API資源新增JWT
                    //AccessTokenLifetime=5, 
                    // 沒有互動性使用者,使用 clientid/secret 實現認證。
                    AllowedGrantTypes = GrantTypes.ClientCredentials, 
                    // 用於認證的密碼
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    // 客戶端有權訪問的範圍(Scopes)
                    AllowedScopes = { "api1" }
                }
            };
        }
    }

4、在StartUp.cs 裡註冊 IdentityServer4 

  ConfigureServices()

    services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
    })
      .AddDeveloperSigningCredential()//解決Keyset is missing 錯誤
      //.AddTestUsers(TestUsers.Users)
      //.AddInMemoryIdentityResources(IdpConfig.GetApiResources())
      .AddInMemoryApiResources(IdpConfig.GetApis())
      .AddInMemoryClients(IdpConfig.GetClients());

  Configure()方法新增使用 IdentityServer4 中介軟體

app.UseIdentityServer();

5、配置完成

  啟動專案,訪問 http://localhost:5002/.well-known/openid-configuration (我的埠號是5002) ,可以瀏覽 發現文件,參考下圖,說明已經配置成功。

  後面客戶端會使用裡面的資料進行請求toke

  專案第一次啟動根目錄也會生成一個檔案 tempkey.rsa

   

二、客戶端

1、新建一個.Net Core Web 專案

  這裡可以使用其他建立客戶端 。例如:控制檯程式、wpf 等等。需要新增 NuGet 包 IdentityModel

  

2、新建一個 Controller 用來測試訪問上面的IdentityServer

  獲取token,訪問 http://localhost:5003/Idp/token ,提示訪問成功

    public class IdpController : Controller
    { 
        private static readonly string _idpBaseUrl = "http://localhost:5002"; 
        public async Task<IActionResult> Token()
        {
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync(_idpBaseUrl);
            if (disco.IsError)
            {
                return Content("獲取發現文件失敗。error:" + disco.Error);
            }
            var token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
            {
                Address = disco.TokenEndpoint,
                //ClientId、ClientSecret、Scope 這裡要和 API 裡定義的Client一模一樣
                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });
            if (token.IsError)
            {
                return Content("獲取 AccessToken 失敗。error:" + disco.Error);
            } 
            return Content("獲取 AccessToken 成功。Token:" + token.AccessToken);
        }  
    }

三、新增API資源

1、新建一個API專案

  我把API專案和IdentityServer 放到同一個解決方案,這個自己定,無所謂的

  API資源指的是IdentityServer IdpConfig.GetApis() 裡面新增的 api1(這個api1名稱隨便起,但是要注意一定要保持一致)

  新增認證之後就可以測試用 AccessToken 請求資源了

2、新增JWT 認證

  StartUp.ConfigureServices()

       services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
       .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
       {
           // IdentityServer 地址
           options.Authority = "http://localhost:5002";
           //不需要https
           options.RequireHttpsMetadata = false;
           //這裡要和 IdentityServer 定義的 api1 保持一致
           options.Audience = "api1";
           //token 預設容忍5分鐘過期時間偏移,這裡設定為0,
           //這裡就是為什麼定義客戶端設定了過期時間為5秒,過期後仍可以訪問資料
           options.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
           options.Events = new JwtBearerEvents
           {
               //AccessToken 驗證失敗
               OnChallenge = op =>
               {
                   //跳過所有預設操作
                   op.HandleResponse();
                   //下面是自定義返回訊息
                   //op.Response.Headers.Add("token", "401");
                   op.Response.ContentType = "application/json";
                   op.Response.StatusCode = StatusCodes.Status401Unauthorized;
                   op.Response.WriteAsync(JsonConvert.SerializeObject(new
                   {
                       status = StatusCodes.Status401Unauthorized,
                       msg = "token無效"
                   }));
                   return Task.CompletedTask;
               }
           };
       });

3、新增認證中介軟體

//這裡注意 一定要在 UseMvc前面,順序不可改變
app.UseAuthentication();

4、Controller 新增特性認證 [Authorize]

    [Route("api/[controller]")]
    [Authorize] 
    public class SuiBianController : Controller
    {
        [HttpGet]
        public string Get()
        {
            var roles = User.Claims.Where(l => l.Type == ClaimTypes.Role);
            return "訪問成功,當前使用者角色 " + string.Join(',', roles.Select(l => l.Value));
        }
    }

5、測試

  訪問 http://localhost:5001/api/suibian ,提示 token 無效,證明我們增加認證成功

  

四、客戶端測試

1、修改 IdpController, 新增一個action 訪問 API資源 /api/suibian

    public class IdpController : Controller
    {
        //記憶體快取 需要提前註冊  services.AddMemoryCache();
        private IMemoryCache _memoryCache;
        private static readonly string _idpBaseUrl = "http://localhost:5002";
        private static readonly string _apiBaseUrl = "http://localhost:5001";
        public IdpController(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }
        public async Task<IActionResult> Token()
        {
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync(_idpBaseUrl);
            if (disco.IsError)
            {
                return Content("獲取發現文件失敗。error:" + disco.Error);
            }
            var token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
            {
                Address = disco.TokenEndpoint,
                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });
            if (token.IsError)
            {
                return Content("獲取 AccessToken 失敗。error:" + disco.Error);
            }
            //將token 臨時儲存到 快取中
            _memoryCache.Set("AccessToken", token.AccessToken);
            return Content("獲取 AccessToken 成功。Token:" + token.AccessToken);
        }

        public async Task<IActionResult> SuiBian()
        {
            string token, apiurl = GetApiUrl("suibian");
            _memoryCache.TryGetValue("AccessToken", out token);
            if (string.IsNullOrEmpty(token))
            {
                return Content("token is null");
            }
            var client = new HttpClient();
            client.SetBearerToken(token);
            var response = await client.GetAsync(apiurl);
            var result = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                _memoryCache.Remove("AccessToken");
                return Content($"獲取 {apiurl} 失敗。StatusCode:{response.StatusCode} \r\n Token:{token} \r\n result:{result}");
            }
            return Json(new
            {
                code = response.StatusCode,
                data = result
            });
        }

        private string GetApiUrl(string address)
        {
            return _apiBaseUrl + "/api/" + address;
        }
    }

2、請求 AccessToken

  http://localhost:5003/Idp/token ,請求成功後會將 token 儲存到 cache 中

     

3、請求 API 資源

  http://localhost:5003/Idp/suibian ,token是直接在快取裡面取出來的

五、專案目錄

 

 

相關文章