快速接入 GitHub、QQ 第三方登入方式

一、GitHub 登入

1。1 註冊應用

進入 Github 的 Setting 頁面,點選 Developer settings,如圖所示:

快速接入 GitHub、QQ 第三方登入方式

進入後點擊 New Oauth App,如圖所示:

快速接入 GitHub、QQ 第三方登入方式

在其中填寫主頁 URL 和 回撥 URL,回撥 URL 尤為重要,如果不太明白可以先和我一致。

快速接入 GitHub、QQ 第三方登入方式

點選註冊後,上方會生成 Client ID 和 Client Secret,這兩個後面要用到。

快速接入 GitHub、QQ 第三方登入方式

1.2 HTML 頁面

頁面十分簡單,只有兩個跳轉連結:

<!DOCTYPE html>

lang

=

“en”

>

charset

=

“UTF-8”

>

</p><p>三方登入</p><p>

三方登入Demo

href

=

“/githubLogin”

>GitHub登入

href

=

“/qqLogin”

>QQ登入

1。3 Github 登入方法

在這個方法中,我們需要訪問 GitHub 的認證伺服器,使用 Get 請求,這裡使用重定向來實現。

遵循 Oauth 2。0 規範,需要攜帶以下引數:

response_type :對於授權碼模式,該值固定為code

client_id :註冊應用時的 Client ID

state :回撥時會原樣返回

redirect_uri : 回撥 URL,註冊應用時填寫的

這裡的 state 引數我要額外說明下,因為該引數會在後面的回撥 URL 中被原樣攜帶回來,絕大多數的開發者會忽略該欄位,阮一峰老師的文章也沒有著重提及這一點。但是

忽略該引數是會導致 CSRF攻擊的,在回撥函式中應當對該欄位進行校驗!

關於如何校驗,我一開始的想法是使用 session 來儲存 state 進行校驗的,但是我發現使用重定向後 session 不是同一個 session,方案一失敗。

然後我想透過 ajax 請求,在頁面中使用window。location。href 方法跳轉到認證伺服器,使用 session 儲存,但是很不幸這樣也不是同一個 session,方案二失敗。

最後我的解決辦法是使用 redis 快取,使用 set 儲存,回撥時判斷是否存在。當然你也可以用 HashMap 來儲存,這也是一個解決辦法。

關於 Redis,可以參考:https://jitwxs。cn/e331e26a。html

private

static

String

GITHUB_CLIENT_ID =

“0307dc634e4c5523cef2”

private

static

String

GITHUB_CLIENT_SECRET =

“707647176eb3bef1d4c2a50fcabf73e0401cc877”

private

static

String

GITHUB_REDIRECT_URL =

“http://127。0。0。1:8080/githubCallback”

@RequestMapping

“/githubLogin”

public

void

githubLogin(HttpServletResponse response) throws Exception {

// Github認證伺服器地址

String

url =

“https://github。com/login/oauth/authorize”

// 生成並儲存state,忽略該引數有可能導致CSRF攻擊

String

state = oauthService。genState();

// 傳遞引數response_type、client_id、state、redirect_uri

String

param =

“response_type=code&”

+

“client_id=”

+ GITHUB_CLIENT_ID +

“&state=”

+ state

+

“&redirect_uri=”

+ GITHUB_REDIRECT_URL;

// 1、請求Github認證伺服器

response。sendRedirect(url +

“?”

+ param);

}

1。4 Github 回撥方法

在上一步中,瀏覽器會被跳轉到 Github 的授權頁,當用戶登入並點選確認後,GitHub認證伺服器會跳轉到我們填寫的回撥URL中,我們在程式中處理回撥。

在回撥方法中,步驟如下:

1。 首先驗證 state 與傳送時是否一致,如果不一致,可能遭遇了 CSRF 攻擊。

2。 得到 code,向 GitHub 認證伺服器申請令牌(token)

這一步使用模擬的 POST 請求,攜帶引數包括:

grant_type :授權碼模式固定為authorization_code

code :上一步中得到的 code

