OAuth2.0實(shí)戰(zhàn):ASP.NET Core接入第三方登錄

新年新氣象,趁著新年的喜慶,肝了十來天,終于發(fā)了第一版,希望大家喜歡。
如果有不喜歡看文字的童鞋,可以直接看下面的地址體驗(yàn)一下:
Github: https://github.com/mrhuo/MrHuo.OAuth 唯一官網(wǎng):https://oauthlogin.net
前言
此次帶來的這個(gè)小項(xiàng)目是 OAuth2 登錄組件,看到 Java 平臺(tái) JustAuth 項(xiàng)目很方便的接入第三方平臺(tái)登錄,心里癢癢啊,搜了一大圈,發(fā)現(xiàn)我大 .netcore 能用的可說是少之又少,而且代碼寫得一塌糊涂,全在一個(gè)庫(kù)里,代碼風(fēng)格也看不慣,所以下定決定,操起鍵盤,開干。
關(guān)于 OAuth2 的一些基礎(chǔ)、原理介紹文章太多了,寫的好的不在少數(shù),在頁(yè)尾我提供了幾個(gè)鏈接,喜歡的朋友看一下,這里就不深入解釋,直入主題。
如何使用
這里拿接入 github 登錄做演示,新建 Asp.NET Core Web應(yīng)用程序 項(xiàng)目,名叫 GithubLogin(PS:你可以自己起個(gè)和更牛×的名字),選擇模型視圖控制器這個(gè),當(dāng)然你可以選擇其他的。
第一步:安裝
安裝這個(gè) nuget 包:
Install-Package?MrHuo.OAuth.Github?-Version?1.0.0
第二步:配置
打開 appsettings.json 寫入下面的配置:
{
??"oauth":?{
????"github":?{
??????"app_id":?"github_app_id",
??????"app_key":?"github_app_key",
??????"redirect_uri":?"https://oauthlogin.net/oauth/githubcallback",
??????"scope":?"repo"
????}
??}
}
這里的配置可以通過 https://github.com/settings/applications/new 來注冊(cè),redirect_uri 可以填寫本地 localhost 地址的,超級(jí)方便,這也是為什么使用 github 登錄做演示的原因。

創(chuàng)建完成后,在這個(gè)界面里生成 client secret:

輸入密碼,生成成功后是這樣的:

把界面里的 Client ID,Client secret,連同上一個(gè)界面里填寫的 Authorization callback URL 全部填寫到配置文件對(duì)應(yīng)位置。現(xiàn)在配置文件 appsettings.json 是這樣的:
{
??"Logging":?{
????"LogLevel":?{
??????"Default":?"Information",
??????"Microsoft":?"Warning",
??????"Microsoft.Hosting.Lifetime":?"Information"
????}
??},
??"AllowedHosts":?"*",
??"oauth":?{
????"github":?{
??????"app_id":?"c95fxxxxxx0d09",
??????"app_key":?"c6a73xxxxxx6375",
??????"redirect_uri":?"http://localhost:5000/oauth/githubcallback",
??????"scope":?"repo"
????}
??}
}
下面的
scope暫且不管他,你想深入了解它的作用的話,后面再說。
第三步:寫代碼
在 Startup.cs 文件中注入組件:
//?This?method?gets?called?by?the?runtime.?Use?this?method?to?add?services?to?the?container.
public?void?ConfigureServices(IServiceCollection?services)
{
????services.AddControllersWithViews();
????services.AddSingleton(new?GithubOAuth(OAuthConfig.LoadFrom(Configuration,?"oauth:github")));
}
文件中其他代碼沒有修改,只加了這一行而已。
新建一個(gè) OAuthController 類,代碼如下:
using?System.Threading.Tasks;
using?Microsoft.AspNetCore.Mvc;
using?MrHuo.OAuth.Github;
namespace?GithubLogin.Controllers
{
????public?class?OAuthController:?Controller
????{
????????[HttpGet("oauth/github")]
????????public?IActionResult?Github([FromServices]?GithubOAuth?githubOAuth)
????????{
????????????return?Redirect(githubOAuth.GetAuthorizeUrl());
????????}
????????[HttpGet("oauth/githubcallback")]
????????public?async?Task?GithubCallback(
????????????[FromServices]?GithubOAuth?githubOAuth,
????????????[FromQuery]?string?code)
????????{
????????????return?Json(await?githubOAuth.AuthorizeCallback(code));
????????}
????}
}
你沒看錯(cuò),就這點(diǎn)代碼就好了。我們來運(yùn)行一下試試:

項(xiàng)目運(yùn)行之后,在地址欄里輸入下面這個(gè)地址:http://localhost:5000/oauth/github,因?yàn)槲覀儧]有修改任何代碼,沒有在視圖上做任何鏈接,所以就勞煩手動(dòng)啦~~
回車之后,順利跳轉(zhuǎn)到 github 授權(quán):

點(diǎn)擊綠色的 Authorize 按鈕之后稍等片刻,你會(huì)看到下面這個(gè)結(jié)果:

順利拿到了用戶信息(PS:請(qǐng)忽略我少的可憐的粉絲,曾經(jīng)我不強(qiáng)求 --ToT)

好了,到這里我的表演結(jié)束了,可以看到接入流程非常流暢,卡人主要是在申請(qǐng)這些步驟。下面講講原理之類的,隨便說一些...如果覺得我啰嗦,那么就不用往下看了,因?yàn)橄旅嫖視?huì)更啰嗦。
當(dāng)然,除了 github 現(xiàn)在已經(jīng)接入了12個(gè)平臺(tái),其中 QQ 和抖音我沒有注冊(cè)到應(yīng)用,無法測(cè)試,所以暫時(shí)沒有 nuget 包,一個(gè)人的力量總是有限的,在這里我請(qǐng)求各位有閑時(shí)間或者有 appid 資源的大佬,為這個(gè)小項(xiàng)目做一些貢獻(xiàn),是她走的遠(yuǎn)一些。

更多的 nuget 包,進(jìn)這里 https://www.nuget.org/profiles/mrhuo 或者在 VS nuget 包管理器里搜索 MrHuo.OAuth,就可以了。
請(qǐng)忽略
nuget上其他幾個(gè)垃圾包,那是很多年很多年以前寫的,舍不得刪。
開發(fā)背景
第三方平臺(tái)登錄說白了就是實(shí)現(xiàn) OAuth2 協(xié)議,很多平臺(tái)比如支付寶、百度、github、微軟,甚至是抖音、快手很多平臺(tái)都提供了開放接口。但是,很多平臺(tái)會(huì)在這個(gè)標(biāo)準(zhǔn)協(xié)議的基礎(chǔ)上增加、修改一些東西,比如:標(biāo)準(zhǔn)協(xié)議里,獲取 authorize code 時(shí)應(yīng)提供 client_id,微信公眾平臺(tái)非要把它改成 appid。再比如:獲取用戶信息時(shí),只需要 access_token 參數(shù),微信公眾平臺(tái)這邊非要提供一個(gè) openid,當(dāng)然這是在所難免的,因?yàn)楦鱾€(gè)平臺(tái)實(shí)際業(yè)務(wù)還是千差萬別,無法做到完全的統(tǒng)一,那這就給我們開發(fā)者帶來一個(gè)困擾,開發(fā)第三方登錄時(shí)很困難,當(dāng)然,開發(fā)一兩個(gè)也無所謂,要是多了呢?
假如有這么一個(gè)產(chǎn)品經(jīng)理,他想接入很多的登錄方式,讓使用者無論使用哪種平臺(tái),都能在這里順利登錄,找到回家的路呢(PS:產(chǎn)品經(jīng)理你別跑,看我40米的大刀)。

無疑,給我們一個(gè)考驗(yàn),如何做到一個(gè)標(biāo)準(zhǔn)化,可配置,可擴(kuò)展呢?這就是一個(gè)需要深究的問題。下面我就說說我肝這個(gè)項(xiàng)目的一些想法,說的不好別噴我,我還年輕(PS:三十多歲老大叔別裝嫩),還要臉......

