你的位置:首页 > ASP.net教程

[ASP.net教程]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 Cores实现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

 

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