前言

在上一篇中实现了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地址

             var host = new WebHostBuilder()
.UseKestrel()
//IdentityServer4的使用需要配置UseUrls
.UseUrls("http://localhost:5114")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();

Startup.cs -->ConfigureServices方法中的配置:

             //RSA:证书长度2048以上,否则抛异常
//配置AccessToken的加密证书
var rsa = new RSACryptoServiceProvider();
//从配置文件获取加密证书
rsa.ImportCspBlob(Convert.FromBase64String(Configuration["SigningCredential"]));
//配置IdentityServer4
services.AddSingleton<IClientStore, MyClientStore>(); //注入IClientStore的实现,可用于运行时校验Client
services.AddSingleton<IScopeStore, MyScopeStore>(); //注入IScopeStore的实现,可用于运行时校验Scope
//注入IPersistedGrantStore的实现,用于存储AuthorizationCode和RefreshToken等等,默认实现是存储在内存中,
//如果服务重启那么这些数据就会被清空了,因此可实现IPersistedGrantStore将这些数据写入到数据库或者NoSql(Redis)中
services.AddSingleton<IPersistedGrantStore, MyPersistedGrantStore>();
services.AddIdentityServer()
.AddSigningCredential(new RsaSecurityKey(rsa));
//.AddTemporarySigningCredential() //生成临时的加密证书,每次重启服务都会重新生成
//.AddInMemoryScopes(Config.GetScopes()) //将Scopes设置到内存中
//.AddInMemoryClients(Config.GetClients()) //将Clients设置到内存中

Startup.cs --> Configure方法中的配置:

             //使用IdentityServer4
app.UseIdentityServer();
//使用Cookie模块
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
AutomaticAuthenticate = false,
AutomaticChallenge = false
});

Client配置

方式一:

.AddInMemoryClients(Config.GetClients())    //将Clients设置到内存中,IdentityServer4从中获取进行验证

方式二(推荐):

services.AddSingleton<IClientStore, MyClientStore>();   //注入IClientStore的实现,用于运行时获取和校验Client

IClientStore的实现

     public class MyClientStore : IClientStore
{
readonly Dictionary<string, Client> _clients;
readonly IScopeStore _scopes;
public MyClientStore(IScopeStore scopes)
{
_scopes = scopes;
_clients = new Dictionary<string, Client>()
{
{
"auth_clientid",
new Client
{
ClientId = "auth_clientid",
ClientName = "AuthorizationCode Clientid",
AllowedGrantTypes = new string[] { GrantType.AuthorizationCode }, //允许AuthorizationCode模式
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:6321/Home/AuthCode" },
PostLogoutRedirectUris = { "http://localhost:6321/" },
//AccessTokenLifetime = 3600, //AccessToken过期时间, in seconds (defaults to 3600 seconds / 1 hour)
//AuthorizationCodeLifetime = 300, //设置AuthorizationCode的有效时间,in seconds (defaults to 300 seconds / 5 minutes)
//AbsoluteRefreshTokenLifetime = 2592000, //RefreshToken的最大过期时间,in seconds. Defaults to 2592000 seconds / 30 day
AllowedScopes = (from l in _scopes.GetEnabledScopesAsync(true).Result select l.Name).ToList(),
}
}
};
} public Task<Client> FindClientByIdAsync(string clientId)
{
Client client;
_clients.TryGetValue(clientId, out client);
return Task.FromResult(client);
}
}

Scope配置

方式一:

.AddInMemoryScopes(Config.GetScopes()) //将Scopes设置到内存中,IdentityServer4从中获取进行验证

方式二(推荐):

services.AddSingleton<IScopeStore, MyScopeStore>();    //注入IScopeStore的实现,用于运行时获取和校验Scope

IScopeStore的实现

     public class MyScopeStore : IScopeStore
{
readonly static Dictionary<string, Scope> _scopes = new Dictionary<string, Scope>()
{
{
"api1",
new Scope
{
Name = "api1",
DisplayName = "api1",
Description = "My API",
}
},
{
//RefreshToken的Scope
StandardScopes.OfflineAccess.Name,
StandardScopes.OfflineAccess
},
}; public Task<IEnumerable<Scope>> FindScopesAsync(IEnumerable<string> scopeNames)
{
List<Scope> scopes = new List<Scope>();
if (scopeNames != null)
{
Scope sc;
foreach (var sname in scopeNames)
{
if (_scopes.TryGetValue(sname, out sc))
{
scopes.Add(sc);
}
else
{
break;
}
}
}
//返回值scopes不能为null
return Task.FromResult<IEnumerable<Scope>>(scopes);
} public Task<IEnumerable<Scope>> GetScopesAsync(bool publicOnly = true)
{
//publicOnly为true:获取public的scope;为false:获取所有的scope
//这里不做区分
return Task.FromResult<IEnumerable<Scope>>(_scopes.Values);
}
}

