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 }

