1. 程式人生 > >SPA+.NET Core3.1 GitHub第三方授權登入 使用AspNet.Security.OAuth.GitHub

SPA+.NET Core3.1 GitHub第三方授權登入 使用AspNet.Security.OAuth.GitHub

GitHub第三方授權登入

使用SPA+.NET Core3.1實現 GitHub第三方授權登入 類似使用AspNet.Security.OAuth.GitHub,前端使用如下:VUE+Vue-Router+axios

AspNet.Security.OAuth.GitHub

  • GitHub https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers

GitHub授權登入

什麼配置的過程不說了。。有一推。

  • GitHub 第三方登入
  • 給你的網站新增第三方登入以及簡訊驗證功能

下面為示例

client_id:0be6b05fc717bfc4fb67
client_secret:dcaced9f176afba64e89d88b9b06ffc4a887a609

Get

https://github.com/login/oauth/authorize?client_id=0be6b05fc717bfc4fb67&redirect_uri=https://localhost:5001/signin-github

會重定向到

https://localhost:5001/signin-github?code=07537a84d12bbae08361

這個code放到下面的請求中,獲取access_token
POST方式(PostMan去請求)

https://github.com/login/oauth/access_token?client_id=0be6b05fc717bfc4fb67&client_secret=dcaced9f176afba64e89d88b9b06ffc4a887a609&code=07537a84d12bbae08361

Get方式

https://api.github.com/user?access_token=787506afa3271d077b98f18af56d7cfdc8db43b4

然後就能獲取使用者資訊

{
   "login": "luoyunchong",
   "id": 18613266,
   "node_id": "MDQ6VXNlcjE4NjEzMjY2",
   "avatar_url": "https://avatars1.githubusercontent.com/u/18613266?v=4",
   "gravatar_id": "",
   "url": "https://api.github.com/users/luoyunchong",
   "html_url": "https://github.com/luoyunchong",
   "followers_url": "https://api.github.com/users/luoyunchong/followers",
   "following_url": "https://api.github.com/users/luoyunchong/following{/other_user}",
   "gists_url": "https://api.github.com/users/luoyunchong/gists{/gist_id}",
   "starred_url": "https://api.github.com/users/luoyunchong/starred{/owner}{/repo}",
   "subscriptions_url": "https://api.github.com/users/luoyunchong/subscriptions",
   "organizations_url": "https://api.github.com/users/luoyunchong/orgs",
   "repos_url": "https://api.github.com/users/luoyunchong/repos",
   "events_url": "https://api.github.com/users/luoyunchong/events{/privacy}",
   "received_events_url": "https://api.github.com/users/luoyunchong/received_events",
   "type": "User",
   "site_admin": false,
   "name": "IGeekFan",
   "company": null,
   "blog": "https://blog.igeekfan.cn",
   "location": null,
   "email": "[email protected]",
   "hireable": null,
   "bio": "學習之路漫漫無期。",
   "public_repos": 14,
   "public_gists": 0,
   "followers": 16,
   "following": 11,
   "created_at": "2016-04-22T10:33:44Z",
   "updated_at": "2019-12-21T14:49:33Z"
}

.NET Core3.1

以下程式碼為主要程式碼,完整程式碼看下面的DEMO連結。

使用WebApi時,看了一些專案,全是基於MVC結構的,都不是我想要的。看了一些部落格上面介紹 ,步驟都是千篇一律,都是配合前後端分離的。

  • 前端執行在:http://localhost:8081
  • 後端執行在:https://localhost:5001

    前後端分離的SPA 配合第三方授權登入流程如下

本地測試時,gitHub回撥地址設定 http(s)://ip:埠/signin-github

  • 如: https://localhost:5001/signin-github。

1. 上面這個明明填寫的後端的地址,那後端怎麼把結果通知前端呢?

前端請求https://localhost:5001/signin?provider=GitHub&redirectUrl=http://localhost:8080/login-result

  • 提供引數provider為GitHub,
  • redirectUrl為GitHub授權登入後,回撥signin-github後,後端再去重定向的地址,這裡填前端的一個路由。

2. 後端只提供了signin,signin-callback路由,沒有signin-github,那github上配置的路由是怎麼回調回來呢?

google-登入,微軟文件,其中有一個更改預設回撥 URI,通過 AddGitHub中的CallbackPath屬性配置。

介紹了回撥地址應配置signin-google,所以這裡應該是signin-github,他是可以配置的,不需要自己寫程式處理signin-google這個路由,內部有中介軟體已經處理了。

3. 回撥到signin-github後,後端怎麼處理,才能讓前端重新整理。獲取登入後的資訊呢。

具體上面的根據code獲取access_token,根據access_token獲取使用者的資訊的過程,這些處理的過程,都不需要我們自己處理。我們可以用直接獲取使用者資訊。

