GitHub上例項都是整合了Identity來實現,我這裡去掉了相關東西,實現自定義的登入滿足自己的結構要求
服務端配置新增資料庫服務以及定時任務服務
builder.Services.AddDbContext<OpenIdDbContext>(options => { options.UseMySql(constr, ServerVersion.AutoDetect(constr), builder => { builder.UseRelationalNulls(); builder.MigrationsAssembly("OpenIdService"); }); options.UseOpenIddict(); }).AddQuartz(options => { options.UseMicrosoftDependencyInjectionJobFactory(); options.UseSimpleTypeLoader(); options.UseInMemoryStore(); }).AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
OpenIddict服務配置 根據需要而定
builder.Services.AddOpenIddict() .AddCore(options => { //配置OpenIddict以使用EntityFrameworkCore儲存和模型。 注意: 呼叫replacedefaultenentities()來替換預設的OpenIddict實體。 options.UseEntityFrameworkCore().UseDbContext<OpenIdDbContext>(); //喜歡使用MongoDB的開發人員可以刪除前面的程式碼行並配置OpenIddict使用指定的MongoDB資料庫: // options.UseMongoDb() .UseDatabase(new MongoClient().GetDatabase("openiddict")); options.UseQuartz(); }) .AddServer(options => { //配置互動服務地址 options.SetAuthorizationEndpointUris("/connect/authorize") .SetDeviceEndpointUris("/connect/device") .SetIntrospectionEndpointUris("/connect/introspect") .SetRevocationEndpointUris("/connect/revocat") .SetUserinfoEndpointUris("/connect/userinfo") .SetVerificationEndpointUris("/connect/verify") .SetLogoutEndpointUris("/connect/logout") .SetTokenEndpointUris("/connect/token") //這是允許的模式 .AllowAuthorizationCodeFlow() .AllowClientCredentialsFlow() .AllowDeviceCodeFlow() .AllowHybridFlow() .AllowImplicitFlow() .AllowPasswordFlow() .AllowRefreshTokenFlow() .RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles) //提供給API校驗Jwt令牌使用是配置 .AddEncryptionKey(new SymmetricSecurityKey( Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY="))) // 加密憑證 、註冊簽名 .AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate() //強制客戶端應用程式使用 Proof Key Code Exchange (PKCE) .RequireProofKeyForCodeExchange() .Configure(options => { options.CodeChallengeMethods.Add(CodeChallengeMethods.Plain); }) //配置 啟用通過後的後續處理 .UseAspNetCore().EnableStatusCodePagesIntegration() .EnableAuthorizationEndpointPassthrough() .EnableLogoutEndpointPassthrough() .EnableTokenEndpointPassthrough() .EnableUserinfoEndpointPassthrough() .EnableVerificationEndpointPassthrough() .DisableTransportSecurityRequirement(); // 禁用HTTPS 在開發測試環境 #region 禁用忽略選項配置 //禁用授權資訊儲存 // options.DisableAuthorizationStorage(); // options.AcceptAnonymousClients(); // options.DisableScopeValidation(); // options.IgnoreEndpointPermissions() // .IgnoreGrantTypePermissions() // .IgnoreResponseTypePermissions() // .IgnoreScopePermissions(); options.DisableAccessTokenEncryption(); #endregion }).AddValidation(options => { options.UseLocalServer(); //強制授權條目驗證 出於效能原因,OpenIddict 3.0在接收API請求時預設不檢查授權條目的狀態:即使附加的授權被撤銷 //,訪問令牌也被認為是有效的 options.EnableAuthorizationEntryValidation(); options.UseAspNetCore(); });
準備工作基本完成,如果你需要做一個服務登入介面,這是需要提供相關頁面和認證服務程式碼,然後去完成相關頁面邏輯
builder.Services.AddAuthentication(options => { options.DefaultScheme = UosoAuthenticationScheme.AuthenticationScheme; }).AddCookie(UosoAuthenticationScheme.AuthenticationScheme, options => { options.AccessDeniedPath = "/Account/Login"; options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/LogOut"; });
這裡需要自定義AuthorizationController 來實現認證邏輯,這裡可以參考官方例子程式碼說明
登入頁面邏輯採用自己的方式如下實現 SignInAsync 方法完成
var properties = new AuthenticationProperties { IsPersistent = model.RememberMe, ExpiresUtc= DateTimeOffset.UtcNow.AddMinutes(30) }; var principal = CreateUserPrincpal(resultdata); await HttpContext.SignInAsync(UosoAuthenticationScheme.AuthenticationScheme, principal, properties);
這裡放下AuthorizationController原始碼
public class AuthorizationController : Controller { private const string applicationname = UosoAuthenticationScheme.AuthenticationScheme; #region 注入OpenIddict相關互動介面 private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly IOpenIddictScopeManager _scopeManager; IAuthenticationSchemeProvider _schemeProvider; IUSUserLoginService _userService; #endregion #region 注入使用者資訊互動介面 //private readonly SignInManager<ApplicationUser> _signInManager; //private readonly UserManager<ApplicationUser> _userManager; #endregion public AuthorizationController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, IUSUserLoginService userService) { _applicationManager = applicationManager; _authorizationManager = authorizationManager; _scopeManager = scopeManager; _userService = userService; } #region 授權端點的操作 指定路由 這一步自己處理 private IEnumerable<string> GetDestinations(Claim claim) { // Note: by default, claims are NOT automatically included in the access and identity tokens. // To allow OpenIddict to serialize them, you must attach them a destination, that specifies // whether they should be included in access tokens, in identity tokens or in both. return claim.Type switch { Claims.Name or Claims.Subject => ImmutableArray.Create(Destinations.AccessToken, Destinations.IdentityToken), _ => ImmutableArray.Create(Destinations.AccessToken), }; } private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal) { switch (claim.Type) { case Claims.Name: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Profile)) yield return Destinations.IdentityToken; yield break; case Claims.Email: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Email)) yield return Destinations.IdentityToken; yield break; case Claims.Role: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Roles)) yield return Destinations.IdentityToken; yield break; case "AspNet.Identity.SecurityStamp": yield break; default: yield return Destinations.AccessToken; yield break; } } private ClaimsPrincipal CreateUserPrincpal(USUserLoginInfo resultdata, string claimsIdentityName = "USLOGININFO") { //登入成功流程 ClaimsIdentity identity = new ClaimsIdentity(claimsIdentityName); identity.AddClaim(new Claim(Claims.Subject, resultdata.Id + "")); identity.AddClaim(new Claim(Claims.Name, resultdata.UserName)); identity.AddClaim(new Claim(Claims.Nickname, resultdata.NickName)); identity.AddClaim(new Claim("tenantid", resultdata.TenantId + "")); identity.AddClaim(new Claim("organizes", resultdata.Organizes + "")); identity.AddClaim(new Claim("usergroups", resultdata.UserGroups)); identity.AddClaim(new Claim("usertype", resultdata.UserType + "")); identity.AddClaim(new Claim("userroles", resultdata.Roles + "")); identity.AddClaim(new Claim("userposts", resultdata.Posts + "")); return new ClaimsPrincipal(identity); } /// <summary> /// 登入許可權校驗 /// </summary> /// <returns></returns> /// <exception cref="InvalidOperationException"></exception> /// <exception cref="Exception"></exception> [HttpGet("~/connect/authorize")] [HttpPost("~/connect/authorize")] [IgnoreAntiforgeryToken] public async Task<IActionResult> Authorize() { // var s=await _schemeProvider.GetAllSchemesAsync(); //通過擴充套件的獲取自定義的引數校驗 var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("未獲取到相關認證情況"); #region 存在登入憑證且明確了登入請求的行為 // 存在登入憑證且明確了登入請求的行為 if (request.HasPrompt(Prompts.Login)) { //這裡有個小坑,在Challenge之前必須把這個行為去掉 不然 Challenge 進入 /connect/authorize 路由陷入死迴圈 var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login)); var parameters = Request.HasFormContentType ? Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt))); return Challenge( authenticationSchemes: applicationname, // IdentityConstants.ApplicationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) }); } #endregion //檢索本地的Cookies資訊 確定重定向頁面 這裡都是UTC時間來設定的過期情況 這裡沒有用Identity 所以這裡可以指定自己的應用名稱 var result = await HttpContext.AuthenticateAsync(applicationname); //IdentityConstants.ApplicationScheme #region 未獲取本地Cookies資訊或者 cookie過期的情況 if (request == null || !result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) { //是否是無效授權 if (request.HasPrompt(Prompts.None)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "使用者未登入." })); } return Challenge( authenticationSchemes: applicationname, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create( Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) }); } #endregion var resultdata = _userService.GetLoginInfo(new Guid(result.Principal.GetClaim(Claims.Subject) ?? throw new Exception("使用者標識存在"))) ?? throw new Exception("使用者詳細資訊不存在"); // 獲取客戶端詳細資訊 驗證其他資料 var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ?? throw new InvalidOperationException("未查詢到該客戶端的應用詳細資訊"); //查詢當前情況客戶端下請求使用者的持久化授權資料資訊 var authorizations = await _authorizationManager.FindAsync( subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("沒有找到客戶端的應用資訊"), //這裡區分下 是application的Id而不是ClientId status: Statuses.Valid, type: AuthorizationTypes.Permanent, scopes: request.GetScopes()).ToListAsync(); var consenttype = await _applicationManager.GetConsentTypeAsync(application); //獲取授權同意確認頁面 switch (consenttype) { //判斷授權同意的型別 //1 外部允許的且沒有任何授權項 case ConsentTypes.External when !authorizations.Any(): return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "登入使用者沒用訪問該客戶端應用的許可權" })); // 隱式、外部授權、顯示模式模式 case ConsentTypes.Implicit: case ConsentTypes.External when authorizations.Any(): case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent): ClaimsPrincipal principal = CreateUserPrincpal(resultdata); //設定請求的範圍 principal.SetScopes(request.GetScopes()); //查詢scope允許訪問的資源 var resources = await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync(); //通過擴充套件設定不同的資源訪問 其實本質都是設定Claims 只是 key 在 scope以及Resource上不同 //Resource = "oi_rsrc"; // Scope = "oi_scp"; principal.SetResources(resources); // 自動建立一個永久授權,以避免需要明確的同意 用於包含相同範圍的未來授權或令牌請求 var authorization = authorizations.LastOrDefault(); if (authorization is null) { authorization = await _authorizationManager.CreateAsync( principal: principal, subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application), type: AuthorizationTypes.Permanent, scopes: principal.GetScopes()); } principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } //登入 OpenIddict簽發令牌 return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // At this point, no authorization was found in the database and an error must be returned // if the client application specified prompt=none in the authorization request. case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Interactive user consent is required." })); // In every other case, render the consent form. default: return View(new AuthorizeViewModel { ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), Scope = request.Scope }); } } #region 同意、拒絕邏輯 [Authorize, FormValueRequired("submit.Accept")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] public async Task<IActionResult> Accept() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); var resultdata = _userService.GetLoginInfo(new Guid(User.GetClaim(Claims.Subject) ?? throw new Exception("使用者標識存在"))) ?? throw new Exception("使用者詳細資訊不存在"); // 獲取客戶端詳細資訊 驗證其他資料 var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ?? throw new InvalidOperationException("未查詢到該客戶端的應用詳細資訊"); //查詢當前情況客戶端下請求使用者的持久化授權資料資訊 var authorizations = await _authorizationManager.FindAsync( subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("沒有找到客戶端的應用資訊"), //這裡區分下 是application的Id而不是ClientId status: Statuses.Valid, type: AuthorizationTypes.Permanent, scopes: request.GetScopes()).ToListAsync(); if (!authorizations.Any() && await _applicationManager.HasConsentTypeAsync(application, ConsentTypes.External)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The logged in user is not allowed to access this client application." })); } ClaimsPrincipal principal = CreateUserPrincpal(resultdata); principal.SetScopes(request.GetScopes()); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); var authorization = authorizations.LastOrDefault(); if (authorization is null) { authorization = await _authorizationManager.CreateAsync( principal: principal, subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("未找到客戶端應用資訊"), type: AuthorizationTypes.Permanent, scopes: principal.GetScopes()); } principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } [Authorize, FormValueRequired("submit.Deny")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); #endregion #endregion #region 獲取Token地址 包括所有方式 /// <summary> /// 可以指定不同的獲取Token的客戶端邏輯 /// </summary> /// <returns></returns> /// <exception cref="InvalidOperationException"></exception> [HttpPost("~/connect/token"), Produces("application/json")] public async Task<IActionResult> Exchange() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OIDC請求不存在."); if (request.IsClientCredentialsGrantType()) { var application = await _applicationManager.FindByClientIdAsync(request.ClientId); if (application == null) { throw new InvalidOperationException("當前客戶端應用不存在"); } var identity = new ClaimsIdentity( TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); // Use the client_id as the subject identifier. identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application), Destinations.AccessToken, Destinations.IdentityToken); identity.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application), Destinations.AccessToken, Destinations.IdentityToken); var principal = new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim)); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsPasswordGrantType()) { var result = _userService.UserLogin(new LoginModel { username = request?.Username, userpwd = request.Password }); if (!result.IsSuccess) { throw new OpenIddictExceptions.ValidationException(result.Message); //return Forbid( // authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, // properties: new AuthenticationProperties(new Dictionary<string, string?> // { // [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, // [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = result.Message // })); } var resultdata = result.Data as USUserLoginInfo ?? throw new Exception("登入使用者資訊異常"); var principal = CreateUserPrincpal(resultdata); //這是密碼模式能方位那些 principal.SetScopes(new[] { Scopes.OpenId, Scopes.Email, Scopes.Profile, Scopes.Roles, "user_api", "openiddict_api" }); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType()) { var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; // principal.Identity.IsAuthenticated var user = _userService.GetLoginInfo(new Guid(principal?.GetClaim(Claims.Subject) ?? throw new Exception("使用者標識存在"))); if (user == null) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string?> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "令牌已失效" })); } // Ensure the user is still allowed to sign in. //if (!await _signInManager.CanSignInAsync(user)) //{ // return Forbid( // authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, // properties: new AuthenticationProperties(new Dictionary<string, string?> // { // [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, // [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." // })); //} foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } throw new InvalidOperationException("The specified grant type is not supported."); } [HttpGet("~/connect/logout")] public IActionResult Logout() => View(); [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken] public async Task<IActionResult> LogoutPost() { await HttpContext.SignOutAsync(); return SignOut( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties { RedirectUri = "/" }); } #endregion }
整個邏輯如下我畫一張圖結合關鍵程式碼來詮釋它
先說WebSite通過 OpenIdConnect 來完成登入認證 ,最後都是交給中介軟體中的signin-oidc頁面來完成客戶端Cookie登入狀態維持,如果在客戶端我們註釋掉會發生什麼
services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; // options.RequireAuthenticatedSignIn = true; }) // .AddCookie(options => //{ // options.LoginPath = "/"; // options.ExpireTimeSpan = TimeSpan.FromMinutes(50); // options.SlidingExpiration = false; //}) .AddOpenIdConnect(options => { // Note: these settings must match the application details // inserted in the database at the server level. options.ClientId = "mvc"; options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"; options.RequireHttpsMetadata = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = false; options.UsePkce=true; options.Prompt = OpenIdConnectPrompt.Login; options.ResponseType = OpenIdConnectResponseType.Code; options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; //options.Prompt = "login"; options.Authority = "http://localhost:5276"; //options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); options.Scope.Add("roles"); options.Scope.Add("user_api"); options.Scope.Add("openiddict_api"); options.SecurityTokenValidator = new JwtSecurityTokenHandler { InboundClaimTypeMap = new Dictionary<string, string>() }; options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; options.AccessDeniedPath = "/"; });
這就是為什麼我們寫回撥地址 要寫 http://localhost:44381/signin-oidc 的signin-oidc ,如果要寫其他的,需要我們自己處理,這裡OpenIdConnect中介軟體幫我們處理了,這其中包括維持客戶端的登入狀態,類似我們用其他第三方登入一樣回撥後需要按某個協議來處理,如果客戶端設定了
options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = false;
GetClaimsFromUserInfoEndpoint=true 需要服務端準備UserInfo介面否則會報錯
SaveTokens=true 客戶端可以拿到 var token = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);拿到AccessToken
WebAPI 可結合服務端配置的
AddEncryptionKey(new SymmetricSecurityKey( Convert.FromBase64String("DRjd/GnduI3Efzen9V9BvbNUfc/VKgXltV7Kbk9sMkY=")))
Token令牌的簽發 /connect/token 介面 根據請求模式簽發對應令牌
if (request.IsClientCredentialsGrantType()) { //處理 } else if (request.IsPasswordGrantType()) { //處理 } else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType()) { //處理 }