制定標(biāo)準(zhǔn)
看了很多文檔之后,我們會(huì)發(fā)現(xiàn),萬變不離其宗,總有規(guī)律可循,總的來說,有下面3個(gè)步驟:
GetAuthorizeUrl
這一步通過 client_id,redirect_uri 等幾個(gè)參數(shù)來獲取授權(quán) url,跳轉(zhuǎn)到這個(gè) url 之后將在第三方平臺(tái)上完成登錄,完成登錄之后會(huì)跳轉(zhuǎn)到上面提供的 redirect_uri 這個(gè)地址,并且?guī)弦粋€(gè) code 參數(shù)。
GetAccessToken
這一步里,拿到上面的 code 之后去第三方平臺(tái)換 access_token。
GetUserInfo
這一步并非必須,但是我們既然是做第三方登錄,登錄之后還是需要和自己平臺(tái)的一些業(yè)務(wù)綁定用戶賬號(hào),或者使用現(xiàn)有信息注冊(cè)一個(gè)用戶,這個(gè)方法就顯得尤為重要了。
到此,就這3個(gè)步驟,我覺得是需要制定在標(biāo)準(zhǔn)里面的,所以我就寫了下面這個(gè)接口來規(guī)范它:
///?
///?OAuth?登錄?API?接口規(guī)范
///?
public?interface?IOAuthLoginApi<TAccessTokenModel,?TUserInfoModel>
????where?TAccessTokenModel?:?IAccessTokenModel
????where?TUserInfoModel?:?IUserInfoModel
{
????///?
????///?獲取跳轉(zhuǎn)授權(quán)的?URL
????///?
????///?
????///?
????string?GetAuthorizeUrl(string?state?=?"");
????///?
????///?異步獲取?AccessToken
????///?
????///?
????///?
????///?
????Task?GetAccessTokenAsync(string?code,?string?state?=?"") ;
????///?
????///?異步獲取用戶詳細(xì)信息
????///?
????///?
????///?
????Task?GetUserInfoAsync(TAccessTokenModel?accessTokenModel) ;
}
可以看到我將 AccessToken 和 UserInfo 做成了泛型參數(shù),因?yàn)樗麄兪沁@個(gè)規(guī)范里的可變部分。代碼中 state 參數(shù)的作用呢就是為了防止 CORS 攻擊做的防偽驗(yàn)證,這里暫不做解釋,其他文檔里都有這個(gè)參數(shù)的解釋。

