一、GitHub 登入
1。1 註冊應用
進入 Github 的 Setting 頁面,點選 Developer settings,如圖所示:
進入後點擊 New Oauth App,如圖所示:
在其中填寫主頁 URL 和 回撥 URL,回撥 URL 尤為重要,如果不太明白可以先和我一致。
點選註冊後,上方會生成 Client ID 和 Client Secret,這兩個後面要用到。
1.2 HTML 頁面
頁面十分簡單,只有兩個跳轉連結:
<!DOCTYPE html>
lang
=
“en”
>
charset
=
“UTF-8”
>
三方登入
三方登入Demo
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 的步驟大差不差:
註冊後,可以看到應用的 APP ID、APP Key,以及你被允許的介面,當然只有一個獲取使用者資訊。
官方開發文件點選這裡:
http://wiki。connect。qq。com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side
注意:稽核狀態為稽核中和稽核失敗也是可以使用的,不用擔心(只是無法實際上線而已,作為 Demo 足夠了)。
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”
);
}