身份驗證、授權
什麼是身份認證
身份認證是指當客戶端訪問服務端資源時,驗證客戶端是否合法的一種機制
什麼是授權
授權就是指當客戶端經過身份認證後,能夠有限的訪問服務端資源的一種機制
爲什麼要使用身份驗證和授權
爲了保證服務端資源的安全,我們要理解必須從真實項目中去理解
身份認證和授權方式有哪些
1、Base認證
Base64編號認證 == https
2、Digest認證
MD5消息摘要認證 == https
3、Bearer認證
就是基於token(電子身份證)認證,講用戶信息等其他信息轉換成爲token,然後以token進行認證
token認證是一種無狀態的認證方式,能夠無限擴展,特別適合做單點登錄
3.1 OAuth 2.0 ==== 授權方式的認證
3.2 JWT的也是一種身份認證
token 分兩種類型
引用類型token == OAuth 2.0
無用戶相關信息
自包含token
有用戶相關信息 JWT === 地址,電話,id,等
在實際分佈式項目中,大部分都是使用Bearer來進行身份認證和身份授權
在分佈式項目或者微服務項目中,爲了保證系統統一登錄(SSO登錄),
使用OpenID協議標準來規範身份認證功能 === 規範
同時使用OAuth 2.0協議標準來規範權限訪問 === 規範
爲了將身份認證(單點登錄)和授權結合起來,所以出現了OpenID Connect協議標準 === 接口
OpenID Connect = OpenID + OAuth 2.0
SSO+OAuth 2.0可以實現單點登錄和授權
IdentityServer4 也可以實現單點登錄和授權
然後綜合實現了這些框架就是今天要講的IdentityServer4 身份認證服務器
IdentityServer4 是什麼
IdentityServer是基於OpenID Connect協議標準的身份認證和授權程序,它實現了OpenID Connect 和 OAuth 2.0 協議。
這裏只記錄了簡單使用,深入瞭解可以去官網地址
IdentityServer4 官網地址
中文地址:http://www.identityserver.com.cn/Home/Detail/Zhengtizongshu
英文地址:https://identityserver4.readthedocs.io/en/3.1.0/
IdentityServer4 功能
保護你的資源
使用本地帳戶或通過外部身份提供程序對用戶進行身份驗證
提供會話管理和單點登錄
管理和驗證客戶機
向客戶發出標識和訪問令牌
驗證令牌
IdentityServer4 內部概念
**用戶(User)** 用戶是使用已註冊的客戶端(指在id4中已經註冊)訪問資源的人。
**客戶端(Client)** 客戶端就是從identityserver請求令牌的軟件(你可以理解爲一個app即可),既可以通過身份認證令牌來驗證識別用戶身份,又可以通過授權令牌來訪問服務端的資源。但是客戶端首先必須在申請令牌前已經在identityserver服務中註冊過。 實際客戶端不僅可以是Web應用程序,app或桌面應用程序(你就理解爲pc端的軟件即可),SPA,服務器進程等。 **資源(Resources)** 資源就是你想用identityserver保護的東東,可以是用戶的身份數據或者api資源。 每一個資源都有一個唯一的名稱,客戶端使用這個唯一的名稱來確定想訪問哪一個資源(在訪問之前,實際identityserver服務端已經配置好了哪個客戶端可以訪問哪個資源,所以你不必理解爲客戶端只要指定名稱他們就可以隨便訪問任何一個資源)。 用戶的身份信息實際由一組claim組成,例如姓名或者郵件都會包含在身份信息中(將來通過identityserver校驗後都會返回給被調用的客戶端)。 API資源就是客戶端想要調用的功能(通常以json或xml的格式返回給客戶端,例如webapi,wcf,webservice),通常通過webapi來建立模型,但是不一定是webapi,我剛纔已經強調可以使其他類型的格式,這個要看具體的使用場景了。 **身份令牌(顧名思義用於做身份認證,例如sso其實主要就是用於身份認證)** id_token jwt 一個身份令牌指的就是對認證過程的描述。它至少要標識某個用戶(Called the sub aka subject claim)的主身份信息,和該用戶的認證時間和認證方式。但是身份令牌可以包含額外的身份數據,具體開發者可以自行設定,但是一般情況爲了確保數據傳輸的效率,開發者一般不做過多額外的設置,大家也可以根據使用場景自行決定。 **訪問令牌(用於做客戶端訪問授權)** access_token oauth 2.0 訪問令牌允許客戶端訪問某個 API 資源。客戶端請求到訪問令牌,然後使用這個令牌來訪問 API資源。訪問令牌包含了客戶端和用戶(如果有的話,這取決於業務是否需要,但通常不必要)的相關信息,API通過這些令牌信息來授予客戶端的數據訪問權限。
項目實例
創建認證服務端
1. 新建一個MVC項目,安裝nuget包
IdentityServer4
2. 創建配置中心類 MemoryConfigs.cs
public class MemoryConfigs { public static List<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResource{ Name="openid", Enabled=true, Emphasize=true, Required=true, DisplayName="用戶授權認證信息", Description="獲取你的授權認證" }, new IdentityResource{ Name="profile", Enabled=true, Emphasize=false, Required=true, DisplayName="用戶個人信息", Description="獲取你的個人基本資料信息,如:姓名、性別、年齡等" } }; } public static List<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource{ Name="userapi", DisplayName="用戶服務", Description="用戶服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("userapi") } }, new ApiResource{ Name="signalrapi", DisplayName="WebSocket服務", Description="WebSocket服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("signalrapi") } }, new ApiResource{ Name="fileapi", DisplayName="文件服務", Description="文件服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("fileapi") } }, new ApiResource{ Name="physicalexamapi", DisplayName="體檢服務", Description="體檢服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("physicalexamapi") } }, new ApiResource{ Name="printapi", DisplayName="打印服務", Description="打印服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("printapi") } }, new ApiResource{ Name="reportapi", DisplayName="報表服務", Description="報表服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("reportapi") } }, new ApiResource{ Name="vaccineapi", DisplayName="疫苗服務", Description="疫苗服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("vaccineapi") } }, new ApiResource{ Name="authenticationapi", DisplayName="認證管理服務", Description="認證管理服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("authenticationapi") } }, new ApiResource{ Name="configapi", DisplayName="配置中心服務", Description="配置中心服務", ApiSecrets={ new Secret("testcode".Sha256()) }, Scopes={ new Scope("configapi") } } }; } public static List<Client> GetClients() { return new List<Client> { new Client(){ ClientId="clientcode", ClientName="C/S客戶端", ClientSecrets={ new Secret("clientcode".Sha256()) }, AllowedGrantTypes= GrantTypes.ClientCredentials, AccessTokenType= AccessTokenType.Reference, AllowedScopes={ "signalrapi" } }, new Client(){ ClientId="fileclient", ClientName="C/S客戶端", ClientSecrets={ new Secret("fileclient".Sha256()) }, AllowedGrantTypes= GrantTypes.ClientCredentials, AccessTokenType= AccessTokenType.Reference, AllowedScopes={ "fileapi" } }, new Client(){ ClientId="configclient", ClientName="C/S客戶端", ClientSecrets={ new Secret("configclient".Sha256()) }, AllowedGrantTypes= GrantTypes.ClientCredentials, AccessTokenType= AccessTokenType.Reference, AllowedScopes={ "configapi" } }, new Client(){ ClientId="testcode", ClientName="測試授權碼", ClientSecrets={ new Secret("testcode".Sha256()) }, AllowedGrantTypes= GrantTypes.Code, AccessTokenType= AccessTokenType.Reference, RequireConsent=false, RequirePkce=true, AllowOfflineAccess=true, AllowAccessTokensViaBrowser=true, // IdToken過期時間 客戶端可以設置UseTokenLifetime = true將客戶端過期時間設置爲IdToken過期時間 IdentityTokenLifetime=60*60*2, RedirectUris={ "http://localhost:2001/signin-oidc"}, FrontChannelLogoutUri= "http://localhost:2001/" , PostLogoutRedirectUris = { "http://localhost:2001/signout-callback-oidc" }, AllowedScopes={ "openid", "profile", "userapi", "signalrapi", "fileapi", "printapi", "reportapi", "vaccineapi", "authenticationapi" } }, new Client(){ ClientId="appclient", ClientName="App客戶端", ClientSecrets={ new Secret("appclient".Sha256()) }, AllowedGrantTypes= GrantTypes.ResourceOwnerPassword, AccessTokenType= AccessTokenType.Reference, RequireConsent=false, RequirePkce=true, AllowOfflineAccess=true, AllowAccessTokensViaBrowser=true, AllowedScopes={ "openid", "profile", "vaccineapi", "offline_access", } }, new Client(){ ClientId="testcodeapi", ClientName="API授權", ClientSecrets={ new Secret("testcodeapi".Sha256()) }, AllowedGrantTypes= GrantTypes.Implicit, AccessTokenType= AccessTokenType.Reference, RequireConsent=false, AllowAccessTokensViaBrowser=true, AlwaysIncludeUserClaimsInIdToken=true, RedirectUris={ "http://localhost:3002/swagger/oauth2-redirect.html", "http://localhost:3004/swagger/oauth2-redirect.html", "http://localhost:3005/swagger/oauth2-redirect.html", "http://localhost:3006/swagger/oauth2-redirect.html", "http://localhost:3007/swagger/oauth2-redirect.html", "http://localhost:4000/swagger/oauth2-redirect.html", "http://localhost:2001/signin-oidc" }, AllowedScopes={ //必需的。通知授權服務器客戶機正在發出一個OpenID連接請求。如果openid作用域值不存在,則行爲完全沒有指定。 IdentityServerConstants.StandardScopes.OpenId, //可選的。這個作用域值請求訪問終端用戶的默認配置文件聲明,它們是:name、family_name、given_name、middle_name、暱稱、preferred_username、profile、picture、website、性別、生日、zoneinfo、locale和updated_at。 IdentityServerConstants.StandardScopes.Profile, "userapi", "signalrapi", "fileapi", "physicalexamapi", "printapi", "reportapi", "vaccineapi", "authenticationapi" } }, new Client { ClientId = "mvc", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Implicit, RequireConsent=false, //登錄後重定向到哪裏 RedirectUris = { "http://localhost:2001/signin-oidc" }, //註銷後重定向到哪裏 PostLogoutRedirectUris = { "http://localhost:2001/signout-callback-oidc" }, AllowedScopes = new List<string> { //必需的。通知授權服務器客戶機正在發出一個OpenID連接請求。如果openid作用域值不存在,則行爲完全沒有指定。 IdentityServerConstants.StandardScopes.OpenId, //可選的。這個作用域值請求訪問終端用戶的默認配置文件聲明,它們是:name、family_name、given_name、middle_name、暱稱、preferred_username、profile、picture、website、性別、生日、zoneinfo、locale和updated_at。 IdentityServerConstants.StandardScopes.Profile } } }; } //添加用戶(密碼授權模式) public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { SubjectId = "1", Username = "Darcy", Password = "123", Claims = new List<Claim> //可自定義存入Claim中,將一起添加到身份令牌中 { new Claim("name", "Darcy"), new Claim("website", "https://Darcy.com"), new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServerConstants.ClaimValueTypes.Json) } }, new TestUser { SubjectId = "2", Username = "Larry", Password = "123", Claims = new List<Claim> { new Claim("name", "Larry"), new Claim("website", "https://Larry.com"), new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServerConstants.ClaimValueTypes.Json), new Claim("location", "somewhere") } } }; } }
3. 在ConfigureServices中注入服務
#region 通過配置使用idr4,測試時使用 services.AddIdentityServer() //依賴注入identityserver .AddDeveloperSigningCredential() //擴展在每次啓動時,爲令牌簽名創建了一個臨時密鑰 .AddInMemoryIdentityResources(MemoryConfigs.GetIdentityResources())//添加身份資源 // 使用內存存儲的密鑰,客戶端和API資源來配置ids4。 .AddInMemoryApiResources(MemoryConfigs.GetApiResources()) //添加api資源 .AddInMemoryClients(MemoryConfigs.GetClients()) //添加客戶端 .AddTestUsers(MemoryConfigs.GetUsers()); //添加測試用戶 #endregion
4. 在Configure中使用
app.UseIdentityServer(); app.UseAuthorization();
5. 創建account控制器及相關登錄接口
/// <summary> /// 這個示例控制器爲本地和外部帳戶實現了一個典型的登錄/註銷/提供工作流。 /// 登錄服務封裝了與用戶數據存儲的交互。此數據存儲僅在內存中,不能用於生產! /// 交互服務爲UI提供了一種與identityserver通信的方式,用於驗證和上下文檢索 /// </summary> public class AccountController : Controller { private readonly TestUserStore _users; private readonly IIdentityServerInteractionService _interaction; private readonly IClientStore _clientStore; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IEventService _events; public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IAuthenticationSchemeProvider schemeProvider, IEventService events, TestUserStore users = null) { //如果TestUserStore不在DI中,那麼我們將只使用全局用戶集合 //在這裏,您可以插入自己的自定義身份管理庫(例如, ASP.NET Identity) _users = users ?? new TestUserStore(MemoryConfigs.GetUsers()); _interaction = interaction; _clientStore = clientStore; _schemeProvider = schemeProvider; _events = events; } /// <summary> /// 展示登錄頁 /// </summary> [HttpGet] public async Task<IActionResult> Login(string backurl) { // 構建一個模型,以便我們知道在登錄頁面上顯示什麼 var vm = await BuildLoginViewModelAsync(backurl); if (vm.IsExternalLoginOnly) { //我們只有一個登錄選項,它是一個外部提供者 return await ExternalLogin(vm.ExternalLoginScheme, backurl); } return View(vm); } public async Task<IActionResult> Login(LoginInputModel model, string button) { if (button != "login") { //用戶點擊“取消”按鈕 var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); if (context != null) { //如果用戶取消,則將結果發送回IdentityServer //拒絕同意(即使該客戶不需要同意)。 //這將向客戶端發送一個訪問被拒絕的OIDC錯誤響應。 await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); //我們可以信任模型。因爲GetAuthorizationContextAsync返回非空 return Redirect(model.ReturnUrl); } else { //因爲我們沒有一個有效的上下文,所以我們只能返回主頁 return Redirect("~/"); } } if (ModelState.IsValid) { //在內存存儲中驗證用戶名/密碼 if (_users.ValidateCredentials(model.Username, model.Password)) { var user = _users.FindByUsername(model.Username); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); //只有當用戶選擇“記住我”時才設置顯式過期。 //否則,我們依賴於在cookie中間件中配置的過期。 AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; //List<Claim> customClaims = new List<Claim>(); //customClaims.Add(new Claim("custom", "自定義信息")); //使用主題ID和用戶名發出身份驗證cookie await HttpContext.SignInAsync(user.SubjectId, user.Username, props,new Claim("custom", "自定義Claim")); //確保returnUrl仍然有效,如果有效,則重定向回授權端點或本地頁 //只有在需要支持其他本地頁面時才需要進行IsLocalUrl檢查,否則IsValidReturnUrl將更加嚴格 if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } //出了問題,顯示錯誤的形式 var vm = await BuildLoginViewModelAsync(model); return View(vm); } /// <summary> /// 啓動到外部身份驗證提供者的往返 /// </summary> [HttpGet] public async Task<IActionResult> ExternalLogin(string provider, string returnUrl) { if (AccountOptions.WindowsAuthenticationSchemeName == provider) { // windows身份驗證需要特殊處理 return await ProcessWindowsLoginAsync(returnUrl); } else { //開始挑戰和往返返回的URL和 var props = new AuthenticationProperties() { RedirectUri = Url.Action("ExternalLoginCallback"), Items = { { "returnUrl", returnUrl }, { "scheme", provider }, } }; return Challenge(props, provider); } } /// <summary> /// 外部認證的後處理 /// </summary> [HttpGet] public async Task<IActionResult> ExternalLoginCallback() { //從臨時cookie讀取外部標識 var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); if (result?.Succeeded != true) { throw new Exception("External authentication error"); } //查找我們的用戶和外部提供商信息 var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); if (user == null) { //這可能是您啓動用戶註冊的自定義工作流的地方 //在這個示例中,作爲示例實現,我們沒有展示如何實現 //簡單地自動提供新的外部用戶 user = AutoProvisionUser(provider, providerUserId, claims); } //這允許我們收集任何附加的聲明或屬性 //用於特定的prtotocols,並將其存儲在本地的auth cookie中。 //這通常用於存儲從這些協議簽出所需的數據。 var additionalLocalClaims = new List<Claim>(); var localSignInProps = new AuthenticationProperties(); ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps); ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps); ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps); //爲用戶發出認證cookie await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray()); //刪除外部身份驗證期間使用的臨時cookie await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); //驗證返回的URL並將其重定向回授權端點或本地頁面 var returnUrl = result.Properties.Items["returnUrl"]; if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return Redirect("~/"); } /// <summary> /// 顯示註銷頁 /// </summary> [HttpGet] public async Task<IActionResult> Logout(string logoutId) { //建立一個模型,以便註銷頁面知道顯示什麼 var vm = await BuildLogoutViewModelAsync(logoutId); if (vm.ShowLogoutPrompt == false) { //如果從IdentityServer正確驗證了註銷請求,那麼 //我們不需要顯示提示,可以直接將用戶登出即可。 return await Logout(vm); } return View(vm); } /// <summary> /// 處理註銷頁面回發 /// </summary> [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Logout(LogoutInputModel model) { // 建立一個模型,以便註銷頁面知道顯示什麼 var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); if (User?.Identity.IsAuthenticated == true) { //刪除本地身份驗證cookie await HttpContext.SignOutAsync(); //引發註銷事件 await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); } //檢查是否需要在上游身份提供商觸發註銷 if (vm.TriggerExternalSignout) { //構建一個返回URL,以便上游提供者將重定向回來 //在用戶登出後發送給我們。這樣我們就可以 //完成我們的單次簽名處理。 string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); //這會觸發重定向到外部提供者以便註銷 return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); } return View("LoggedOut", vm); } /// <summary> /// 刷新Token /// </summary> /// <param name="refreshToken"></param> /// <returns></returns> [HttpGet] public async Task<IActionResult> RefreshToken(string refreshToken) { HttpClient client2 = new HttpClient(); DiscoveryDocumentResponse disco2 = await client2.GetDiscoveryDocumentAsync("http://localhost:2000"); if (disco2.IsError) { Console.WriteLine($"[DiscoveryDocumentResponse Error]: {disco2.Error}"); } //// 1.1、通過客戶端獲取AccessToken //TokenResponse tokenResponse2 = await client2.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest //{ // Address = disco2.TokenEndpoint, // 1、生成AccessToken中心 // ClientId = "clientcode", // 2、客戶端編號 // ClientSecret = "clientcode",// 3、客戶端密碼 // Scope = "signalrapi" // 4、客戶端需要訪問的API //}); TokenResponse tokenResponse3 = await client2.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = disco2.TokenEndpoint, RefreshToken = refreshToken, ClientId = "appclient", // 2、客戶端編號 ClientSecret = "appclient",// 3、客戶端密碼 Scope = "openid profile offline_access signalrapi", //Scope = "signalrapi", }); return new JsonResult(JsonConvert.SerializeObject(tokenResponse3)); } /*****************************************/ /* AccountController API幫助類 */ /*****************************************/ private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl) { var context = await _interaction.GetAuthorizationContextAsync(returnUrl); if (context?.IdP != null) { // 這意味着將UI短路,只觸發一個外部IdP return new LoginViewModel { EnableLocalLogin = false, ReturnUrl = returnUrl, Username = context?.LoginHint, ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } } }; } var schemes = await _schemeProvider.GetAllSchemesAsync(); var providers = schemes .Where(x => x.DisplayName != null || (x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) ) .Select(x => new ExternalProvider { DisplayName = x.DisplayName, AuthenticationScheme = x.Name }).ToList(); var allowLocal = true; if (context?.ClientId != null) { var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); if (client != null) { allowLocal = client.EnableLocalLogin; if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) { providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); } } } return new LoginViewModel { AllowRememberLogin = AccountOptions.AllowRememberLogin, EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, ReturnUrl = returnUrl, Username = context?.LoginHint, ExternalProviders = providers.ToArray() }; } private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model) { var vm = await BuildLoginViewModelAsync(model.ReturnUrl); vm.Username = model.Username; vm.RememberLogin = model.RememberLogin; return vm; } private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId) { var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; if (User?.Identity.IsAuthenticated != true) { //如果用戶沒有經過身份驗證,那麼只顯示註銷頁面 vm.ShowLogoutPrompt = false; return vm; } var context = await _interaction.GetLogoutContextAsync(logoutId); if (context?.ShowSignoutPrompt == false) { //自動退出是安全的 vm.ShowLogoutPrompt = false; return vm; } //顯示註銷提示。這可以防止用戶攻擊 //被另一個惡意網頁自動註銷。 return vm; } private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId) { // 獲取上下文信息(用於聯合註銷的客戶端名稱、post logout重定向URI和iframe) var logout = await _interaction.GetLogoutContextAsync(logoutId); var vm = new LoggedOutViewModel { AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, SignOutIframeUrl = logout?.SignOutIFrameUrl, LogoutId = logoutId }; if (User?.Identity.IsAuthenticated == true) { var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider) { var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp); if (providerSupportsSignout) { if (vm.LogoutId == null) { //如果當前沒有註銷上下文,我們需要創建一個 //從當前登錄的用戶獲取必要的信息 //在我們註銷和重定向到外部IdP進行註銷之前 vm.LogoutId = await _interaction.CreateLogoutContextAsync(); } vm.ExternalAuthenticationScheme = idp; } } } return vm; } private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl) { // 看看windows auth是否已經被請求成功 var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName); if (result?.Principal is WindowsPrincipal wp) { //我們將發出外部cookie,然後重定向 //用戶返回到外部回調,本質上是tresting windows // auth與任何其他外部身份驗證機制相同 var props = new AuthenticationProperties() { RedirectUri = Url.Action("ExternalLoginCallback"), Items = { { "returnUrl", returnUrl }, { "scheme", AccountOptions.WindowsAuthenticationSchemeName }, } }; var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName); id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); // 添加組作爲聲明——如果組的數量太大,請小心 if (AccountOptions.IncludeWindowsGroups) { var wi = wp.Identity as WindowsIdentity; var groups = wi.Groups.Translate(typeof(NTAccount)); var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); id.AddClaims(roles); } await HttpContext.SignInAsync( IdentityServerConstants.ExternalCookieAuthenticationScheme, new ClaimsPrincipal(id), props); return Redirect(props.RedirectUri); } else { //觸發窗口 //由於windows auth不支持重定向uri, //當我們調用challenge時,這個URL被重新觸發 return Challenge(AccountOptions.WindowsAuthenticationSchemeName); } } private (TestUser user, string provider, string providerUserId, IEnumerable<Claim> claims) FindUserFromExternalProvider(AuthenticateResult result) { var externalUser = result.Principal; //嘗試確定外部用戶的唯一id(由提供者發出) //最常見的索賠類型是子索賠和名稱標識符 //根據外部提供者的不同,可能會使用其他一些索賠類型 var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? throw new Exception("Unknown userid"); //刪除用戶id聲明,這樣我們在提供用戶時就不會將其作爲額外的聲明 var claims = externalUser.Claims.ToList(); claims.Remove(userIdClaim); var provider = result.Properties.Items["scheme"]; var providerUserId = userIdClaim.Value; //尋找外部用戶 var user = _users.FindByExternalProvider(provider, providerUserId); return (user, provider, providerUserId, claims); } private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims) { var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); return user; } private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps) { //如果外部系統發送了會話id聲明,請複製它 //這樣我們就可以用它來做單點簽到了 var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); if (sid != null) { localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); } //如果外部提供者發出id_token,我們將保留它以便註銷 var id_token = externalResult.Properties.GetTokenValue("id_token"); if (id_token != null) { localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); } } private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps) { } private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps) { } }
6. 創建登錄相關的model
public class AccountOptions { public static bool AllowLocalLogin = true; public static bool AllowRememberLogin = true; public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); public static bool ShowLogoutPrompt = true; public static bool AutomaticRedirectAfterSignOut = false; // 指定正在使用的Windows身份驗證方案 public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme; // 如果用戶使用windows auth,是否應該從windows加載組 public static bool IncludeWindowsGroups = false; public static string InvalidCredentialsErrorMessage = "Invalid username or password"; } public class ExternalProvider { public string DisplayName { get; set; } public string AuthenticationScheme { get; set; } } public class LoggedOutViewModel { public string PostLogoutRedirectUri { get; set; } public string ClientName { get; set; } public string SignOutIframeUrl { get; set; } public bool AutomaticRedirectAfterSignOut { get; set; } public string LogoutId { get; set; } public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; public string ExternalAuthenticationScheme { get; set; } } public class LoginInputModel { [Required] public string Username { get; set; } [Required] public string Password { get; set; } public bool RememberLogin { get; set; } public string ReturnUrl { get; set; } } public class LoginViewModel : LoginInputModel { public bool AllowRememberLogin { get; set; } public bool EnableLocalLogin { get; set; } public IEnumerable<ExternalProvider> ExternalProviders { get; set; } public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; } public class LogoutInputModel { public string LogoutId { get; set; } } public class LogoutViewModel : LogoutInputModel { public bool ShowLogoutPrompt { get; set; } }
完成了,直接運行訪問 /.well-known/openid-configuration 查看效果
創建客戶端
1. 新建一個MVC項目,安裝nuget包
IdentityServer4
2. 在ConfigureServices中注入服務
//刪除系統所有claims JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //將認證服務依賴注入容器中 services.AddAuthentication(options => { //使用cookie作爲驗證用戶的方法 options.DefaultScheme = "Cookies"; //當需要用戶登錄時,使用OpenID Connect方案 options.DefaultChallengeScheme = "oidc"; }) //添加可以處理Cookie的處理程序 .AddCookie("Cookies") //配置執行OpenID Connect協議的處理程序 .AddOpenIdConnect("oidc", options => { //在OpenID Connect協議完成後使用cookie處理程序發出cookie options.SignInScheme = "Cookies"; //id4地址 options.Authority = "http://localhost:2000"; //獲取或設置元數據地址或權限是否需要HTTPS,默認爲true,只能在開發環境設置 options.RequireHttpsMetadata = false; //用於識別客戶端 options.ClientId = "mvc";
//options.ClientSecret = "testcode"; //code授權碼模式才設置
//options.ResponseType = "code"; //code授權碼模式才設置
//用於在Cookie中保存IdentityServer中的令牌 options.SaveTokens = true; });
3. 在Configure中新增認證服務
app.UseAuthentication();
app.UseAuthorization();
4. 在客戶端中添加測試代碼
[Authorize] public IActionResult Userinfo() { ViewBag.Userinfo = User.Claims; return View(); }
運行結果
如果出現輸入賬號密碼後無法跳轉,並且出現這種錯誤 “idr4 The cookie 'idsrv.session' has set 'SameSite=None' and must also set 'S” 那就是因爲谷歌瀏覽器版本問題,需要手動設置 SameSite
在我們認證服務器中 ConfigureServices 手動設置服務
services.Configure<CookiePolicyOptions>(options => { options.MinimumSameSitePolicy = SameSiteMode.Lax; options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); }); private static void CheckSameSite(HttpContext httpContext, CookieOptions options) { if (options.SameSite == SameSiteMode.None) { if (options.SameSite == SameSiteMode.None) { if (httpContext.Request.Scheme != "https") { options.SameSite = SameSiteMode.Lax; } } } }
在Configure中添加中間件
app.UseCookiePolicy();
IdentityServer4 集成EF Core
1. 添加nuget包
Pomelo.EntityFrameworkCore.MySql
Microsoft.EntityFrameworkCore.Design
2. 在ConfigureServices中注入服務,在appsettings.json 配置數據庫連接字符串
"ConnectionStrings": "server=localhost;port=3306;user=root;password=hua3182486;database=fcb_idr4;SslMode=none;"
#region 通過EF Core 使用idr4, 實際開發中使用 services.AddIdentityServer(options => { //options.Authentication.CookieSlidingExpiration = true; options.IssuerUri = "http://localhost:2000"; //var publicOrgin = options.PublicOrigin = Configuration.GetSection("PublicOrigin").Value; //if (!string.IsNullOrEmpty(publicOrgin)) //{ //options.PublicOrigin = publicOrgin; //} IdentityModelEventSource.ShowPII = true; //設置用戶交互時路由 options.UserInteraction = new UserInteractionOptions { LoginUrl = "/account/login", LogoutUrl = "/account/logout", ConsentUrl = "/consent/index", ConsentReturnUrlParameter = "backurl", ErrorUrl = "/account/error", LoginReturnUrlParameter = "backurl", LogoutIdParameter = "logoutid", ErrorIdParameter = "errorid", CookieMessageThreshold = 5 }; options.Authentication.CookieLifetime = TimeSpan.FromDays(1); #region 事件設置 options.Events.RaiseErrorEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseSuccessEvents = true; #endregion }) .AddDeveloperSigningCredential() //.AddSigningCredential() .AddProfileService<CustomProfileService>() .AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>() //.AddSecretValidator<CustomSecretValidator>() .AddInMemoryCaching() .AddConfigurationStore(options => { options.ConfigureDbContext = builder => { builder.UseMySql(Configuration.GetSection("ConnectionStrings").Value, ServerVersion.AutoDetect(Configuration.GetSection("ConnectionStrings").Value), sqloption => { sqloption.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); sqloption.UseRelationalNulls(); }); }; }) .AddOperationalStore(options => { options.ConfigureDbContext = builder => builder.UseMySql(Configuration.GetSection("ConnectionStrings").Value, ServerVersion.AutoDetect(Configuration.GetSection("ConnectionStrings").Value), sqloption => { sqloption.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); sqloption.UseRelationalNulls(); }); options.EnableTokenCleanup = true; options.TokenCleanupInterval = 3600; }) ; #endregion
3. 生成 Migrations 文件
dotnet ef migrations add initdb -c ConfigurationDbContext -o Migrations/Configuration
dotnet ef migrations add initdb -c PersistedGrantDbContext -o Migrations/PersistedGrant
4. 加入初始化數據
//根據migration初始化數據庫 public static void UseInitIdr4Data(this IApplicationBuilder app) { using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { //初始化數據庫結構 scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate(); var configContext = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>(); configContext.Database.Migrate(); //初始數據 if (!configContext.Clients.Any()) { foreach (var client in MemoryConfigs.GetClients()) { configContext.Clients.Add(client.ToEntity()); } configContext.SaveChanges(); } if (!configContext.IdentityResources.Any()) { foreach (var resource in MemoryConfigs.GetIdentityResources()) { configContext.IdentityResources.Add(resource.ToEntity()); } configContext.SaveChanges(); } if (!configContext.ApiResources.Any()) { foreach (var resource in MemoryConfigs.GetApiResources()) { configContext.ApiResources.Add(resource.ToEntity()); } configContext.SaveChanges(); } } }
5. 在Configure 中使用
app.UseInitIdr4Data();
6. 添加一些自定義驗證,例如賬號密碼驗證等等 可以忽略
//自定義登錄擴展 //這裏是通過openid登錄 public class CustomExtensionValidator : IExtensionGrantValidator { public string GrantType => "password"; private readonly IHttpClientFactory _httpClientFactory; private readonly IEventService _eventService; public CustomExtensionValidator(IHttpClientFactory httpClientFactory, IEventService eventService) { _httpClientFactory = httpClientFactory; _eventService = eventService; } public async Task ValidateAsync(ExtensionGrantValidationContext context) { #region 正常的微信登錄示例 //var openid = context.Request.Raw["openid"]; //var clientapi = _httpClientFactory.CreateClient("你的服務名稱ServicesName"); //var result = await clientapi.PostAsJsonAsync("/api/oauth/wxlogin", new { openid = openid}); //if (result.IsSuccessStatusCode) //{ // var operatorResult = await result.Content.ReadAsStringAsync(); // var dataresult = JsonConvert.DeserializeObject<OperatorResult>(operatorResult); // if (dataresult.Result == 0) // { // var data = JsonConvert.DeserializeObject<UserInfo>(JsonConvert.SerializeObject(dataresult.Data)); // #region 登錄具體 // ClaimsIdentity claimsIdentity = new ClaimsIdentity("UserIdentity"); // claimsIdentity.AddClaim(new Claim("authcode", data.AuthCode + "")); // claimsIdentity.AddClaim(new Claim("username", data.UserName + "")); // claimsIdentity.AddClaim(new Claim("nickname", data.NickName + "")); // claimsIdentity.AddClaim(new Claim("oid", data.OId + "")); // claimsIdentity.AddClaim(new Claim("oname", data.OName + "")); // claimsIdentity.AddClaim(new Claim("usertype", data.UserType + "")); // context.Result = new GrantValidationResult(subject: data.Id.ToString(), authenticationMethod: "password", claims: claimsIdentity.Claims.ToArray()); // #endregion // } // else // { // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, dataresult.Message); // } //} //else //{ // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "登錄失敗"); //} #endregion await Task.CompletedTask; } }
public class CustomProfileService : IProfileService { public async Task GetProfileDataAsync(ProfileDataRequestContext context) { context.IssuedClaims = context.Subject.Claims.ToList(); await Task.CompletedTask; } public async Task IsActiveAsync(IsActiveContext context) { await Task.CompletedTask; } }
/// <summary> /// 手機 賬號密碼登錄擴展 /// </summary> public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly IHttpClientFactory _httpClientFactory; private readonly IEventService _eventService; public CustomResourceOwnerPasswordValidator(IHttpClientFactory httpClientFactory, IEventService eventService) { _httpClientFactory = httpClientFactory; _eventService = eventService; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { #region 自定義賬號密碼登錄示例 //string username = context.UserName; //string userpwd = context.Password; //var clientapi = _httpClientFactory.CreateClient(ServicesUrl.UserServicesName); //var wxtype = context.Request.Raw["wxtype"]; //if (string.IsNullOrEmpty(wxtype)) //{ // #region 賬戶密碼登錄 // var result = await clientapi.PostAsJsonAsync("/api/oauth/login", new { username = username, userpwd = userpwd }); // if (result.IsSuccessStatusCode) // { // var operatorResult = await result.Content.ReadAsStringAsync(); // var dataresult = JsonConvert.DeserializeObject<OperatorResult>(operatorResult); // if (dataresult.Result == ResultType.Success) // { // var data = JsonConvert.DeserializeObject<UserInfo>(JsonConvert.SerializeObject(dataresult.Data)); // #region 登錄具體 // ////通過事件 這裏可以通過事件配置來設置通知事件 // //await _eventService.RaiseAsync(new UserLoginSuccessEvent(data.UserName, data.Id, data.NickName, clientId: client?.ClientId)); // //寫入Claims // ClaimsIdentity claimsIdentity = new ClaimsIdentity("UserIdentity"); // //if (data.Scopes.Any()) // //{ // // var scopeslst = data.Scopes.Select(c => c.ClaimValue).ToList(); // // claimsIdentity.AddClaim(new Claim("operatorpermission", string.Join(",", scopeslst))); // // //data.Scopes.ForEach(c => // // //{ // // // claimsIdentity.AddClaim(new Claim(c.ClaimType, c.ClaimValue)); // // //}); // //} // ////寫入數據權限 // //if (data.DataPermissions.Any()) // //{ // // claimsIdentity.AddClaim(new Claim("datapermission", JsonConvert.SerializeObject(data.DataPermissions))); // //} // claimsIdentity.AddClaim(new Claim("authcode", data.AuthCode + "")); // claimsIdentity.AddClaim(new Claim("username", data.UserName + "")); // claimsIdentity.AddClaim(new Claim("nickname", data.NickName + "")); // claimsIdentity.AddClaim(new Claim("oid", data.OId + "")); // claimsIdentity.AddClaim(new Claim("oname", data.OName + "")); // claimsIdentity.AddClaim(new Claim("usertype", data.UserType + "")); // context.Result = new GrantValidationResult(subject: data.Id.ToString(), authenticationMethod: "password", claims: claimsIdentity.Claims.ToArray()); // #endregion // } // else // { // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, dataresult.Message); // } // } // else // { // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "登錄失敗"); // } // #endregion //} //else { // #region 微信登錄 // var result = await clientapi.PostAsJsonAsync("/api/oauth/wxlogin", new { openid = userpwd }); // if (result.IsSuccessStatusCode) // { // var operatorResult = await result.Content.ReadAsStringAsync(); // var dataresult = JsonConvert.DeserializeObject<OperatorResult>(operatorResult); // if (dataresult.Result == ResultType.Success) // { // var data = JsonConvert.DeserializeObject<UserInfo>(JsonConvert.SerializeObject(dataresult.Data)); // #region 登錄具體 // ClaimsIdentity claimsIdentity = new ClaimsIdentity("UserIdentity"); // claimsIdentity.AddClaim(new Claim("authcode", data.AuthCode + "")); // claimsIdentity.AddClaim(new Claim("username", data.UserName + "")); // claimsIdentity.AddClaim(new Claim("nickname", data.NickName + "")); // claimsIdentity.AddClaim(new Claim("oid", data.OId + "")); // claimsIdentity.AddClaim(new Claim("oname", data.OName + "")); // claimsIdentity.AddClaim(new Claim("usertype", data.UserType + "")); // claimsIdentity.AddClaim(new Claim("openid", data.OpenId + "")); // context.Result = new GrantValidationResult(subject: data.Id.ToString(), authenticationMethod: "password", claims: claimsIdentity.Claims.ToArray()); // #endregion // } // else // { // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, dataresult.Message); // } // } // else // { // context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "登錄失敗"); // } // #endregion //} #endregion await Task.CompletedTask; } }
public class CustomSecretValidator : ISecretValidator { public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret) { var jsonstr = JsonConvert.SerializeObject(parsedSecret.Properties); return Task.FromResult(new SecretValidationResult { Success = true, Confirmation = jsonstr }); } }
運行,可以看到生成了數據庫
PostMan幾種模式請求示例
創建API資源服務
這裏創建一個API資源服務,方便下面postman幾種模式請求
1. 創建一個API項目,引入nuget包
IdentityServer4.AccessTokenValidation
2. 注入服務
services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:2000"; options.RequireHttpsMetadata = false; options.ApiSecret = "testcode"; options.ApiName = "signalrapi"; });
3. 管道中在授權中間件之前添加認證中間件
app.UseAuthentication();
app.UseAuthorization();
4. 添加測試接口
[Route("api/[controller]")] [ApiController] [Authorize] public class TestController : ControllerBase { public TestController() { } [HttpGet] [Route("[action]")] public new IActionResult Get() { var userCliam = User.Claims; return Ok("請求成功"); } [HttpGet] [Route("[action]")] [AllowAnonymous] public new IActionResult GetCode(string code) { return Ok("請求的code爲:"+code); } }
這裏就不演示效果,下面幾種模式通過獲取的token直接請求這裏 /api/Test/get 就行了,如果
客戶端模式
適用於和用戶無關,機器與機器之間直接交互訪問資源的場景,適用於受信任的設備訪問。
POST https://api.oauth2server.com/token grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
密碼模式
適用於APP等第三方登錄
(授權碼)隱式模式
相對於授權碼模式少了獲取授權碼,直接根據賬號密碼登錄適用於web端
瀏覽器請求地址:
http://localhost:2000/connect/authorize?response_type=token&client_id=testcodeapi&redirect_uri=http://localhost:2002/signin-oidc&scope=signalrapi
授權碼模式
先獲取一個code授權碼,授權碼一般只有五分鐘有效時間,並且用一次就會失效,再通過授權碼加上賬號和密碼請求token。授權碼模式通過後臺傳輸Tokens,相對於簡化模式會更安全一點。
但每當考慮使用授權碼模式的時候,請使用混合模式。混合模式會首先返回一個可驗證的ID Token並且有更多其他特性。
或者
通過瀏覽器獲取token,請求地址:
http://localhost:2000/connect/authorize?response_type=code&client_id=testcode&redirect_uri=http://localhost:2002/api/Test/getcode&scope=signalrapi
再通過code獲取token