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