一個方法SignIn,只要return Challenge(properties, provider);,

  • provider 為 GitHub,
  • properties var properties = new AuthenticationProperties { RedirectUri = url };

這個url為另一個獲取使用者資訊的路由,只要拼接好地址即可。

var authenticateResult = await _contextAccessor.HttpContext.AuthenticateAsync(provider);
string email = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value;
string name = authenticateResult.Principal.FindFirst(ClaimTypes.Name)?.Value;

需要注入

private readonly IHttpContextAccessor _contextAccessor;
public AuthenticationController( IHttpContextAccessor contextAccessor)
{
    _contextAccessor = contextAccessor;
}

程式碼部署(簡化)

開啟NuGet包管理,安裝包

Install-Package AspNet.Security.OAuth.GitHub

appSettings.json

"Authentication": {
    "GitHub": {
      "ClientId": "0be6b05fc717bfc4fb67",
      "ClientSecret": "dcaced9f176afba64e89d88b9b06ffc4a887a609"
    }
}

add擴充套件方法

public static class JwtConfiguration
{
    public static void AddJwtConfiguration(this IServiceCollection services, IConfiguration configuration)
    {

        services.AddAuthentication(opts =>
            {
                opts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddCookie(options =>
        {
            options.LoginPath = "/signin";
            options.LogoutPath = "/signout";
        }).AddGitHub(options =>
        {
            options.ClientId = configuration["Authentication:GitHub:ClientId"];
            options.ClientSecret = configuration["Authentication:GitHub:ClientSecret"];
        });
    }
}

預設情況下,如頭像,email,是沒有獲取的。

.AddGitHub(options =>
{
    options.ClientId = configuration["Authentication:GitHub:ClientId"];
    options.ClientSecret = configuration["Authentication:GitHub:ClientSecret"];
    //options.CallbackPath = new PathString("~/signin-github");//與GitHub上的回撥地址相同,預設即是/signin-github
    options.Scope.Add("user:email");
    //authenticateResult.Principal.FindFirst(LinConsts.Claims.AvatarUrl)?.Value;  得到GitHub頭像
    options.ClaimActions.MapJsonKey(LinConsts.Claims.AvatarUrl, "avatar_url");
    options.ClaimActions.MapJsonKey(LinConsts.Claims.BIO, "bio");
    options.ClaimActions.MapJsonKey(LinConsts.Claims.BlogAddress, "blog");
});

#其中LinConsts類為靜態常量
public static class LinConsts
{
    public static class Claims
    {
        public const string BIO = "urn:github:bio";
        public const string AvatarUrl = "urn:github:avatar_url";
        public const string BlogAddress = "urn:github:blog";
    }
}

startup.cs

ConfigureServices中配置此服務

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddJwtConfiguration(Configuration);

建立AuthenticationController.cs
增加SignIn,用於處理使用者授權成功後,重定回signin-callback,並將引數帶回。

        private readonly IHttpContextAccessor _contextAccessor;
        private readonly IConfiguration _configuration;

        public AuthenticationController(IHttpContextAccessor contextAccessor, IConfiguration configuration)
        {
            _contextAccessor = contextAccessor;
            _configuration = configuration;
        }
        
        [HttpGet("~/signin")]
        public async Task<IActionResult> SignIn(string provider, string redirectUrl)
        {
            var request = _contextAccessor.HttpContext.Request;
            var url =
                $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}-callback?provider={provider}&redirectUrl={redirectUrl}";
            var properties = new AuthenticationProperties { RedirectUri = url };
            properties.Items["LoginProviderKey"] = provider;
            return Challenge(properties, provider);

        }

在signin方法中,使用者點選授權後(第一次),會根據其傳遞的URL,重定向到這個地址,signin-callback,引數也會一同攜帶。provider為GitHub,redirectUrl為:http://localhost:8081/login-result.

[HttpGet("~/signin-callback")]
public async Task<IActionResult> Home(string provider = null, string redirectUrl = "")
{
    var authenticateResult = await _contextAccessor.HttpContext.AuthenticateAsync(provider);
    if (!authenticateResult.Succeeded) return Redirect(redirectUrl);
    var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier);
    if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value))
        return Redirect(redirectUrl);

    //TODO 記錄授權成功後的資訊 

    string email = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value;
    string name = authenticateResult.Principal.FindFirst(ClaimTypes.Name)?.Value;
    string gitHubName = authenticateResult.Principal.FindFirst(GitHubAuthenticationConstants.Claims.Name)?.Value;
    string gitHubUrl = authenticateResult.Principal.FindFirst(GitHubAuthenticationConstants.Claims.Url)?.Value;
    //startup 中 AddGitHub配置項  options.ClaimActions.MapJsonKey(LinConsts.Claims.AvatarUrl, "avatar_url");
    string avatarUrl = authenticateResult.Principal.FindFirst(LinConsts.Claims.AvatarUrl)?.Value;

    return Redirect($"{redirectUrl}?openId={openIdClaim.Value}");
}