redirect_uri :回撥URL

client_id :註冊應用時的Client ID

client_secret :註冊應用時的Client Secret

3。 得到令牌(access_token)和令牌型別(token_type),向GitHub資源伺服器獲取資源(以 user_info 為例)

這一步使用模擬的 GET 請求,攜帶引數包括:

access_token :令牌

token_type :令牌型別

4。 輸出結果

/**

* GitHub回撥方法

* @param code 授權碼

* @param state 應與傳送時一致

* @author jitwxs

* @since 2018/5/21 15:24

*/

@RequestMapping

“/githubCallback”

public

void

githubCallback(

String

code,

String

state, HttpServletResponse response) throws Exception {

// 驗證state,如果不一致,可能被CSRF攻擊

if

(!oauthService。checkState(state)) {

throw

new

Exception(

“State驗證失敗”

);

}

// 2、向GitHub認證伺服器申請令牌

String

url =

“https://github。com/login/oauth/access_token”

// 傳遞引數grant_type、code、redirect_uri、client_id

String

param =

“grant_type=authorization_code&code=”

+ code +

“&redirect_uri=”

+

GITHUB_REDIRECT_URL +

“&client_id=”

+ GITHUB_CLIENT_ID +

“&client_secret=”

+ GITHUB_CLIENT_SECRET;

// 申請令牌,注意此處為post請求

String

result = HttpClientUtils。sendPostRequest(url, param);

/*

* result示例:

* 失敗:error=incorrect_client_credentials&error_description=The+client_id+and%2For+client_secret+passed+are+incorrect。&

* error_uri=https%3A%2F%2Fdeveloper。github。com%2Fapps%2Fmanaging-oauth-apps%2Ftroubleshooting-oauth-app-access-token-request-errors%2F%23incorrect-client-credentials

* 成功:access_token=7c76186067e20d6309654c2bcc1545e41bac9c61&scope=&token_type=bearer

*/

Map<

String

String

> resultMap = HttpClientUtils。params2Map(result);

// 如果返回的map中包含error,表示失敗,錯誤原因儲存在error_description

if

(resultMap。containsKey(

“error”

)) {

throw

new

Exception(resultMap。get(

“error_description”

));

}

// 如果返回結果中包含access_token,表示成功

if

(!resultMap。containsKey(

“access_token”

)) {

throw

new

Exception(

“獲取token失敗”

);

}

// 得到token和token_type

String

accessToken = resultMap。get(

“access_token”

);

String

tokenType = resultMap。get(

“token_type”

);

// 3、向資源伺服器請求使用者資訊,攜帶access_token和tokenType

String

userUrl =

“https://api。github。com/user”

String

userParam =

“access_token=”

+ accessToken +

“&token_type=”

+ tokenType;

// 申請資源

String

userResult = HttpClientUtils。sendGetRequest(userUrl, userParam);

// 4、輸出使用者資訊

response。setContentType(

“text/html;charset=utf-8”

);

response。getWriter()。write(userResult);

}

二、QQ 登入

2。1 註冊應用

進入 QQ 互聯管理中心:https://connect。qq。com/manage。html,建立一個新應用(需要先稽核個人身份):

快速接入 GitHub、QQ 第三方登入方式

然後註冊應用資訊,和 GitHub 的步驟大差不差:

快速接入 GitHub、QQ 第三方登入方式

快速接入 GitHub、QQ 第三方登入方式

註冊後,可以看到應用的 APP ID、APP Key,以及你被允許的介面,當然只有一個獲取使用者資訊。

官方開發文件點選這裡:

http://wiki。connect。qq。com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side

注意:稽核狀態為稽核中和稽核失敗也是可以使用的,不用擔心(只是無法實際上線而已,作為 Demo 足夠了)。

快速接入 GitHub、QQ 第三方登入方式

2。2 QQ 登入方法

private

static

String

QQ_APP_ID =

“101474821”

private

static

String

QQ_APP_KEY =

“00d91cc7f636d71faac8629d559f9fee”

private

