ASP.NET Core實現OAuth2.0的AuthorizationCode模式

ASP.NET Core實現OAuth2的AuthorizationCode模式

受權服務器

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             });
複製代碼

Client配置

方式一:api

.AddInMemoryClients(Config.GetClients())    //將Clients設置到內存中,IdentityServer4從中獲取進行驗證服務器

方式二(推薦):cookie

services.AddSingleton<IClientStore, MyClientStore>();   //注入IClientStore的實現,用於運行時獲取和校驗Clientapp

IClientStore的實現

複製代碼
 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     }
複製代碼

Scope配置

方式一:async

.AddInMemoryScopes(Config.GetScopes()) //將Scopes設置到內存中,IdentityServer4從中獲取進行驗證ide

方式二(推薦):測試

services.AddSingleton<IScopeStore, MyScopeStore>();    //注入IScopeStore的實現,用於運行時獲取和校驗Scope

IScopeStore的實現

複製代碼
 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):

流程實現

步驟A

第三方客戶端頁面簡單實現:

點擊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);
複製代碼

 

步驟B

 受權服務器接收到請求後,會判斷用戶是否已經登錄,若是未登錄那麼跳轉到登錄頁面(若是已經登錄,登錄的一些相關信息會存儲在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         }
複製代碼

 

步驟C

受權成功,重定向到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         }
複製代碼

 

步驟D

受權成功後重定向到指定的第三方端(步驟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         }
複製代碼

 

步驟E

受權服務器對步驟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         }
複製代碼
相關文章
相關標籤/搜索