首页 > 代码库 > ASP.NET Core实现OAuth2.0的AuthorizationCode模式

ASP.NET Core实现OAuth2.0的AuthorizationCode模式

ASP.NET Core实现OAuth2的AuthorizationCode模式

授权服务器

Program.cs --> Main方法中:需要调用UseUrls设置IdentityServer4授权服务的IP地址

技术分享

1             var host = new WebHostBuilder()2                 .UseKestrel()3                 //IdentityServer4的使用需要配置UseUrls4                 .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             //使用IdentityServer42             app.UseIdentityServer();3             //使用Cookie模块4             app.UseCookieAuthentication(new CookieAuthenticationOptions5             {6                 AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,7                 AutomaticAuthenticate = false,8                 AutomaticChallenge = false9             });

技术分享

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 Client13                     {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 day26                         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的Scope16                 StandardScopes.OfflineAccess.Name,17                 StandardScopes.OfflineAccess18             },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                     else34                     {35                         break;36                     }37                 }38             }39             //返回值scopes不能为null40             return Task.FromResult<IEnumerable<Scope>>(scopes);41         }42 43         public Task<IEnumerable<Scope>> GetScopesAsync(bool publicOnly = true)44         {45             //publicOnly为true:获取public的scope;为false:获取所有的scope46             //这里不做区分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.Clientid5                             + "&redirect_uri=" + OAuthConstants.AuthorizeCodeCallBackPath6                             + "&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 AuthenticationProperties29                         {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             //生成错误信息的LoginViewModel47             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 ErrorViewModel16             {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                         //设置已选择授权的Scopes24                         if (model.ScopesConsented != null && model.ScopesConsented.Any())25                         {26                             response = new ConsentResponse27                             {28                                 RememberConsent = model.RememberConsent,29                                 ScopesConsented = model.ScopesConsented30                             };31                         }32                         else33                         {34                             ModelState.AddModelError("", "You must pick at least one permission.");35                         }36                     }37                     else38                     {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 ErrorViewModel57             {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 Code11                     "&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 LogoutViewModel14             {15                 LogoutId = logoutId16             };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 LoggedOutViewModel38             {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


ASP.NET Core实现OAuth2.0的AuthorizationCode模式