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


前言

在上一篇中實現了resource owner password credentials和client credentials模式:http://www.cnblogs.com/skig/p/6079457.html ,而這篇介紹實現AuthorizationCode模式。

OAuth2.0授權框架文檔說明參考:https://tools.ietf.org/html/rfc6749

ASP.NET Core開發OAuth2的項目使用了IdentityServer4,參考:https://identityserver4.readthedocs.io/en/dev/,源碼:https://github.com/IdentityServer

.NET中開發OAuth2可使用OWIN,可參考:https://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server

 

ASP.NET Core實現OAuth2的AuthorizationCode模式

授權服務器

Program.cs --> Main方法中:需要調用UseUrls設置IdentityServer4授權服務的IP地址

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方法中的配置:

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配置

方式一:

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

方式二(推薦):

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

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配置

方式一:

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

方式二(推薦):

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         }

 

注意

1. 授權服務器中生成的RefreshToken和AuthorizationCode默認是存儲在內存中的,因此如果服務重啟這些數據就失效了,那么就需要實現IPersistedGrantStore接口對這些數據的存儲,將這些數據寫入到數據庫或者NoSql(Redis)中,實現代碼可參考源代碼;

2.資源服務器在第一次解析AccessToken的時候會先到授權服務器獲取配置數據(例如會訪問:http://localhost:5114/.well-known/openid-configuration 獲取配置的,http://localhost:5114/.well-known/openid-configuration/jwks 獲取jwks)),之后解析AccessToken都會使用第一次獲取到的配置數據,因此如果授權服務的配置更改了(加密證書等等修改了),那么應該重啟資源服務器使之重新獲取新的配置數據;

3.調試IdentityServer4框架的時候應該配置好ILogger,因為授權過程中的訪問(例如授權失敗等等)信息都會調用ILogger進行日志記錄,可使用NLog,例如:

  在Startup.cs --> Configure方法中配置:loggerFactory.AddNLog();//添加NLog

 

源碼:http://files.cnblogs.com/files/skig/OAuth2AuthorizationCode.zip

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM