ASP.NET WebAPI2第007天_實踐Authentication以及Authorization_OAuth概述
在.net web api 專案若要去實踐Authentication以及Authorization
預設專案配置會涉及到asp.net identity的使用
而identity又會涉及到Entity Framework的使用
這些在過去的學習部落格篇章都有筆記到
因此不會特別在贅述
稍微介紹到一個詞 Oauth
OAuth是一個開發標準(Open Standard)用來處理有關「授權」(Authorization)相關的行為。
有分為OAuth 1.0跟OAuth 2.0
OAuth 2.0是OAuth協定的下一版本,但不向下相容OAuth 1.0。
OAuth 2.0關注客戶端開發者的簡易性,同時為Web應用、桌面應用、手機和智慧型裝置提供專門的認證流程。
OAuth 允許讓第三方應用存取該使用者在某一網站或平台(Google,Facebook,Twitter,Apple)
上儲存的私密的資源(如相片,影片,聯絡人列表),
而無需將使用者名稱和密碼提供給第三方應用。
而無需將使用者名稱和密碼提供給第三方應用。
更白話說就是透過第三方登入
Sign in with Google
Sign in with Facebook....
在OAuth 2
具有四種角色
Resource Owner(資料擁有者) : 你(我們自己)、user ,也可能直接是Client (應用)不是人
Client(應用程式) : User授權給第三方的應用程式,例如:蝦皮、instagram、FB心理測驗遊戲、手機APP。
Authorization Server(總管授權Server) : 可能是一個網頁要讓User本人授權取回的是一個Grant(授權狀)而非Token。
Resource Server(Client從這拿資料也就是API) : 通常一般情況跟Authorization Server同來源
規範了四個核發(Grant)類型流程
- Authorization Code(授權碼) Grant Type Flow
- Implicit Grant Type Flow
- Resource Owner Password Credentials Grant Type Flow
- Client Credentials Grant Type Flow
四個核發流程是運用在不同的場合,
Authorization Code 核發流程的安全性最高,然而流程最繁複,實作起來的步驟也多。
Implicit 核發流程是其簡化版本,然而安全性低了一些(曝露了 Acccess Token)。
Password Credentials 核發流程則適用於對於密碼在自己控管中的場合,統一管理授權可採用 。
Client Credentials 核發流程適用於不涉及使用者的內部伺服器間授權管理,或者內部完全可信任的客戶端 。
這四種核發(授予)模式可根據以下幾個指標去判定適用何種場合
1.存取token(Access token)由誰持有
存放在固定機器 or 給User持有
2.客戶的型態為何?
網站應用 or 行動裝置APP
3.客戶與資源伺服器是 第一方 還 第三方 關係
3-1.Instagram想讓使用者能使用Facebook身份登入,
由於Instagram與Facebook是同公司的兩個服務,兩者是第一方關係。
3-2.Spotify想讓使用者能使用Facebook身份登入,
由於Spotify與Facebook是不同公司,兩者是第三方關係。
更多Oauth的細節可能無法一時全介紹
只要知道一個核心概念就是全新一套避免直接提供帳密而用token替代的授權機制
大致上的流程以Sign in with Google為例
Step 1:授權請求
Client User點擊第三方應用程式「 以 Google 登入 」,
此時,該應用程式就會向使用者(即資源擁有者 Resource owner )索取資料使用的授權請求。
Step 2:授權許可
Client User會收到一個授權請求,若他點擊同意授權,該應用程式則會收到一個授權許可,
有點類似確認是否真的是你本人才授權該應用,代表他願意讓應用程式代為向 Google 的授權伺服器 (Authorization server) 進行溝通。
Step 3:Access Token 請求
由於已得到使用者本人的授權許可,因此,該應用程式會憑藉授權許可,主動向 Google 的授權伺服器發送 Access Token 請求,接著 Google 會進行資料比對,如 用戶端 ID、用戶端 密碼,確認應用程式的合法性。
Step 4:Access Token 簽發
Google 的授權伺服器,若資料成功比對後,則會發給該應用程式一個 Access Token,讓它接下來能夠透過這個 Access Token 向 Google 資源伺服器 (Resource Server)索取所需要的資料。
Step 5:資料索取
此時,該應用程式,不僅透過 Access Token 認證身份,同時,也會向 Google 資源伺服器提出資料的請求。
Step 6:資料回傳
最後,Google 資源伺服器會驗證 Access Token,若確認無誤,則會將使用者被保護的資料授權給該應用程式來使用,進而完成透過 Google 完成第三方登入應用程式的功能。
新增一個全新.net web專案(for web api)
預設產出的專案
~\MyWebAPI2_Oauth\Controllers\ValuesController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace MyWebAPI2_Oauth.Controllers { [Authorize] public class ValuesController : ApiController { // GET api/values public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 public string Get(int id) { return "value"; } // POST api/values public void Post([FromBody]string value) { } // PUT api/values/5 public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 public void Delete(int id) { } } } |
~\MyWebAPI2_Oauth\Controllers\AccountController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | using System; using System.Collections.Generic; using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.OAuth; using MyWebAPI2_Oauth.Models; using MyWebAPI2_Oauth.Providers; using MyWebAPI2_Oauth.Results; namespace MyWebAPI2_Oauth.Controllers { [Authorize] [RoutePrefix("api/Account")] public class AccountController : ApiController { private const string LocalLoginProvider = "Local"; private ApplicationUserManager _userManager; public AccountController() { } public AccountController(ApplicationUserManager userManager, ISecureDataFormat<AuthenticationTicket> accessTokenFormat) { UserManager = userManager; AccessTokenFormat = accessTokenFormat; } public ApplicationUserManager UserManager { get { return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); } private set { _userManager = value; } } public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; } // GET api/Account/UserInfo [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)] [Route("UserInfo")] public UserInfoViewModel GetUserInfo() { ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity); return new UserInfoViewModel { Email = User.Identity.GetUserName(), HasRegistered = externalLogin == null, LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null }; } // POST api/Account/Logout [Route("Logout")] public IHttpActionResult Logout() { Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType); return Ok(); } // GET api/Account/ManageInfo?returnUrl=%2F&generateState=true [Route("ManageInfo")] public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false) { IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); if (user == null) { return null; } List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>(); foreach (IdentityUserLogin linkedAccount in user.Logins) { logins.Add(new UserLoginInfoViewModel { LoginProvider = linkedAccount.LoginProvider, ProviderKey = linkedAccount.ProviderKey }); } if (user.PasswordHash != null) { logins.Add(new UserLoginInfoViewModel { LoginProvider = LocalLoginProvider, ProviderKey = user.UserName, }); } return new ManageInfoViewModel { LocalLoginProvider = LocalLoginProvider, Email = user.UserName, Logins = logins, ExternalLoginProviders = GetExternalLogins(returnUrl, generateState) }; } // POST api/Account/ChangePassword [Route("ChangePassword")] public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST api/Account/SetPassword [Route("SetPassword")] public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST api/Account/AddExternalLogin [Route("AddExternalLogin")] public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken); if (ticket == null || ticket.Identity == null || (ticket.Properties != null && ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow)) { return BadRequest("外部登入失敗。"); } ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity); if (externalData == null) { return BadRequest("外部登入已與帳戶相關聯。"); } IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey)); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST api/Account/RemoveLogin [Route("RemoveLogin")] public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result; if (model.LoginProvider == LocalLoginProvider) { result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId()); } else { result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(), new UserLoginInfo(model.LoginProvider, model.ProviderKey)); } if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // GET api/Account/ExternalLogin [OverrideAuthentication] [HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)] [AllowAnonymous] [Route("ExternalLogin", Name = "ExternalLogin")] public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null) { if (error != null) { return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error)); } if (!User.Identity.IsAuthenticated) { return new ChallengeResult(provider, this); } ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity); if (externalLogin == null) { return InternalServerError(); } if (externalLogin.LoginProvider != provider) { Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); return new ChallengeResult(provider, this); } ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey)); bool hasRegistered = user != null; if (hasRegistered) { Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager, OAuthDefaults.AuthenticationType); ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName); Authentication.SignIn(properties, oAuthIdentity, cookieIdentity); } else { IEnumerable<Claim> claims = externalLogin.GetClaims(); ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType); Authentication.SignIn(identity); } return Ok(); } // GET api/Account/ExternalLogins?returnUrl=%2F&generateState=true [AllowAnonymous] [Route("ExternalLogins")] public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false) { IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes(); List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>(); string state; if (generateState) { const int strengthInBits = 256; state = RandomOAuthStateGenerator.Generate(strengthInBits); } else { state = null; } foreach (AuthenticationDescription description in descriptions) { ExternalLoginViewModel login = new ExternalLoginViewModel { Name = description.Caption, Url = Url.Route("ExternalLogin", new { provider = description.AuthenticationType, response_type = "token", client_id = Startup.PublicClientId, redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri, state = state }), State = state }; logins.Add(login); } return logins; } // POST api/Account/Register [AllowAnonymous] [Route("Register")] public async Task<IHttpActionResult> Register(RegisterBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = new ApplicationUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user, model.Password); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST api/Account/RegisterExternal [OverrideAuthentication] [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)] [Route("RegisterExternal")] public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var info = await Authentication.GetExternalLoginInfoAsync(); if (info == null) { return InternalServerError(); } var user = new ApplicationUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user); if (!result.Succeeded) { return GetErrorResult(result); } result = await UserManager.AddLoginAsync(user.Id, info.Login); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } protected override void Dispose(bool disposing) { if (disposing && _userManager != null) { _userManager.Dispose(); _userManager = null; } base.Dispose(disposing); } #region Helper private IAuthenticationManager Authentication { get { return Request.GetOwinContext().Authentication; } } private IHttpActionResult GetErrorResult(IdentityResult result) { if (result == null) { return InternalServerError(); } if (!result.Succeeded) { if (result.Errors != null) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } if (ModelState.IsValid) { // 沒有要傳送的 ModelState 錯誤,因此僅傳回空的 BadRequest。 return BadRequest(); } return BadRequest(ModelState); } return null; } private class ExternalLoginData { public string LoginProvider { get; set; } public string ProviderKey { get; set; } public string UserName { get; set; } public IList<Claim> GetClaims() { IList<Claim> claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider)); if (UserName != null) { claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider)); } return claims; } public static ExternalLoginData FromIdentity(ClaimsIdentity identity) { if (identity == null) { return null; } Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier); if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer) || String.IsNullOrEmpty(providerKeyClaim.Value)) { return null; } if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer) { return null; } return new ExternalLoginData { LoginProvider = providerKeyClaim.Issuer, ProviderKey = providerKeyClaim.Value, UserName = identity.FindFirstValue(ClaimTypes.Name) }; } } private static class RandomOAuthStateGenerator { private static RandomNumberGenerator _random = new RNGCryptoServiceProvider(); public static string Generate(int strengthInBits) { const int bitsPerByte = 8; if (strengthInBits % bitsPerByte != 0) { throw new ArgumentException("strengthInBits 必須平均地被 8 除盡。", "strengthInBits"); } int strengthInBytes = strengthInBits / bitsPerByte; byte[] data = new byte[strengthInBytes]; _random.GetBytes(data); return HttpServerUtility.UrlTokenEncode(data); } } #endregion } } |
在此嘗試GET方式存取API https://localhost:44354/api/values
當運行起來會發現有出現"已拒絕此要求的授權"的錯誤訊息
這邊我們會需要註冊一個新帳戶才能進行API存取
這裡準備RegisterModel所需欄位的json資料透過post呼叫
/api/Account/Register做註冊
再到檢視->SQL Server物件總管看就能看到新增的一筆註冊用戶資料了
在我們的
~\App_Start\Startup.Auth.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Google; using Microsoft.Owin.Security.OAuth; using Owin; using MyWebAPI2_Oauth.Providers; using MyWebAPI2_Oauth.Models; namespace MyWebAPI2_Oauth { public partial class Startup { public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; } public static string PublicClientId { get; private set; } // 如需設定驗證的詳細資訊,請瀏覽 https://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { // 設定資料庫內容和使用者管理員以針對每個要求使用單一執行個體 app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); // 讓應用程式使用 Cookie 儲存已登入使用者的資訊 // 並使用 Cookie 暫時儲存使用者利用協力廠商登入提供者登入的相關資訊; app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // 設定 OAuth 基礎流程的應用程式 PublicClientId = "self"; OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/Token"), Provider = new ApplicationOAuthProvider(PublicClientId), AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(14), // 在生產模式中設定 AllowInsecureHttp = false AllowInsecureHttp = true }; // 讓應用程式使用 Bearer 權杖驗證使用者 app.UseOAuthBearerTokens(OAuthOptions); // 註銷下列各行以啟用利用協力廠商登入提供者登入 //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() //{ // ClientId = "", // ClientSecret = "" //}); } } } |
TokenEndpointPath 預先指定要取得token的存取路徑 /token
在此利用post並設置username , password , grant_type來獲取token
1 2 3 4 5 6 7 8 | { "access_token": "kpUftta22l_14Yzn8q-WATFRdV2USA2xp5ptuSGvUEOOIqIem-AQiCwAy3aKYOKb2UJNtnLz_Q3L2UHlt68aXF_6SnikK_lqyulQOOgEzTh1uREAC_aC5RaF_oboKrfL5s-stSmRIGguSP1W7Wi_CQJ-_xVxxd79H3vCpHSLGgK1w1RHTxXSTm_3oXjEYLsJWfE7djDPRnsQd5Y_oCqyXUIV8Y1dCQQpqJ2f5T0FvYgCEdx5hVK1DY-YTA3nYr72YJSXbaUuL6FRQWDCmoc9OUYWBvJlyJRsMyj1SmNKrlzk_kzByBiBXPX3SToCbfme4aCCFo8S0Ekm94cfJRC_erVh4WYX6-EevKULRVKCYI-qiEZJlrmyn_Vhk7DLWTRFMvgW_ed1eooC_sPCJ44yRIG_3qSzjrCIoJmTI0Haunafhycl_5CQYgFDUBT7_MvUNDoMf1KIr7480yF92f0-4w_4dyQuIwX4Ene3dxy5ROSPKA27wYohlRg9OBRB0nmN", "token_type": "bearer", "expires_in": 1209599, "userName": "abc321@gmail.com", ".issued": "Tue, 28 Dec 2021 07:56:00 GMT", ".expires": "Tue, 11 Jan 2022 07:56:00 GMT" } |
再次存取values API時
就需要設置Header 鍵值
Authorization:bearer kpUftta22l_14Yzn8q-WATFRdV2USA2xp5ptuSGvUEOOIqIem-AQiCwAy3aKYOKb2UJNtnLz_Q3L2UHlt68aXF_6SnikK_lqyulQOOgEzTh1uREAC_aC5RaF_oboKrfL5s-stSmRIGguSP1W7Wi_CQJ-_xVxxd79H3vCpHSLGgK1w1RHTxXSTm_3oXjEYLsJWfE7djDPRnsQd5Y_oCqyXUIV8Y1dCQQpqJ2f5T0FvYgCEdx5hVK1DY-YTA3nYr72YJSXbaUuL6FRQWDCmoc9OUYWBvJlyJRsMyj1SmNKrlzk_kzByBiBXPX3SToCbfme4aCCFo8S0Ekm94cfJRC_erVh4WYX6-EevKULRVKCYI-qiEZJlrmyn_Vhk7DLWTRFMvgW_ed1eooC_sPCJ44yRIG_3qSzjrCIoJmTI0Haunafhycl_5CQYgFDUBT7_MvUNDoMf1KIr7480yF92f0-4w_4dyQuIwX4Ene3dxy5ROSPKA27wYohlRg9OBRB0nmN
Ref:
【開放銀行特別報導】金融個資存取如何更安全?開放銀行將引進OAuth 2委任授權架構
OpenID Connect 是什麼?
電影欣賞《OAuth 2.0 and OpenID Connect (in plain English)》
「筆記」- 何謂 OAuth?Google 實例說明 OAuth 處理流程
留言
張貼留言