如何擴(kuò)展新的平臺(tái)
這里拿 Gitee 來做演示:
第一步:找平臺(tái)對(duì)應(yīng) OAuth 文檔,找到獲取用戶信息接口返回JSON,轉(zhuǎn)換為 C# 實(shí)體類。如下:
根據(jù)自己需要和接口標(biāo)準(zhǔn),擴(kuò)展用戶屬性
public?class?GiteeUserModel?:?IUserInfoModel
{
????[JsonPropertyName("name")]
????public?string?Name?{?get;?set;?}
????[JsonPropertyName("avatar_url")]
????public?string?Avatar?{?get;?set;?}
????[JsonPropertyName("message")]
????public?string?ErrorMessage?{?get;?set;?}
????[JsonPropertyName("email")]
????public?string?Email?{?get;?set;?}
????[JsonPropertyName("blog")]
????public?string?Blog?{?get;?set;?}
????//...其他屬性類似如上
}
這里使用了
.netcore內(nèi)置的Json序列化庫(kù),據(jù)說性能提高了不少!
第二步:寫對(duì)應(yīng)平臺(tái)的授權(quán)接口
///?
///?https://gitee.com/api/v5/oauth_doc#/
///?
public?class?GiteeOAuth?:?OAuthLoginBase<GiteeUserModel>
{
????public?GiteeOAuth(OAuthConfig?oauthConfig)?:?base(oauthConfig)?{?}
????protected?override?string?AuthorizeUrl?=>?"https://gitee.com/oauth/authorize";
????protected?override?string?AccessTokenUrl?=>?"https://gitee.com/oauth/token";
????protected?override?string?UserInfoUrl?=>?"https://gitee.com/api/v5/user";
}
加上注釋,總共十行,如你所見,非常方便。如果該平臺(tái)協(xié)議遵循 OAuth2 標(biāo)準(zhǔn)開發(fā),那么就這么幾行就好了。
當(dāng)然,如果不按規(guī)矩自定義字段的平臺(tái),也可以擴(kuò)展,比如微信公眾平臺(tái)。
WechatAccessTokenModel.cs AccessToken 類擴(kuò)展
namespace?MrHuo.OAuth.Wechat
{
????public?class?WechatAccessTokenModel?:?DefaultAccessTokenModel
????{
????????[JsonPropertyName("openid")]
????????public?string?OpenId?{?get;?set;?}
????}
}
繼承自
DefaultAccessTokenModel,新增字段OpenId,因?yàn)楂@取用戶信息需要獲取OpenId,所以這里需要它。
WechatUserInfoModel.cs 用戶信息類
using?System.Collections.Generic;
using?System.Text.Json.Serialization;
namespace?MrHuo.OAuth.Wechat
{
????public?class?WechatUserInfoModel?:?IUserInfoModel
????{
????????[JsonPropertyName("nickname")]
????????public?string?Name?{?get;?set;?}
????????[JsonPropertyName("headimgurl")]
????????public?string?Avatar?{?get;?set;?}
????????[JsonPropertyName("language")]
????????public?string?Language?{?get;?set;?}
????????[JsonPropertyName("openid")]
????????public?string?Openid?{?get;?set;?}
????????[JsonPropertyName("sex")]
????????public?int?Sex?{?get;?set;?}
????????[JsonPropertyName("province")]
????????public?string?Province?{?get;?set;?}
????????[JsonPropertyName("city")]
????????public?string?City?{?get;?set;?}
????????[JsonPropertyName("country")]
????????public?string?Country?{?get;?set;?}
????????///?
????????///?用戶特權(quán)信息,json?數(shù)組,如微信沃卡用戶為(chinaunicom)
????????///?
????????[JsonPropertyName("privilege")]
????????public?List<string>?Privilege?{?get;?set;?}
????????[JsonPropertyName("unionid")]
????????public?string?UnionId?{?get;?set;?}
????????[JsonPropertyName("errmsg")]
????????public?string?ErrorMessage?{?get;?set;?}
????}
}
這里用戶信息字段上邊的
[JsonPropertyName("xxxx")]完全按照文檔里的字段寫,否則獲取不到正確的值。如果不需要太多的字段,自行刪減。
WechatOAuth.cs 核心類
using?System.Collections.Generic;
namespace?MrHuo.OAuth.Wechat
{
????///?
????///?Wechat OAuth 相關(guān)文檔參考:
????///? https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
????///?
????public?class?WechatOAuth?:?OAuthLoginBase<WechatAccessTokenModel,?WechatUserInfoModel>
????{
????????public?WechatOAuth(OAuthConfig?oauthConfig)?:?base(oauthConfig)?{?}
????????protected?override?string?AuthorizeUrl?=>?"https://open.weixin.qq.com/connect/oauth2/authorize";
????????protected?override?string?AccessTokenUrl?=>?"https://api.weixin.qq.com/sns/oauth2/access_token";
????????protected?override?string?UserInfoUrl?=>?"https://api.weixin.qq.com/sns/userinfo";
????????protected?override?Dictionary<string,?string>?BuildAuthorizeParams(string?state)
????????{
????????????return?new?Dictionary<string,?string>()
????????????{
????????????????["response_type"]?=?"code",
????????????????["appid"]?=?oauthConfig.AppId,
????????????????["redirect_uri"]?=?System.Web.HttpUtility.UrlEncode(oauthConfig.RedirectUri),
????????????????["scope"]?=?oauthConfig.Scope,
????????????????["state"]?=?state
????????????};
????????}
????????public?override?string?GetAuthorizeUrl(string?state?=?"")
????????{
????????????return?$"{base.GetAuthorizeUrl(state)}#wechat_redirect";
????????}
????????protected?override?Dictionary<string,?string>?BuildGetAccessTokenParams(Dictionary<string,?string>?authorizeCallbackParams)
????????{
????????????return?new?Dictionary<string,?string>()
????????????{
????????????????["grant_type"]?=?"authorization_code",
????????????????["appid"]?=?$"{oauthConfig.AppId}",
????????????????["secret"]?=?$"{oauthConfig.AppKey}",
????????????????["code"]?=?$"{authorizeCallbackParams["code"]}"
????????????};
????????}
????????protected?override?Dictionary<string,?string>?BuildGetUserInfoParams(WechatAccessTokenModel?accessTokenModel)
????????{
????????????return?new?Dictionary<string,?string>()
????????????{
????????????????["access_token"]?=?accessTokenModel.AccessToken,
????????????????["openid"]?=?accessTokenModel.OpenId,
????????????????["lang"]?=?"zh_CN",
????????????};
????????}
????}
}
乍一看好多內(nèi)容,懵了?先別懵,我一個(gè)一個(gè)來說一下:
protected?override?Dictionary<string,?string>?BuildAuthorizeParams(string?state)
{
????return?new?Dictionary<string,?string>()
????{
????????["response_type"]?=?"code",
????????["appid"]?=?oauthConfig.AppId,
????????["redirect_uri"]?=?System.Web.HttpUtility.UrlEncode(oauthConfig.RedirectUri),
????????["scope"]?=?oauthConfig.Scope,
????????["state"]?=?state
????};
}
細(xì)心的讀者發(fā)現(xiàn)了,這一段就是為了構(gòu)造 Authorize Url 時(shí)后邊的參數(shù)列表,返回一個(gè) Dictionary 即可,以為微信公眾號(hào)把 client_id 字段修改為 appid,所以這里需要處理一下。
public?override?string?GetAuthorizeUrl(string?state?=?"")
{
????return?$"{base.GetAuthorizeUrl(state)}#wechat_redirect";
}
這一段,在 Authorize Url 后邊綴了個(gè) #wechat_redirect,雖然不知道微信在這個(gè)參數(shù)上做了什么文章(PS:知道的朋友,言傳一下~~),但是他文檔里寫就給他寫上吧。
protected?override?Dictionary<string,?string>?BuildGetAccessTokenParams(Dictionary<string,?string>?authorizeCallbackParams)
{
????return?new?Dictionary<string,?string>()
????{
????????["grant_type"]?=?"authorization_code",
????????["appid"]?=?$"{oauthConfig.AppId}",
????????["secret"]?=?$"{oauthConfig.AppKey}",
????????["code"]?=?$"{authorizeCallbackParams["code"]}"
????};
}
同理,這一段是為了構(gòu)造 GetAccessToken 接口參數(shù)。
protected?override?Dictionary<string,?string>?BuildGetUserInfoParams(WechatAccessTokenModel?accessTokenModel)
{
????return?new?Dictionary<string,?string>()
????{
????????["access_token"]?=?accessTokenModel.AccessToken,
????????["openid"]?=?accessTokenModel.OpenId,
????????["lang"]?=?"zh_CN",
????};
}
同理,這一段是為了構(gòu)造 GetUserInfo 接口參數(shù)。
可以看到哈,這個(gè)框架本著自由、開放的原則,任何能自定義的地方,都可以自定義。還有我原本的出發(fā)點(diǎn),并非只針對(duì) OAuth 登錄這一個(gè)方向,我想把他平臺(tái)里面提供的 API 全部接入進(jìn)來,因?yàn)閿U(kuò)展太容易了,但是吧,時(shí)間精力有限,再說人上了年紀(jì),過了30歲,腦袋就不怎么靈光了,所以機(jī)會(huì)留給年輕人。
加入貢獻(xiàn)
我期待更多的朋友能加入到這個(gè)項(xiàng)目中,貢獻(xiàn)代碼也好,貢獻(xiàn) appid 資源做測(cè)試也好,提供意見建議也好。如果你也感興趣,請(qǐng)聯(lián)系我。
如果覺得有用幫到你了,貢獻(xiàn)一顆幼兒園之星 ?,點(diǎn)個(gè)關(guān)注,fork 走一波~~(PS: 手動(dòng)調(diào)皮)
相關(guān)文檔:
OAuth2:https://oauth.net/2/ rfc6749:https://tools.ietf.org/html/rfc6749 ruanyifeng:http://www.ruanyifeng.com/blog/2019/04/github-oauth.html


臥槽又來一個(gè)神器,可以查看微信朋友圈訪客記錄!

副業(yè)剛需,個(gè)人開發(fā)者如何通過小程序變現(xiàn)?已經(jīng)有朋友變現(xiàn)月入4k了!
