Program.cs --> Main方法中:須要調用UseUrls設置IdentityServer4受權服務的IP地址html
1 var host = new WebHostBuilder() 2 .UseKestrel() 3 //IdentityServer4的使用須要配置UseUrls 4 .UseUrls("http://localhost:5114") 5 .UseContentRoot(Directory.GetCurrentDirectory()) 6 .UseIISIntegration() 7 .UseStartup<Startup>() 8 .Build();
Startup.cs -->ConfigureServices方法中的配置:數據庫
1 //RSA:證書長度2048以上,不然拋異常 2 //配置AccessToken的加密證書 3 var rsa = new RSACryptoServiceProvider(); 4 //從配置文件獲取加密證書 5 rsa.ImportCspBlob(Convert.FromBase64String(Configuration["SigningCredential"])); 6 //配置IdentityServer4 7 services.AddSingleton<IClientStore, MyClientStore>(); //注入IClientStore的實現,可用於運行時校驗Client 8 services.AddSingleton<IScopeStore, MyScopeStore>(); //注入IScopeStore的實現,可用於運行時校驗Scope 9 //注入IPersistedGrantStore的實現,用於存儲AuthorizationCode和RefreshToken等等,默認實現是存儲在內存中, 10 //若是服務重啓那麼這些數據就會被清空了,所以可實現IPersistedGrantStore將這些數據寫入到數據庫或者NoSql(Redis)中 11 services.AddSingleton<IPersistedGrantStore, MyPersistedGrantStore>(); 12 services.AddIdentityServer() 13 .AddSigningCredential(new RsaSecurityKey(rsa)); 14 //.AddTemporarySigningCredential() //生成臨時的加密證書,每次重啓服務都會從新生成 15 //.AddInMemoryScopes(Config.GetScopes()) //將Scopes設置到內存中 16 //.AddInMemoryClients(Config.GetClients()) //將Clients設置到內存中
Startup.cs --> Configure方法中的配置:json
1 //使用IdentityServer4 2 app.UseIdentityServer(); 3 //使用Cookie模塊 4 app.UseCookieAuthentication(new CookieAuthenticationOptions 5 { 6 AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme, 7 AutomaticAuthenticate = false, 8 AutomaticChallenge = false 9 });
方式一:api
.AddInMemoryClients(Config.GetClients()) //將Clients設置到內存中,IdentityServer4從中獲取進行驗證服務器
方式二(推薦):cookie
services.AddSingleton<IClientStore, MyClientStore>(); //注入IClientStore的實現,用於運行時獲取和校驗Clientapp
1 public class MyClientStore : IClientStore 2 { 3 readonly Dictionary<string, Client> _clients; 4 readonly IScopeStore _scopes; 5 public MyClientStore(IScopeStore scopes) 6 { 7 _scopes = scopes; 8 _clients = new Dictionary<string, Client>() 9 { 10 { 11 "auth_clientid", 12 new Client 13 { 14 ClientId = "auth_clientid", 15 ClientName = "AuthorizationCode Clientid", 16 AllowedGrantTypes = new string[] { GrantType.AuthorizationCode }, //容許AuthorizationCode模式 17 ClientSecrets = 18 { 19 new Secret("secret".Sha256()) 20 }, 21 RedirectUris = { "http://localhost:6321/Home/AuthCode" }, 22 PostLogoutRedirectUris = { "http://localhost:6321/" }, 23 //AccessTokenLifetime = 3600, //AccessToken過時時間, in seconds (defaults to 3600 seconds / 1 hour) 24 //AuthorizationCodeLifetime = 300, //設置AuthorizationCode的有效時間,in seconds (defaults to 300 seconds / 5 minutes) 25 //AbsoluteRefreshTokenLifetime = 2592000, //RefreshToken的最大過時時間,in seconds. Defaults to 2592000 seconds / 30 day 26 AllowedScopes = (from l in _scopes.GetEnabledScopesAsync(true).Result select l.Name).ToList(), 27 } 28 } 29 }; 30 } 31 32 public Task<Client> FindClientByIdAsync(string clientId) 33 { 34 Client client; 35 _clients.TryGetValue(clientId, out client); 36 return Task.FromResult(client); 37 } 38 }
方式一:async
.AddInMemoryScopes(Config.GetScopes()) //將Scopes設置到內存中,IdentityServer4從中獲取進行驗證ide
方式二(推薦):測試
services.AddSingleton<IScopeStore, MyScopeStore>(); //注入IScopeStore的實現,用於運行時獲取和校驗Scope
1 public class MyScopeStore : IScopeStore 2 { 3 readonly static Dictionary<string, Scope> _scopes = new Dictionary<string, Scope>() 4 { 5 { 6 "api1", 7 new Scope 8 { 9 Name = "api1", 10 DisplayName = "api1", 11 Description = "My API", 12 } 13 }, 14 { 15 //RefreshToken的Scope 16 StandardScopes.OfflineAccess.Name, 17 StandardScopes.OfflineAccess 18 }, 19 }; 20 21 public Task<IEnumerable<Scope>> FindScopesAsync(IEnumerable<string> scopeNames) 22 { 23 List<Scope> scopes = new List<Scope>(); 24 if (scopeNames != null) 25 { 26 Scope sc; 27 foreach (var sname in scopeNames) 28 { 29 if (_scopes.TryGetValue(sname, out sc)) 30 { 31 scopes.Add(sc); 32 } 33 else 34 { 35 break; 36 } 37 } 38 } 39 //返回值scopes不能爲null 40 return Task.FromResult<IEnumerable<Scope>>(scopes); 41 } 42 43 public Task<IEnumerable<Scope>> GetScopesAsync(bool publicOnly = true) 44 { 45 //publicOnly爲true:獲取public的scope;爲false:獲取全部的scope 46 //這裏不作區分 47 return Task.FromResult<IEnumerable<Scope>>(_scopes.Values); 48 } 49 }
資源服務器的配置在上一篇中已介紹(http://www.cnblogs.com/skig/p/6079457.html ),詳情也可參考源代碼。
AuthorizationCode模式的流程圖(來自:https://tools.ietf.org/html/rfc6749):
第三方客戶端頁面簡單實現:
點擊AccessToken按鈕進行訪問受權服務器,就是流程圖中步驟A:
1 //訪問受權服務器 2 return Redirect(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.AuthorizePath + "?" 3 + "response_type=code" 4 + "&client_id=" + OAuthConstants.Clientid 5 + "&redirect_uri=" + OAuthConstants.AuthorizeCodeCallBackPath 6 + "&scope=" + OAuthConstants.Scopes 7 + "&state=" + OAuthConstants.State);
受權服務器接收到請求後,會判斷用戶是否已經登錄,若是未登錄那麼跳轉到登錄頁面(若是已經登錄,登錄的一些相關信息會存儲在cookie中):
1 /// <summary> 2 /// 登錄頁面 3 /// </summary> 4 [HttpGet] 5 public async Task<IActionResult> Login(string returnUrl) 6 { 7 var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 8 var vm = BuildLoginViewModel(returnUrl, context); 9 return View(vm); 10 } 11 12 /// <summary> 13 /// 登錄帳號驗證 14 /// </summary> 15 [HttpPost] 16 [ValidateAntiForgeryToken] 17 public async Task<IActionResult> Login(LoginInputModel model) 18 { 19 if (ModelState.IsValid) 20 { 21 //帳號密碼驗證 22 if (model.Username == "admin" && model.Password == "123456") 23 { 24 AuthenticationProperties props = null; 25 //判斷是否 記住登錄 26 if (model.RememberLogin) 27 { 28 props = new AuthenticationProperties 29 { 30 IsPersistent = true, 31 ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1) 32 }; 33 }; 34 //參數一:Subject,可在資源服務器中獲取到,資源服務器經過User.Claims.Where(l => l.Type == "sub").FirstOrDefault();獲取 35 //參數二:帳號 36 await HttpContext.Authentication.SignInAsync("admin", "admin", props); 37 //驗證ReturnUrl,ReturnUrl爲重定向到受權頁面 38 if (_interaction.IsValidReturnUrl(model.ReturnUrl)) 39 { 40 return Redirect(model.ReturnUrl); 41 } 42 return Redirect("~/"); 43 } 44 ModelState.AddModelError("", "Invalid username or password."); 45 } 46 //生成錯誤信息的LoginViewModel 47 var vm = await BuildLoginViewModelAsync(model); 48 return View(vm); 49 }
登錄成功後,重定向到受權頁面,詢問用戶是否受權,就是流程圖的步驟B了:
1 /// <summary> 2 /// 顯示用戶可授予的權限 3 /// </summary> 4 /// <param name="returnUrl"></param> 5 /// <returns></returns> 6 [HttpGet] 7 public async Task<IActionResult> Index(string returnUrl) 8 { 9 var vm = await BuildViewModelAsync(returnUrl); 10 if (vm != null) 11 { 12 return View("Index", vm); 13 } 14 15 return View("Error", new ErrorViewModel 16 { 17 Error = new ErrorMessage { Error = "Invalid Request" }, 18 }); 19 }
受權成功,重定向到redirect_uri(步驟A傳遞的)所指定的地址(第三方端),而且會把Authorization Code也設置到url的參數code中:
1 /// <summary> 2 /// 用戶受權驗證 3 /// </summary> 4 [HttpPost] 5 [ValidateAntiForgeryToken] 6 public async Task<IActionResult> Index(ConsentInputModel model) 7 { 8 //解析returnUrl 9 var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 10 if (request != null && model != null) 11 { 12 if (ModelState.IsValid) 13 { 14 ConsentResponse response = null; 15 //用戶不一樣意受權 16 if (model.Button == "no") 17 { 18 response = ConsentResponse.Denied; 19 } 20 //用戶贊成受權 21 else if (model.Button == "yes") 22 { 23 //設置已選擇受權的Scopes 24 if (model.ScopesConsented != null && model.ScopesConsented.Any()) 25 { 26 response = new ConsentResponse 27 { 28 RememberConsent = model.RememberConsent, 29 ScopesConsented = model.ScopesConsented 30 }; 31 } 32 else 33 { 34 ModelState.AddModelError("", "You must pick at least one permission."); 35 } 36 } 37 else 38 { 39 ModelState.AddModelError("", "Invalid Selection"); 40 } 41 if (response != null) 42 { 43 //將受權的結果設置到identityserver中 44 await _interaction.GrantConsentAsync(request, response); 45 //受權成功重定向 46 return Redirect(model.ReturnUrl); 47 } 48 } 49 //有錯誤,從新受權 50 var vm = await BuildViewModelAsync(model.ReturnUrl, model); 51 if (vm != null) 52 { 53 return View(vm); 54 } 55 } 56 return View("Error", new ErrorViewModel 57 { 58 Error = new ErrorMessage { Error = "Invalid Request" }, 59 }); 60 }
受權成功後重定向到指定的第三方端(步驟A所指定的redirect_uri),而後這個重定向的地址中去實現獲取AccessToken(就是由第三方端實現):
1 public IActionResult AuthCode(AuthCodeModel model) 2 { 3 GrantClientViewModel vmodel = new GrantClientViewModel(); 4 if (model.state == OAuthConstants.State) 5 { 6 //經過Authorization Code獲取AccessToken 7 var client = new HttpClientHepler(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.TokenPath); 8 client.PostAsync(null, 9 "grant_type=" + "authorization_code" + 10 "&code=" + model.code + //Authorization Code 11 "&redirect_uri=" + OAuthConstants.AuthorizeCodeCallBackPath + 12 "&client_id=" + OAuthConstants.Clientid + 13 "&client_secret=" + OAuthConstants.Secret, 14 hd => hd.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded"), 15 rtnVal => 16 { 17 var jsonVal = JsonConvert.DeserializeObject<dynamic>(rtnVal); 18 vmodel.AccessToken = jsonVal.access_token; 19 vmodel.RefreshToken = jsonVal.refresh_token; 20 }, 21 fault => _logger.LogError("Get AccessToken Error: " + fault.ReasonPhrase), 22 ex => _logger.LogError("Get AccessToken Error: " + ex)).Wait(); 23 } 24 25 return Redirect("~/Home/Index?" 26 + nameof(vmodel.AccessToken) + "=" + vmodel.AccessToken + "&" 27 + nameof(vmodel.RefreshToken) + "=" + vmodel.RefreshToken); 28 }
受權服務器對步驟D請求傳遞的Authorization Code進行驗證,驗證成功生成AccessToken並返回:
其中,點擊RefreshToken進行刷新AccessToken:
1 //刷新AccessToken 2 var client = new HttpClientHepler(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.TokenPath); 3 client.PostAsync(null, 4 "grant_type=" + "refresh_token" + 5 "&client_id=" + OAuthConstants.Clientid + 6 "&client_secret=" + OAuthConstants.Secret + 7 "&refresh_token=" + model.RefreshToken, 8 hd => hd.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded"), 9 rtnVal => 10 { 11 var jsonVal = JsonConvert.DeserializeObject<dynamic>(rtnVal); 12 vmodel.AccessToken = jsonVal.access_token; 13 vmodel.RefreshToken = jsonVal.refresh_token; 14 }, 15 fault => _logger.LogError("RefreshToken Error: " + fault.ReasonPhrase), 16 ex => _logger.LogError("RefreshToken Error: " + ex)).Wait();
點擊CallResources訪問資源服務器:
1 //訪問資源服務 2 var client = new HttpClientHepler(OAuthConstants.ResourceServerBaseAddress + OAuthConstants.ResourcesPath); 3 client.GetAsync(null, 4 hd => hd.Add("Authorization", "Bearer " + model.AccessToken), 5 rtnVal => vmodel.Resources = rtnVal, 6 fault => _logger.LogError("CallResources Error: " + fault.ReasonPhrase), 7 ex => _logger.LogError("CallResources Error: " + ex)).Wait();
點擊Logout爲註銷登錄:
1 //訪問受權服務器,註銷登錄 2 return Redirect(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.LogoutPath + "?" 3 + "logoutId=" + OAuthConstants.Clientid);
受權服務器的註銷實現代碼:
1 /// <summary> 2 /// 註銷登錄頁面(由於帳號的一些相關信息會存儲在cookie中的) 3 /// </summary> 4 [HttpGet] 5 public async Task<IActionResult> Logout(string logoutId) 6 { 7 if (User.Identity.IsAuthenticated == false) 8 { 9 //若是用戶並未受權過,那麼返回 10 return await Logout(new LogoutViewModel { LogoutId = logoutId }); 11 } 12 //顯示註銷提示, 這能夠防止攻擊, 若是用戶簽署了另外一個惡意網頁 13 var vm = new LogoutViewModel 14 { 15 LogoutId = logoutId 16 }; 17 return View(vm); 18 } 19 20 /// <summary> 21 /// 處理註銷登錄 22 /// </summary> 23 [HttpPost] 24 [ValidateAntiForgeryToken] 25 public async Task<IActionResult> Logout(LogoutViewModel model) 26 { 27 //清除Cookie中的受權信息 28 await HttpContext.Authentication.SignOutAsync(); 29 //設置User使之呈現爲匿名用戶 30 HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); 31 Client logout = null; 32 if (model != null && !string.IsNullOrEmpty(model.LogoutId)) 33 { 34 //獲取Logout的相關信息 35 logout = await _clientStore.FindClientByIdAsync(model.LogoutId); 36 } 37 var vm = new LoggedOutViewModel 38 { 39 PostLogoutRedirectUri = logout?.PostLogoutRedirectUris?.FirstOrDefault(), 40 ClientName = logout?.ClientName, 41 }; 42 return View("LoggedOut", vm); 43 }