AspNetCore3.1_Secutiry源码解析_5_Authentication_OAuth
文章目录
- AspNetCore3.1_Secutiry源码解析_1_目录
- AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目
- AspNetCore3.1_Secutiry源码解析_3_Authentication_Cookies
- AspNetCore3.1_Secutiry源码解析_4_Authentication_JwtBear
- AspNetCore3.1_Secutiry源码解析_5_Authentication_OAuth
- AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect
- AspNetCore3.1_Secutiry源码解析_7_Authentication_其他
- AspNetCore3.1_Secutiry源码解析_8_Authorization_授权框架
OAuth简介
现在随便一个网站,不用注册,只用微信扫一扫,然后就可以自动登录,然后第三方网站右上角还出现了你的微信头像和昵称,怎么做到的?
用户->>x站点: 请求微信登录
x站点->>微信: 请求 oauth token
微信->>用户: x站点请求基本资料权限,是否同意?
用户->>微信: 同意
微信->>x站点: token
x站点->>微信: 请求user基本资料(token)
微信->微信: 校验token
微信->>x站点: user基本资料
大概就这么个意思,OAuth可以让第三方获取有限的授权去获取资源。
入门的看博客
https://www.cnblogs.com/linianhui/p/oauth2-authorization.html
英文好有基础的直接看协议
依赖注入
配置类:OAuthOptions
处理器类: OAuthHandler
public static class OAuthExtensions
{
public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions> configureOptions)
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, configureOptions);
public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions> configureOptions)
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, displayName, configureOptions);
public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions> configureOptions)
where TOptions : OAuthOptions, new()
where THandler : OAuthHandler<TOptions>
=> builder.AddOAuth<TOptions, THandler>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
where TOptions : OAuthOptions, new()
where THandler : OAuthHandler<TOptions>
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
}
}
OAuthOptions - 配置类
class OAuthOptions{
ClientId
ClientSecret
AuthorizationEndpoint
TokenEndPoint
UserInformationEndPoint
Scope
Events
ClaimActions
StateDataFormat
}
class RemoteAuthenticationOptions{
BackchannelTimeout
BackchannelHttpHandler
Backchannel
DataProtectionProvider
CallbackPath
AccessDeniedPath
ReturnUrlParameter
SignInScheme
RemoteAuthenticationTimeout
SaveTokens
}
class AuthenticationSchemeOptions{
}
OAuthOptions-->RemoteAuthenticationOptions
RemoteAuthenticationOptions-->AuthenticationSchemeOptions
下面是校验逻辑,这些配置是必需的。
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(ClientId))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientId)), nameof(ClientId));
}
if (string.IsNullOrEmpty(ClientSecret))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientSecret)), nameof(ClientSecret));
}
if (string.IsNullOrEmpty(AuthorizationEndpoint))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AuthorizationEndpoint)), nameof(AuthorizationEndpoint));
}
if (string.IsNullOrEmpty(TokenEndpoint))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(TokenEndpoint)), nameof(TokenEndpoint));
}
if (!CallbackPath.HasValue)
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(CallbackPath)), nameof(CallbackPath));
}
}
OAuthPostConfigureOptions - 配置处理
- DataProtectionProvider没有配置的话则使用默认实现
- Backchannel没有配置的话则处理构造默认配置
- StateDataFormat没有配置的话则使用PropertiesDataFormat
public void PostConfigure(string name, TOptions options)
{
options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
if (options.Backchannel == null)
{
options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth handler");
options.Backchannel.Timeout = options.BackchannelTimeout;
options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
if (options.StateDataFormat == null)
{
var dataProtector = options.DataProtectionProvider.CreateProtector(
typeof(THandler).FullName, name, "v1");
options.StateDataFormat = new PropertiesDataFormat(dataProtector);
}
}
这个StateDataFormat就是处理state字段的加密解密的,state在认证过程中用于防止跨站伪造攻击和存放一些状态信息,我们看一下协议的定义
state
RECOMMENDED. An opaque value used by the client to maintain
state between the request and callback. The authorization
server includes this value when redirecting the user-agent back
to the client. The parameter SHOULD be used for preventing
cross-site request forgery as described in Section 10.12.
比如,认证之后的回跳地址就是存放在这里。所以如果希望从state字段中解密得到信息的话,就需要使用到PropertiesDataFormat。PropertiesDataFormat没有任何代码,继承自SecureDataFormat。 为什么这里介绍这么多呢,因为实际项目中用到过这个。
public class SecureDataFormat<TData> : ISecureDataFormat<TData>
{
private readonly IDataSerializer<TData> _serializer;
private readonly IDataProtector _protector;
public SecureDataFormat(IDataSerializer<TData> serializer, IDataProtector protector)
{
_serializer = serializer;
_protector = protector;
}
public string Protect(TData data)
{
return Protect(data, purpose: null);
}
public string Protect(TData data, string purpose)
{
var userData = _serializer.Serialize(data);
var protector = _protector;
if (!string.IsNullOrEmpty(purpose))
{
protector = protector.CreateProtector(purpose);
}
var protectedData = protector.Protect(userData);
return Base64UrlTextEncoder.Encode(protectedData);
}
public TData Unprotect(string protectedText)
{
return Unprotect(protectedText, purpose: null);
}
public TData Unprotect(string protectedText, string purpose)
{
try
{
if (protectedText == null)
{
return default(TData);
}
var protectedData = Base64UrlTextEncoder.Decode(protectedText);
if (protectedData == null)
{
return default(TData);
}
var protector = _protector;
if (!string.IsNullOrEmpty(purpose))
{
protector = protector.CreateProtector(purpose);
}
var userData = protector.Unprotect(protectedData);
if (userData == null)
{
return default(TData);
}
return _serializer.Deserialize(userData);
}
catch
{
// TODO trace exception, but do not leak other information
return default(TData);
}
}
}
AddRemoteSchema和AddShema的差别就是做了下面的处理,确认始终有不是远程schema的SignInSchema
private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
{
private readonly AuthenticationOptions _authOptions;
public EnsureSignInScheme(IOptions<AuthenticationOptions> authOptions)
{
_authOptions = authOptions.Value;
}
public void PostConfigure(string name, TOptions options)
{
options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
}
}
OAuthHandler
- 解密state
- 校验CorrelationId,防跨站伪造攻击
- 如果error不为空说明失败返回错误
- 拿到授权码code,换取token
- 如果SaveTokens设置为true,将access_token,refresh_token,token_type存放到properties中
- 创建凭据,返回成功
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
var query = Request.Query;
var state = query["state"];
var properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return HandleRequestResult.Fail("The oauth state was missing or invalid.");
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties))
{
return HandleRequestResult.Fail("Correlation failed.", properties);
}
var error = query["error"];
if (!StringValues.IsNullOrEmpty(error))
{
// Note: access_denied errors are special protocol errors indicating the user didn't
// approve the authorization demand requested by the remote authorization server.
// Since it's a frequent scenario (that is not caused by incorrect configuration),
// denied errors are handled differently using HandleAccessDeniedErrorAsync().
// Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
if (StringValues.Equals(error, "access_denied"))
{
return await HandleAccessDeniedErrorAsync(properties);
}
var failureMessage = new StringBuilder();
failureMessage.Append(error);
var errorDescription = query["error_description"];
if (!StringValues.IsNullOrEmpty(errorDescription))
{
failureMessage.Append(";Description=").Append(errorDescription);
}
var errorUri = query["error_uri"];
if (!StringValues.IsNullOrEmpty(errorUri))
{
failureMessage.Append(";Uri=").Append(errorUri);
}
return HandleRequestResult.Fail(failureMessage.ToString(), properties);
}
var code = query["code"];
if (StringValues.IsNullOrEmpty(code))
{
return HandleRequestResult.Fail("Code was not found.", properties);
}
var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));
if (tokens.Error != null)
{
return HandleRequestResult.Fail(tokens.Error, properties);
}
if (string.IsNullOrEmpty(tokens.AccessToken))
{
return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
}
var identity = new ClaimsIdentity(ClaimsIssuer);
if (Options.SaveTokens)
{
var authTokens = new List<AuthenticationToken>();
authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
if (!string.IsNullOrEmpty(tokens.RefreshToken))
{
authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
}
if (!string.IsNullOrEmpty(tokens.TokenType))
{
authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
}
if (!string.IsNullOrEmpty(tokens.ExpiresIn))
{
int value;
if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
{
// https://www.w3.org/TR/xmlschema-2/#dateTime
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
authTokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
}
}
properties.StoreTokens(authTokens);
}
var ticket = await CreateTicketAsync(identity, properties, tokens);
if (ticket != null)
{
return HandleRequestResult.Success(ticket);
}
else
{
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
}
}
实战
最近做一个第三方对接的项目,我们有多个站点、自己的IdentityServer认证中心,这个联合项目要求将我们的系统以iframe的形式嵌套在他们的菜单里面。整个对接流程大致如下。
第三方->>第三方: 登录
第三方->>本公司系统: 点击菜单请求地址
本公司系统->>第三方: 跳转OAuth静默授权地址(1)
第三方->>本公司系统: 带授权码跳转回调地址(2)
本公司系统->>第三方: 使用code换token(3)
本公司系统->>第三方: 使用token读取个人资料(4)
本公司系统->>本公司系统: 用户名密码模式与本公司认证中心静默授权(5)
本公司系统->>本公司系统: 上下文注入需要的Claims,使用CookieSchema登录维持登录态(6)
本公司系统->>本公司系统: 回跳到开始授权时的地址(7)
利用微软框架,可以比较快速实现
定义XXOptions,继承自OAuthOptions
- ClientId:必填,客户端id
- ClientSecret:必填,客户端秘钥
- AuthorizationEndpoint:必填,授权地址,对应步骤(1)
- TokenEndpoint:必填,中间件会带着授权码code跳转到此地址换取token,对应步骤(2,3)
- UserInformationEndpoint:选填,用户信息接口地址,框架没有使用此属性,需要自己实现,对应步骤(4)
- CallbackPath:必填,授权流程结束之后回跳地址,对应步骤(7)
- 订阅事件:Events.OnCreatingTicket += async (OAuthCreatingTicketContext context) =>
{
//用户凭据签发时触发,将用户信息同步到本公司,使用ClientCredential模式与
//本公司IdentityServer认证中心通讯实现静默授权
//然后将本公司相关会话信息填充到凭据中
}; - SignInSchema:认证完后登入架构名(建议Cookies)
- 如果有特有的配置,也在此处定义
定义XXOAuthHandler,继承自OAuthHandler
- 重写ExchangeCodeAsync,此方法负责使用code换取token,父类实现使用的是form-post,如果任何地方与实际情况不匹配,可以进行重写
- 重写HandleChallengeAsync方法,此方法负责构建质询地址,即步骤(1)的静默授权地址+回调地址
- 重写CreateTicketAsync方法,此方法负责构建用户凭证,包括所有需要未来维持在Cookie中的信息。可以在此处请求UserInformationEndpoint请求用户资料,然后填充到凭证中。
- 重写HandleRemoteAuthenticateAsync:此方法为主干逻辑方法,如果与实际有差异可以进行重写,否则使用父类实现即可。
AspNetCore3.1_Secutiry源码解析_5_Authentication_OAuth的更多相关文章
- AspNetCore3.1_Secutiry源码解析_1_目录
文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目 AspNetCore3.1_ ...
- AspNetCore3.1_Secutiry源码解析_2_Authentication_核心对象
系列文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目 AspNetCore3. ...
- AspNetCore3.1_Secutiry源码解析_3_Authentication_Cookies
系列文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心流程 AspNetCore3. ...
- AspNetCore3.1_Secutiry源码解析_4_Authentication_JwtBear
title: "AspNetCore3.1_Secutiry源码解析_4_Authentication_JwtBear" date: 2020-03-22T16:29:29+08: ...
- AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect
title: "AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect" date: 2020-03-25T21:33: ...
- AspNetCore3.1_Secutiry源码解析_8_Authorization_授权框架
目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心流程 AspNetCore3.1_Se ...
- AspNetCore3.1源码解析_2_Hsts中间件
title: "AspNetCore3.1源码解析_2_Hsts中间件" date: 2020-03-16T12:40:46+08:00 draft: false --- 概述 在 ...
- AspNetCore3.1_Middleware源码解析_3_HttpsRedirection
概述 上文提到3.1版本默认没有使用Hsts,但是使用了这个中间件.看名字就很好理解,https跳转,顾名思义,就是跳转到 https地址. 使用场景,当用户使用http访问网站时,自动跳转到http ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
随机推荐
- Web窗体--控件
服务器基本控件:button: text属性linkbutton:text属性,它是一个超链接模样的普通buttonhyperlink: navigateurl:链接地址,相当于<a>标签 ...
- Uber自动驾驶卡车正式运营,是否会给卡车司机彻底“宣判死刑”?
随着科技的快速迭进和嬗变,大众的生活也在被全面革新和改变.不过有些改变是显性的,比如PC和智能手机的出现.有些改变却是隐性的,比如太空科技.云计算等.而在当下,又一个看似与大众生活紧密相关,但一般人却 ...
- Redis 原理
线程IO模型 Redis 是个单线程程序!(对外网络请求服务) 对于那些时间复杂度为 O(n) 级别的指令,一定要谨慎使用,一不小心就可能会导致 Redis 卡顿. 使用缓冲区,事件轮询 API(Li ...
- 算法基本概念及常用算法Python实现
基础算法概念: 时间复杂度 时间复杂度是从其增速的角度度量的, 时间复杂度一般用大O法表示. 递归 递归指的是调用自己的函数. 如果使用循环,程序性能可能更高: 如果使用递归,程序可能更容易理解. ...
- Dykin's blog
回归分析是一种很重要的预测建模技术.主要是研究自变量与因变量之间的因果关系.本文将会从数学角度与代码角度分析不同类型的回归.当你想预测连续型的非独立变量,或者对一系列独立变量或输入项有所反应时,就会使 ...
- ubuntu16.04安装mysql5.6
apt-get install software-properties-commonsudo add-apt-repository 'deb http://archive.ubuntu.com/ubu ...
- Python——9函数式编程②
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- ODI学习资料
ODI12.2.1.4入门指南:https://docs.oracle.com/en/middleware/fusion-middleware/data-integrator/12.2.1.4/ind ...
- Channel Estimation for High Speed Wireless Systems using Gaussian Particle Filter and Auxiliary Particle Filter
目录 论文来源 摘要 基本概念 1.时变信道 2.粒子滤波 3.高斯粒子滤波 4.辅助粒子滤波 比较 借鉴之处 论文来源 International Conference on Communicati ...
- git指令-工作区和暂存区
#git指令-工作区和暂存区 工作区(Working Directory):就是平常电脑可以看到的文件夹目录 版本库(Repository):存放git内容的文件夹例如: Git的版本库里存了很多东西 ...