文章目录

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. jQuery 源码学习 - 02 - jQuery.fn.extend 与 jQuery.extend

    参考资料:[深入浅出jQuery]源码浅析--整体架构,备用地址:chokcoco/jQuery-. extend 方法在 jQuery 中是一个很重要的方法.jQuery 内部用它来拓展静态方法或者 ...

  2. 重启aliyun esc 需要重新启动redis

    /export/sorftware ./redis-server ../redis.conf redis-server  配置路径 如redis-server  /etc/redis.conf 不是后 ...

  3. JavaScript中的document.fullscreenEnabled

    本文主要讲述了: 什么是document.fullscreenEnabled 作用 兼容性 正文 什么是document.fullscreenEnabled document.fullscreenEn ...

  4. Proto3:编码

    本文介绍protocol buffer消息二进制传输格式.在应用程序中使用protocol buffer时,你并不需要了解这些,但它对你了解protocol buffer格式如何影响你的编码消息的大小 ...

  5. CentOS7安装Ceph

    CentOS 7 下安装Ceph-nautilus 本问主要记录在CentOS 7下如何安装Ceph-nautilus,安装过程中遇到的一些问题及解决方法. 实验准备 以下是本次实验所用到的机器(采用 ...

  6. Python——1变量和数据类型(内含其他知识点链接)

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

  7. Go语言基础篇(1) —— 编写第一个Go程序

    创建文件hello_world.go package main //包,表名代码所在的包 import "fmt" //引入依赖 //main方法 func main(){ fmt ...

  8. HTML常用表单标签

    1.表单元素 <form> HTML 表单用于收集用户输入. 代码示例: <form action="http://xxx.xxx.xxx/xxx.php" me ...

  9. DUBBO 面试灵魂18问

    一.Dubbo 是什么 dubbo 是一个分布式框架,是一个远程服务调用的分布式框架,其核心部分包含: 1)集群容错: 提供基于接口方法的透明远程过程调用,包含多协议支持,以及软负债均衡.失败容错.地 ...

  10. C# 存储相同键多个值的Dictionary

    涉及到两个问题: 一.访问磁盘中文件夹.文件夹下面的文件夹 先看一下磁盘文件夹结构 C盘下面有个根文件夹SaveFile,SaveFIle下面有两个子文件夹分别为,2018.2019, 子文件下201 ...