static

String

QQ_REDIRECT_URL =

“http://127。0。0。1:8080/qqCallback”

@RequestMapping

“/qqLogin”

public

void

qqLogin(HttpServletResponse response) throws Exception {

// QQ認證伺服器地址

String

url =

“https://graph。qq。com/oauth2。0/authorize”

// 生成並儲存state,忽略該引數有可能導致CSRF攻擊

String

state = oauthService。genState();

// 傳遞引數response_type、client_id、state、redirect_uri

String

param =

“response_type=code&”

+

“client_id=”

+ QQ_APP_ID +

“&state=”

+ state

+

“&redirect_uri=”

+ QQ_REDIRECT_URL;

// 1、請求QQ認證伺服器

response。sendRedirect(url +

“?”

+ param);

}

2。3 QQ 回撥方法

/**

* QQ回撥方法

* @param code 授權碼

* @param state 應與傳送時一致

* @author jitwxs

* @since 2018/5/21 15:24

*/

@RequestMapping

“/qqCallback”

public

void

qqCallback(

String

code,

String

state, HttpServletResponse response) throws Exception {

// 驗證state,如果不一致,可能被CSRF攻擊

if

(!oauthService。checkState(state)) {

throw

new

Exception(

“State驗證失敗”

);

}

// 2、向QQ認證伺服器申請令牌

String

url =

“https://graph。qq。com/oauth2。0/token”

// 傳遞引數grant_type、code、redirect_uri、client_id

String

param =

“grant_type=authorization_code&code=”

+ code +

“&redirect_uri=”

+

QQ_REDIRECT_URL +

“&client_id=”

+ QQ_APP_ID +

“&client_secret=”

+ QQ_APP_KEY;

// 申請令牌,注意此處為post請求

// QQ獲取到的access token具有3個月有效期,使用者再次登入時自動重新整理。

String

result = HttpClientUtils。sendPostRequest(url, param);

/*

* result示例:

* 成功:access_token=A24B37194E89A0DDF8DDFA7EF8D3E4F8&expires_in=7776000&refresh_token=BD36DADB0FE7B910B4C8BBE1A41F6783

*/

Map<

String

String

> resultMap = HttpClientUtils。params2Map(result);

// 如果返回結果中包含access_token,表示成功

if

(!resultMap。containsKey(

“access_token”

)) {

throw

new

Exception(

“獲取token失敗”

);

}

// 得到token

String

accessToken = resultMap。get(

“access_token”

);

// 3、使用Access Token來獲取使用者的OpenID

String

meUrl =

“https://graph。qq。com/oauth2。0/me”

String

meParams =

“access_token=”

+ accessToken;

String

meResult = HttpClientUtils。sendGetRequest(meUrl, meParams);

// 成功返回如下:callback( {“client_id”:“YOUR_APPID”,“openid”:“YOUR_OPENID”} );

// 取出openid

String

openid = getQQOpenid(meResult);

// 4、使用Access Token以及OpenID來訪問和修改使用者資料

String

userInfoUrl =

“https://graph。qq。com/user/get_user_info”

String

userInfoParam =

“access_token=”

+ accessToken +

“&oauth_consumer_key=”

+ QQ_APP_ID +

“&openid=”

+ openid;

String

userInfo = HttpClientUtils。sendGetRequest(userInfoUrl, userInfoParam);

// 5、輸出使用者資訊

response。setContentType(

“text/html;charset=utf-8”

);

response。getWriter()。write(userInfo);

}

/**

* 提取Openid

* @param str 形如:callback( {“client_id”:“YOUR_APPID”,“openid”:“YOUR_OPENID”} );

* @author jitwxs

* @since 2018/5/22 21:37

*/

private

String

getQQOpenid(

String

str) {

// 獲取花括號內串

String

json = str。substring(str。indexOf(

“{”

), str。indexOf(

“}”

) +

1

);

// 轉為Map

Map<

String

String

> map = JsonUtils。jsonToPojo(json, Map。class);

return

map。get(

“openid”

);

}