一个适合新手的开源项目(asp.net core)
-
环境VS2019 或 vscode
dotnet core 3.1
-
知识点准备需要熟悉
OAuth2.0
协议流程和一些asp.net core
项目的基本知识
项目介绍
该项目是asp.net core
的一个关于OAuth2.0
的中间件库,如果对OAuth2.0
不是很熟悉的可以用搜索引擎先看看,网上有很多介绍的文章。简单来说我们平时看到的很多的第三方登陆就是用的OAuth2.0
。
而今天要推荐的一个开源项目就是一个在asp.net core
上提供不同网站的OAuth2.0
授权的中间件,有Github
,FaceBook
,Apple
等常见的一些网站,目前国外的网站比较多。当然库里面也有国内的,例如微博,QQ,微信等不过不多,我本人则是为该库贡献了一个码云Gitee
的OAuth2.0
提供程序。如果库里没有你想要的提供程序,那么你就可以提Issues,或则是直接fork下来自己添加然后提PR了。
为什么说这个项目适合新人入手呢?想必很多人都想自己有参与开源项目的经历或者经验,但是有时候找到一个适合自己的开源项目真的不太容易,很多时候都不知道从何入手,想修修bug连bug都找不到,加功能连人家项目业务都不懂。
而今天介绍的这个库因为只是一个网站OAuth2.0
授权流程的提供程序,所以加功能是很明确的,我就是要加某某网站的OAuth2.0
授权流程。而且我们可以参考其他大牛已经贡献的其他OAuth2.0
,学着来自己弄一个并通过测试。
项目结构
上图就是该开源项目的目录结构了,项目结构非常清晰的src
文件夹里面有很多的项目,每个项目就对应着一个OAuth2.0
提供器,百度的提供器也在里面。跟src
同级的有一个test
的文件夹是放项目自动化测试的,写完自己的授权提供器要添加自己写的测试代码哦,否则PR的时候作者也会要你改的…
项目最后会被打包成nuget包被作者发布到Nuget上面去。使用的时候只需要在asp.net core
的Startup.cs
里面简单配置下就可以了。
就是这么简单添加好后其他的登陆编程就跟微软文档上面的第三方登陆一样了,只要登陆时候选择不一样的提供器就会跳转到相应的网站的授权网址。
开始 - Gitee提供器
下面来看看Gitee
提供器里面有什么,新手如何去学着模仿写出自己的OAuth2.0
提供器。该提供器也是笔者贡献的,如有写的不好的地方可以直接指出,谢谢。
注意:模仿的过程中不要太过钻牛角尖,不要过于纠结细节,我们只需要面对现有接口完成开发就好了,代码写好,编译通过,测试通过就可以了你的提供器就能使用了。剩下的很多的asp.net core的技术细节就由框架来做了。
上图就是Gitee
提供器的代码文件了,只有5个代码文件,那我们来逐个看看都是些什么。
GiteeAuthenticationConstants.cs
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/
namespace AspNet.Security.OAuth.Gitee
{
/// <summary>
/// Contains constants specific to the <see cref="GiteeAuthenticationHandler"/>.
/// </summary>
public static class GiteeAuthenticationConstants
{
public static class Claims
{
public const string Name = "urn:gitee:name";
public const string Url = "urn:gitee:url";
}
}
}
可以看出这个文件是定义一些Claims
属性的。
项目中的每一个代码文件都需要有开源协议声明,没有的话直接提PR作者也是会叫你改的…
GiteeAuthenticationDefaults.cs
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
namespace AspNet.Security.OAuth.Gitee
{
/// <summary>
/// Default values used by the Gitee authentication middleware.
/// </summary>
public static class GiteeAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "Gitee";
/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public const string DisplayName = "Gitee";
/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public const string Issuer = "Gitee";
/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public const string CallbackPath = "/signin-gitee";
/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public const string AuthorizationEndpoint = "https://gitee.com/oauth/authorize";
/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public const string TokenEndpoint = "https://gitee.com/oauth/token";
/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public const string UserInformationEndpoint = "https://gitee.com/api/v5/user";
/// <summary>
/// Default value for <see cref="GiteeAuthenticationOptions.UserEmailsEndpoint"/>.
/// </summary>
public const string UserEmailsEndpoint = "https://gitee.com/api/v5/emails";
}
}
可以看到上面的代码就是一些该提供器基本的定义了,例如第一个AuthenticationScheme
的定义,熟悉asp.net core
的朋友都知道是认证方案的名称。我们也可以看到其他提供程序,基本前三个都是你的提供器名称就好了,这没有硬性规定,要知道在使用的时候认证方案名称要对应你的提供器就好了。
然后下面的开始就是授权登陆的回调地址,这个也是统一格式的/signin-{提供器的名称全小写}
,如果不确定可以看下其他提供器全部都是这样的形式。
接着下面开始的认证端口,Token端口,用户信息端口,用户邮箱端口这些就是需要我们看对应的提供商的OAuth2.0
的接口文档了。(有部分网站可能没有单独的用户邮箱端口,这时候就可以不定义用户邮箱端口)。
GiteeAuthenticationExtensions.cs
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/
using System;
using AspNet.Security.OAuth.Gitee;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods to add Gitee authentication capabilities to an HTTP application pipeline.
/// </summary>
public static class GiteeAuthenticationExtensions
{
/// <summary>
/// Adds <see cref="GiteeAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Gitee authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddGitee([NotNull] this AuthenticationBuilder builder)
{
return builder.AddGitee(GiteeAuthenticationDefaults.AuthenticationScheme, options => { });
}
/// <summary>
/// Adds <see cref="GiteeAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Gitee authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="configuration">The delegate used to configure the OpenID 2.0 options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddGitee(
[NotNull] this AuthenticationBuilder builder,
[NotNull] Action<GiteeAuthenticationOptions> configuration)
{
return builder.AddGitee(GiteeAuthenticationDefaults.AuthenticationScheme, configuration);
}
/// <summary>
/// Adds <see cref="GiteeAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Gitee authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Gitee options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddGitee(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[NotNull] Action<GiteeAuthenticationOptions> configuration)
{
return builder.AddGitee(scheme, GiteeAuthenticationDefaults.DisplayName, configuration);
}
/// <summary>
/// Adds <see cref="GiteeAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Gitee authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="caption">The optional display name associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Gitee options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddGitee(
[NotNull]this AuthenticationBuilder builder,
[NotNull] string scheme,
[CanBeNull] string caption,
[NotNull] Action<GiteeAuthenticationOptions> configuration)
{
return builder.AddOAuth<GiteeAuthenticationOptions, GiteeAuthenticationHandler>(scheme, caption, configuration);
}
}
}
这个代码文件应该从名字看都很清除了,是一个放扩展方法的静态类。该类就是我们使用的时候为什么可以使用AddGitee()
这样来添加提供器的原因了。
这个文件没什么好说的了,可以复制其他提供器的这个文件然后改一下名字就好了。完全一模一样的模板,不需要动脑子。
GiteeAuthenticationOptions.cs
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using static AspNet.Security.OAuth.Gitee.GiteeAuthenticationConstants;
namespace AspNet.Security.OAuth.Gitee
{
/// <summary>
/// Defines a set of options used by <see cref="GiteeAuthenticationHandler"/>.
/// </summary>
public class GiteeAuthenticationOptions : OAuthOptions
{
public GiteeAuthenticationOptions()
{
ClaimsIssuer = GiteeAuthenticationDefaults.Issuer;
CallbackPath = GiteeAuthenticationDefaults.CallbackPath;
AuthorizationEndpoint = GiteeAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = GiteeAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = GiteeAuthenticationDefaults.UserInformationEndpoint;
UserEmailsEndpoint = GiteeAuthenticationDefaults.UserEmailsEndpoint;
Scope.Add("user_info");
Scope.Add("emails");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
ClaimActions.MapJsonKey(Claims.Name, "name");
ClaimActions.MapJsonKey(Claims.Url, "url");
}
/// <summary>
/// Gets or sets the address of the endpoint exposing
/// the email addresses associated with the logged in user.
/// </summary>
public string UserEmailsEndpoint { get; set; }
}
}
上面的代码就是提供器的一些配置代码,这里都是一些预先配置的好的配置,例如回调路径,认真端口,令牌端口,用户信息端口,用户邮箱端口等,其中注意Scope.Add()
,该方法是让我们添加OAuth2.0
的请求Scope的,每个OAuth2.0
授权提供商都会有不一样的Scope,这是需要根据对应的文档来设置。再到下面的ClaimAcitons.MapJsonKey()
是把返回的用户信息映射到asp.net core
的ClaimsPrincipal
中,方法第一个参数是Claim类型,第二个参数是返回的Json的key,从而获得用户的相关信息。这部分内容也是需要我们根据提供商的OAuth2.0
文档用户信息端口返回的Json内容来完成映射的。
该类也是在我们使用的时候添加设置的那个设置类
.AddGoogle(options =>
{
options.ClientId = Configuration["Google:ClientId"];
options.ClientSecret = Configuration["Google:ClientSecret"];
})
也就是上面的这个options
。
GiteeAuthenticationHandler.cs
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AspNet.Security.OAuth.Gitee
{
public class GiteeAuthenticationHandler : OAuthHandler<GiteeAuthenticationOptions>
{
public GiteeAuthenticationHandler(
[NotNull] IOptionsMonitor<GiteeAuthenticationOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder,
[NotNull] ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticationTicket> CreateTicketAsync(
[NotNull] ClaimsIdentity identity,
[NotNull] AuthenticationProperties properties,
[NotNull] OAuthTokenResponse tokens)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
"returned a {Status} response with the following payload: {Headers} {Body}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync());
throw new HttpRequestException("An error occurred while retrieving the user profile.");
}
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
context.RunClaimActions();
// When the email address is not public, retrieve it from
// the emails endpoint if the user:email scope is specified.
if (!string.IsNullOrEmpty(Options.UserEmailsEndpoint) &&
!identity.HasClaim(claim => claim.Type == ClaimTypes.Email) &&
Options.Scope.Contains("emails"))
{
string address = await GetEmailAsync(tokens);
if (!string.IsNullOrEmpty(address))
{
identity.AddClaim(new Claim(ClaimTypes.Email, address, ClaimValueTypes.String, Options.ClaimsIssuer));
}
}
await Options.Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
protected async Task<string> GetEmailAsync([NotNull] OAuthTokenResponse tokens)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserEmailsEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogWarning("An error occurred while retrieving the email address associated with the logged in user: " +
"the remote server returned a {Status} response with the following payload: {Headers} {Body}.",
/* Status: */ response.StatusCode,
/* Headers: */ response.Headers.ToString(),
/* Body: */ await response.Content.ReadAsStringAsync());
throw new HttpRequestException("An error occurred while retrieving the email address associated to the user profile.");
}
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return (from address in payload.RootElement.EnumerateArray()
select address.GetString("email")).FirstOrDefault();
}
}
}
接下来就到了GiteeAuthenticationHandler.cs
这个文件了,该文件是整个OAuth2.0
的核心执行器,执行着asp.net core
应用和OAuth2.0
提供商之间完成OAuth2.0
的请求和响应过程。一开始我们可以直接复制其他提供器的Handler来改。
我们也可以看到CreateTicketAsync
函数其实只是请求用户信息端口来创建一个ClaimsPrincipal
的。在这里我们只需要根据提供商的接口文档返回的内容对应的创建一个ClaimsPrincipal
就完成了。所以其实我们并没有改OAuth2.0
的主要认真授权的流程,一个也是因为OAuth2.0
是一套标准的授权认证流程,所有的OAuth2.0
提供商也要遵循着这一套标准来提供接口,提供服务。
因为一般来说按照OAuth2.0
的标准授权认证的流程都是一样的,所以对应认证流程上我们不需要去做什么改动,除非某OAuth2.0
提供商的授权认真流程跟标准的不太一样或者多了某些认值参数这时候我们才要重写OAuthHandler<GiteeAuthenticationOptions>
中的部分方法来实现更灵活的认证。
最后
最后我们来总结一下,对这个开源项目贡献OAuth2.0
提供器的话其实大部分的都是一些可复制,机械性的代码。也只有Handler涉及到一点授权认证的逻辑代码编写。但是这却是一个代码清晰,每个提供器项目足够小,而且在这里我们可以看到国外,国内不同的开发者贡献的代码,有很多值得我们学习借鉴的东西。而且该项目是使用持续集成,和自动化测试发布的。而且也是微软文档上关于第三方登陆推荐的库。
总的来说这是一个新手都可以去试一试的库,大胆的提PR吧。
个人公众号,欢迎关注。天天更新是不可能的,这辈子都不可能天天更新。只有心情好的时候更新一下这样子才维持的了生活。