资源服务器

资源服务器的配置在上一篇中已介绍(http://www.cnblogs.com/skig/p/6079457.html ),详情也可参考源代码。

测试

AuthorizationCode模式的流程图(来自:https://tools.ietf.org/html/rfc6749):

流程实现

步骤A

第三方客户端页面简单实现:

点击AccessToken按钮进行访问授权服务器,就是流程图中步骤A:

                         //访问授权服务器
return Redirect(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.AuthorizePath + "?"
+ "response_type=code"
+ "&client_id=" + OAuthConstants.Clientid
+ "&redirect_uri=" + OAuthConstants.AuthorizeCodeCallBackPath
+ "&scope=" + OAuthConstants.Scopes
+ "&state=" + OAuthConstants.State);

步骤B

授权服务器接收到请求后,会判断用户是否已经登陆,如果未登陆那么跳转到登陆页面(如果已经登陆,登陆的一些相关信息会存储在cookie中):

         /// <summary>
/// 登陆页面
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
var vm = BuildLoginViewModel(returnUrl, context);
return View(vm);
} /// <summary>
/// 登陆账号验证
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
if (ModelState.IsValid)
{
//账号密码验证
if (model.Username == "admin" && model.Password == "")
{
AuthenticationProperties props = null;
//判断是否 记住登陆
if (model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMonths()
};
};
//参数一:Subject,可在资源服务器中获取到,资源服务器通过User.Claims.Where(l => l.Type == "sub").FirstOrDefault();获取
//参数二:账号
await HttpContext.Authentication.SignInAsync("admin", "admin", props);
//验证ReturnUrl,ReturnUrl为重定向到授权页面
if (_interaction.IsValidReturnUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return Redirect("~/");
}
ModelState.AddModelError("", "Invalid username or password.");
}
//生成错误信息的LoginViewModel
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}

登陆成功后,重定向到授权页面,询问用户是否授权,就是流程图的步骤B了:

         /// <summary>
/// 显示用户可授予的权限
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> Index(string returnUrl)
{
var vm = await BuildViewModelAsync(returnUrl);
if (vm != null)
{
return View("Index", vm);
} return View("Error", new ErrorViewModel
{
Error = new ErrorMessage { Error = "Invalid Request" },
});
}

步骤C

授权成功,重定向到redirect_uri(步骤A传递的)所指定的地址(第三方端),并且会把Authorization Code也设置到url的参数code中:

         /// <summary>
/// 用户授权验证
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(ConsentInputModel model)
{
//解析returnUrl
var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (request != null && model != null)
{
if (ModelState.IsValid)
{
ConsentResponse response = null;
//用户不同意授权
if (model.Button == "no")
{
response = ConsentResponse.Denied;
}
//用户同意授权
else if (model.Button == "yes")
{
//设置已选择授权的Scopes
if (model.ScopesConsented != null && model.ScopesConsented.Any())
{
response = new ConsentResponse
{
RememberConsent = model.RememberConsent,
ScopesConsented = model.ScopesConsented
};
}
else
{
ModelState.AddModelError("", "You must pick at least one permission.");
}
}
else
{
ModelState.AddModelError("", "Invalid Selection");
}
if (response != null)
{
//将授权的结果设置到identityserver中
await _interaction.GrantConsentAsync(request, response);
//授权成功重定向
return Redirect(model.ReturnUrl);
}
}
//有错误,重新授权
var vm = await BuildViewModelAsync(model.ReturnUrl, model);
if (vm != null)
{
return View(vm);
}
}
return View("Error", new ErrorViewModel
{
Error = new ErrorMessage { Error = "Invalid Request" },
});
}

步骤D

