文章目录

OAuth简介

现在随便一个网站,不用注册,只用微信扫一扫,然后就可以自动登录,然后第三方网站右上角还出现了你的微信头像和昵称,怎么做到的?

sequenceDiagram
用户->>x站点: 请求微信登录
x站点->>微信: 请求 oauth token
微信->>用户: x站点请求基本资料权限,是否同意?
用户->>微信: 同意
微信->>x站点: token
x站点->>微信: 请求user基本资料(token)
微信->微信: 校验token
微信->>x站点: user基本资料

大概就这么个意思,OAuth可以让第三方获取有限的授权去获取资源。

入门的看博客

https://www.cnblogs.com/linianhui/p/oauth2-authorization.html

英文好有基础的直接看协议

https://tools.ietf.org/html/rfc6749

依赖注入

配置类: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 - 配置类

classDiagram
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 - 配置处理

  1. DataProtectionProvider没有配置的话则使用默认实现
  2. Backchannel没有配置的话则处理构造默认配置
  3. 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的形式嵌套在他们的菜单里面。整个对接流程大致如下。

sequenceDiagram
第三方->>第三方: 登录
第三方->>本公司系统: 点击菜单请求地址
本公司系统->>第三方: 跳转OAuth静默授权地址(1)
第三方->>本公司系统: 带授权码跳转回调地址(2)
本公司系统->>第三方: 使用code换token(3)
本公司系统->>第三方: 使用token读取个人资料(4)
本公司系统->>本公司系统: 用户名密码模式与本公司认证中心静默授权(5)
本公司系统->>本公司系统: 上下文注入需要的Claims,使用CookieSchema登录维持登录态(6)
本公司系统->>本公司系统: 回跳到开始授权时的地址(7)

利用微软框架,可以比较快速实现

  1. 定义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)
    • 如果有特有的配置,也在此处定义
  2. 定义XXOAuthHandler,继承自OAuthHandler

    • 重写ExchangeCodeAsync,此方法负责使用code换取token,父类实现使用的是form-post,如果任何地方与实际情况不匹配,可以进行重写
    • 重写HandleChallengeAsync方法,此方法负责构建质询地址,即步骤(1)的静默授权地址+回调地址
    • 重写CreateTicketAsync方法,此方法负责构建用户凭证,包括所有需要未来维持在Cookie中的信息。可以在此处请求UserInformationEndpoint请求用户资料,然后填充到凭证中。
    • 重写HandleRemoteAuthenticateAsync:此方法为主干逻辑方法,如果与实际有差异可以进行重写,否则使用父类实现即可。

AspNetCore3.1_Secutiry源码解析_5_Authentication_OAuth的更多相关文章

  1. AspNetCore3.1_Secutiry源码解析_1_目录

    文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目 AspNetCore3.1_ ...

  2. AspNetCore3.1_Secutiry源码解析_2_Authentication_核心对象

    系列文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目 AspNetCore3. ...

  3. AspNetCore3.1_Secutiry源码解析_3_Authentication_Cookies

    系列文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心流程 AspNetCore3. ...

  4. AspNetCore3.1_Secutiry源码解析_4_Authentication_JwtBear

    title: "AspNetCore3.1_Secutiry源码解析_4_Authentication_JwtBear" date: 2020-03-22T16:29:29+08: ...

  5. AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect

    title: "AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect" date: 2020-03-25T21:33: ...

  6. AspNetCore3.1_Secutiry源码解析_8_Authorization_授权框架

    目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心流程 AspNetCore3.1_Se ...

  7. AspNetCore3.1源码解析_2_Hsts中间件

    title: "AspNetCore3.1源码解析_2_Hsts中间件" date: 2020-03-16T12:40:46+08:00 draft: false --- 概述 在 ...

  8. AspNetCore3.1_Middleware源码解析_3_HttpsRedirection

    概述 上文提到3.1版本默认没有使用Hsts,但是使用了这个中间件.看名字就很好理解,https跳转,顾名思义,就是跳转到 https地址. 使用场景,当用户使用http访问网站时,自动跳转到http ...

  9. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

随机推荐

  1. MyBatis if test 传入一个数字进行比较报错 There is no getter for property named 'userState' in 'class java.lang.Integer'

    在写MyBatis映射文件中,我要传入一个 int 类型的参数,在映射文件中用 'test' 中进行比较,我花了很长时间百度,发现都是不靠谱的方法,有把数字在比较时转成字符串用 equals 比较的. ...

  2. java MVC 自定义类型转换器(Formatter、AnnotationFormatterFactory)

    下面一个事例,是将传入的一个身份证号,转换成一个对象(提取身份证号的地址.出身日期.性别等) 实体类 Person 有三个字段如下: String province; //地址 Date birthd ...

  3. 插入排序算法&算法分析

  4. MyBatis学习总结之一对一映射

    知识点:JavaType和ofType都是用来指定对象类型的,但是JavaType是用来指定pojo中属性的类型,而ofType指定的是映射到list集合属性中pojo的类型. 本次mybatis的练 ...

  5. yii批量数据插入

    yii框架批量插入数据有两种方法,第一种是循环多次插入和一次批量插入,第一种方法要注意插入数据中间有一次数据插入失败要注意回滚事务 循环插入数据 第一种方法 $model = new User(); ...

  6. Linux 信号介绍

    是内容受限时的一种异步通信机制 首先是用来通信的 是异步的 本质上是 int 型的数字编号,早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31 ...

  7. 网络字体反爬之pyspider爬取起点中文小说

    前几天跟同事聊到最近在看什么小说,想起之前看过一篇文章说的是网络十大水文,就想把起点上的小说信息爬一下,搞点可视化数据看看.这段时间正在看爬虫框架-pyspider,觉得这种网站用框架还是很方便的,所 ...

  8. C++走向远洋——59(项目三、图形面积、抽象类)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  9. openpyxl(python操作Excel)

    一.安装 >>> pip install openpyxl import openpyxl 二.常用操作 1.创建与保存一个工作簿 wb = openpyxl.Workbook() ...

  10. 通过python脚本读取多台虚机硬件信息

    主要通过fabric模块实现 import fabric ''' hosts = [] ,): host = "192.168.75." + str(i) hosts.append ...