• 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

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

    作者:skig
    欢迎转载,但请注明出处。如果大家有任何疑问,可以给我留言,我会及时回复。
  • 相关阅读:
    伪元素 first-letter
    html语义化 -------<fieldset>和<legend>
    《ASP.NET MVC4 WEB编程》学习笔记------ViewBag、ViewData和TempData的使用和区别
    《ASP.NET MVC4 WEB编程》学习笔记------.net mvc实现原理ActionResult/View
    《ASP.NET MVC4 WEB编程》学习笔记------RenderBody,RenderPage,RenderSection
    《转》Visual Studio 2010 终极定制安装精简方法
    《转》IIS中配置通配符应用程序映射
    IIS安装时,添加/编辑应用程序扩展名映射 确定按钮不可用。
    异常:操作可能会破坏运行时稳定性
    petri网学习心得
  • 原文地址:https://www.cnblogs.com/skig/p/AspNetCoreAuthCode.html
Copyright © 2020-2023  润新知