授权成功后重定向到指定的第三方端(步骤A所指定的redirect_uri),然后这个重定向的地址中去实现获取AccessToken(就是由第三方端实现):

         public IActionResult AuthCode(AuthCodeModel model)
{
GrantClientViewModel vmodel = new GrantClientViewModel();
if (model.state == OAuthConstants.State)
{
//通过Authorization Code获取AccessToken
var client = new HttpClientHepler(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.TokenPath);
client.PostAsync(null,
"grant_type=" + "authorization_code" +
"&code=" + model.code + //Authorization Code
"&redirect_uri=" + OAuthConstants.AuthorizeCodeCallBackPath +
"&client_id=" + OAuthConstants.Clientid +
"&client_secret=" + OAuthConstants.Secret,
hd => hd.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded"),
rtnVal =>
{
var jsonVal = JsonConvert.DeserializeObject<dynamic>(rtnVal);
vmodel.AccessToken = jsonVal.access_token;
vmodel.RefreshToken = jsonVal.refresh_token;
},
fault => _logger.LogError("Get AccessToken Error: " + fault.ReasonPhrase),
ex => _logger.LogError("Get AccessToken Error: " + ex)).Wait();
} return Redirect("~/Home/Index?"
+ nameof(vmodel.AccessToken) + "=" + vmodel.AccessToken + "&"
+ nameof(vmodel.RefreshToken) + "=" + vmodel.RefreshToken);
}

步骤E

授权服务器对步骤D请求传递的Authorization Code进行验证,验证成功生成AccessToken并返回:

其中,点击RefreshToken进行刷新AccessToken:

                             //刷新AccessToken
var client = new HttpClientHepler(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.TokenPath);
client.PostAsync(null,
"grant_type=" + "refresh_token" +
"&client_id=" + OAuthConstants.Clientid +
"&client_secret=" + OAuthConstants.Secret +
"&refresh_token=" + model.RefreshToken,
hd => hd.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded"),
rtnVal =>
{
var jsonVal = JsonConvert.DeserializeObject<dynamic>(rtnVal);
vmodel.AccessToken = jsonVal.access_token;
vmodel.RefreshToken = jsonVal.refresh_token;
},
fault => _logger.LogError("RefreshToken Error: " + fault.ReasonPhrase),
ex => _logger.LogError("RefreshToken Error: " + ex)).Wait();

点击CallResources访问资源服务器:

                             //访问资源服务
var client = new HttpClientHepler(OAuthConstants.ResourceServerBaseAddress + OAuthConstants.ResourcesPath);
client.GetAsync(null,
hd => hd.Add("Authorization", "Bearer " + model.AccessToken),
rtnVal => vmodel.Resources = rtnVal,
fault => _logger.LogError("CallResources Error: " + fault.ReasonPhrase),
ex => _logger.LogError("CallResources Error: " + ex)).Wait();

点击Logout为注销登陆:

                             //访问授权服务器,注销登陆
return Redirect(OAuthConstants.AuthorizationServerBaseAddress + OAuthConstants.LogoutPath + "?"
+ "logoutId=" + OAuthConstants.Clientid);

授权服务器的注销实现代码:

         /// <summary>
/// 注销登陆页面(因为账号的一些相关信息会存储在cookie中的)
/// </summary>
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
if (User.Identity.IsAuthenticated == false)
{
//如果用户并未授权过,那么返回
return await Logout(new LogoutViewModel { LogoutId = logoutId });
}
//显示注销提示, 这可以防止攻击, 如果用户签署了另一个恶意网页
var vm = new LogoutViewModel
{
LogoutId = logoutId
};
return View(vm);
} /// <summary>
/// 处理注销登陆
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
//清除Cookie中的授权信息
await HttpContext.Authentication.SignOutAsync();
//设置User使之呈现为匿名用户
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
Client logout = null;
if (model != null && !string.IsNullOrEmpty(model.LogoutId))
{
//获取Logout的相关信息
logout = await _clientStore.FindClientByIdAsync(model.LogoutId);
}
var vm = new LoggedOutViewModel
{
PostLogoutRedirectUri = logout?.PostLogoutRedirectUris?.FirstOrDefault(),
ClientName = logout?.ClientName,
};
return View("LoggedOut", vm);
}

注意

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