這時候我們能獲取使用者資訊了。那麼前端怎麼辦呢。我們寫個方法,獲取使用者資訊,看看效果。

  • 瀏覽器直接開啟能得到github的id。
  • axios GET請求 https://localhost:5001/OpenId 得到null
[HttpGet("~/OpenId")]
public async Task<string> OpenId(string provider = null)
{
   var authenticateResult = await _contextAccessor.HttpContext.AuthenticateAsync(provider);
   if (!authenticateResult.Succeeded) return null;
   var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier);
   return openIdClaim?.Value;
}

我記得之前傳Token時,後臺是可以這樣獲取的。

[HttpGet("~/GetOpenIdByToken")]
public string GetOpenIdByToken()
{
    return User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

LoginResult.vue在created生命週期中。都是得到null

axios({
  methods: "get",
  url: "https://localhost:5001/OpenId?provider=GitHub"
})
  .then(function(response) {
    // handle success
    console.log(response);
  })

axios({
  methods: "get",
  url: "https://localhost:5001/GetOpenIdByToken"
})
  .then(function(response) {
    // handle success
    console.log(response);
  })

為什麼呢???

因為前後端分離,不是基於Cookies的。http是無狀態的。每次請求無法區分使用者的。我們可以根據當前的ClaimsPrincipal,根據JWT生成相應的Token,axios請求時,放到headers中。

安裝包

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

AppSettings.json配置改成

"Authentication": {
"JwtBearer": {
  "SecurityKey": "JWTStudyWebsite_DI20DXU3",
  "Issuer": "JWTStudy",
  "Audience": "JWTStudyWebsite"
},
"GitHub": {
  "ClientId": "0be6b05fc717bfc4fb67",
  "ClientSecret": "dcaced9f176afba64e89d88b9b06ffc4a887a609"
}
}

AddJwtConfiguration改成如下內容

public static void AddJwtConfiguration(this IServiceCollection services, IConfiguration configuration)
{

    services.AddAuthentication(opts =>
        {
            opts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddCookie(options =>
    {
        options.LoginPath = "/signin";
        options.LogoutPath = "/signout";
    }).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.Audience = configuration["Authentication:JwtBearer:Audience"];

        options.TokenValidationParameters = new TokenValidationParameters
        {
            // The signing key must match!
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.ASCII.GetBytes(configuration["Authentication:JwtBearer:SecurityKey"])),

            // Validate the JWT Issuer (iss) claim
            ValidateIssuer = true,
            ValidIssuer = configuration["Authentication:JwtBearer:Issuer"],

            // Validate the JWT Audience (aud) claim
            ValidateAudience = true,
            ValidAudience = configuration["Authentication:JwtBearer:Audience"],

            // Validate the token expiry
            ValidateLifetime = true,

            // If you want to allow a certain amount of clock drift, set that here
            //ClockSkew = TimeSpan.Zero
        };
    }).AddGitHub(options =>
    {
        options.ClientId = configuration["Authentication:GitHub:ClientId"];
        options.ClientSecret = configuration["Authentication:GitHub:ClientSecret"];
        //options.CallbackPath = new PathString("~/signin-github");//與GitHub上的回撥地址相同,預設即是/signin-github
        options.Scope.Add("user:email");
        //authenticateResult.Principal.FindFirst(LinConsts.Claims.AvatarUrl)?.Value;  得到GitHub頭像
        options.ClaimActions.MapJsonKey(LinConsts.Claims.AvatarUrl, "avatar_url");
        options.ClaimActions.MapJsonKey(LinConsts.Claims.BIO, "bio");
        options.ClaimActions.MapJsonKey(LinConsts.Claims.BlogAddress, "blog");
    });
}

前端LoginResult.vue程式碼

前端執行

yarn install
yarn serve

點選GitHub登入

GetOpenIdByToken根據生成的token值,解析出了使用者id,這樣前端在login-result這個元件中,把token儲存好,並重定向自己的主頁,獲取使用者所有資訊即可。

data: 18613266
status: 200
config: {url: "https://localhost:5001/GetOpenIdByToken"}

OpenId?provider=GitHub則得不到資料,只能瀏覽器直接請求https://localhost:5001/OpenId?provider=GitHub,才能到github 的id。這個適應於前後端不分離,或者屬於之前我們經常使用MVC結構,同一域名下,同一埠,基於Cookies登入的判斷。

參考

  • .net Core2.2 WebApi通過OAuth2.0實現微信登入
  • AspNetCore3.0 和 JWT
  • 使用者系統設計:第三方授權、賬號繫結及解綁(下)

Demo 示例

  • GitHub https://github.com/luoyunchong/dotnetcore-examples/tree/master/dotnetcore3.1/VoVo.AspNetCore.OAuth2