寫在前面
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是直接在快取裡面取出來的
五、專案目錄