ASP.NET Core实现OAuth2.0的AuthorizationCode模式的更多相关文章

  1. ASP.NET Core实现OAuth2.0的ResourceOwnerPassword和ClientCredentials模式

    前言 开发授权服务框架一般使用OAuth2.0授权框架,而开发Webapi的授权更应该使用OAuth2.0授权标准,OAuth2.0授权框架文档说明参考:https://tools.ietf.org/ ...

  2. NET Core实现OAuth2.0的ResourceOwnerPassword和ClientCredentials模式

    NET Core实现OAuth2.0的ResourceOwnerPassword和ClientCredentials模式 前言 开发授权服务框架一般使用OAuth2.0授权框架,而开发Webapi的授 ...

  3. ASP.NET Core 1.1.0 Release Notes

    ASP.NET Core 1.1.0 Release Notes We are pleased to announce the release of ASP.NET Core 1.1.0! Antif ...

  4. Asp.net Core 1.0.1升级到Asp.net Core 1.1.0 Preview版本发布到Windows Server2008 R2 IIS中的各种坑

    Asp.net Core 1.0.1升级到Asp.net Core 1.1.0后,程序无法运行了 解决方案:在project.json中加入runtime节点 "runtimes" ...

  5. ASP.NET CORE MVC 2.0 项目中引用第三方DLL报错的解决办法 - InvalidOperationException: Cannot find compilation library location for package

    目前在学习ASP.NET CORE MVC中,今天看到微软在ASP.NET CORE MVC 2.0中又恢复了允许开发人员引用第三方DLL程序集的功能,感到甚是高兴!于是我急忙写了个Demo想试试,我 ...

  6. [翻译] ASP.NET Core 2.1.0 发布

    原文: ASP.NET Core 2.1.0 now available 今天,我们很高兴可以发布 ASP.NET Core 2.1.0!这是我们 .NET平台下开源的.跨平台的 Web 框架的最新版 ...

  7. ASP.NET WebApi 基于OAuth2.0实现Token签名认证

    一.课程介绍 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将是我们需要思考的问题.为了保护我们的WebApi数 ...

  8. 在IIS上部署Asp.Net Core 2.2.0

    1. .NET Core与Windows环境 Asp.Net Core 2.2.0 Windows 10 2. 先决条件   下载并安装.Net Core Hosting Bundle. 3. 部署过 ...

  9. asp.net core 从 3.0 到 3.1

    asp.net core 从 3.0 到 3.1 Intro 今天 .net core 3.1 正式发布了,.net core 3.1 正式版已发布,3.1 主要是对 3.0 的 bug 修复,以及一 ...

随机推荐

  1. jquery插件——点击交换元素位置(带动画效果)

    一.需求的诞生 在我们的网页或者web应用中,想要对列表中的元素进行位置调整(或者说排序)是一个常见的需求.实现方式大概就以下两种,一种是带有类似“上移”.“下移”的按钮,点击可与相邻元素交换位置,另 ...

  2. jQuery基础之选择器

    摘自:http://www.cnblogs.com/webmoon/p/3169360.html jQuery基础之选择器 选择器是jQuery的根基,在jQuery中,对事件处理.遍历DOM和Aja ...

  3. node.js module初步理解

    在开发一个复杂的应用程序的时候,我们需要把各个功能拆分.封装到不同的文件,在需要的时候引用该文件.没人会写一个几万行代码的文件,这样在可读性.复用性和维护性上都很差,几乎所有的编程语言都有自己的模块组 ...

  4. C# socket编程实践——支持广播的简单socket服务器

    在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# socket编程基本知识,写一个支持广播的简单server/clie ...

  5. C# Azure 存储-分布式缓存Redis的新建&配置&查看

    1. 介绍 Redis 是一款开源的,基于 BSD 许可的,高级键值 (key-value) 缓存 (cache) 和存储 (store) 系统.由于 Redis 的键包括 string,hash,l ...

  6. Java中MVC详解以及优缺点总结

     概念:  MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务数据.逻辑.界面显示分离的 ...

  7. EF架构~CodeFirst数据迁移与防数据库删除

    回到目录 本文介绍两个概念,防数据库自动删除,这是由于在code first模式下,当数据实体发生变化时,会对原来数据库进行删除,并将新数据表添加进来,但这对于我们的运营环境数据库,是万万不能接受的, ...

  8. Servlet开发技术,创建,以及Servlet的配置,web.xml的配置

    直接上图,不废话!!! 第一:首先在Eclipse的包资源管理器中,单机鼠标右键,在弹出的快捷键菜单中选择“新建”/Servlet命令,在弹出的对话框中输入新建的Servlet所在的包和类名,然后单击 ...

  9. 【原创】高性能网络编程(二):上一个10年,著名的C10K并发连接问题

    1.前言 对于高性能即时通讯技术(或者说互联网编程)比较关注的开发者,对C10K问题(即单机1万个并发连接问题)应该都有所了解."C10K"概念最早由Dan Kegel发布于其个人 ...

  10. 利用奇异值分解(SVD)简化数据

    特征值与特征向量 下面这部分内容摘自:强大的矩阵奇异值分解(SVD)及其应用 特征值分解和奇异值分解在机器学习领域都是属于满地可见的方法.两者有着很紧密的关系,在接下来会谈到,特征值分解和奇异值分解的 ...