一個適合新手的開源項目(asp.net core)

一個適合新手的開源項目(asp.net core)

  • 項目地址 AspNet.Security.OAuth.Providers

  • 環境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,微信等不過不多,我本人則是爲該庫貢獻了一個碼雲GiteeOAuth2.0提供程序。如果庫裏沒有你想要的提供程序,那麼你就可以提Issues,或則是直接fork下來自己添加然後提PR了。

爲什麼說這個項目適合新人入手呢?想必很多人都想自己有參與開源項目的經歷或者經驗,但是有時候找到一個適合自己的開源項目真的不太容易,很多時候都不知道從何入手,想修修bug連bug都找不到,加功能連人家項目業務都不懂。

而今天介紹的這個庫因爲只是一個網站OAuth2.0授權流程的提供程序,所以加功能是很明確的,我就是要加某某網站的OAuth2.0授權流程。而且我們可以參考其他大牛已經貢獻的其他OAuth2.0,學着來自己弄一個並通過測試。

項目結構

在這裏插入圖片描述

上圖就是該開源項目的目錄結構了,項目結構非常清晰的src文件夾裏面有很多的項目,每個項目就對應着一個OAuth2.0提供器,百度的提供器也在裏面。跟src同級的有一個test的文件夾是放項目自動化測試的,寫完自己的授權提供器要添加自己寫的測試代碼哦,否則PR的時候作者也會要你改的…

項目最後會被打包成nuget包被作者發佈到Nuget上面去。使用的時候只需要在asp.net coreStartup.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 coreClaimsPrincipal中,方法第一個參數是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吧。


個人公衆號,歡迎關注。天天更新是不可能的,這輩子都不可能天天更新。只有心情好的時候更新一下這樣子才維持的了生活。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章