OAuth協議,是一種受權協議,不涉及具體的代碼,只是表示一種約定的流程和規範。OAuth協議通常用於用戶決定是否把本身在某個服務商上面的資源(好比:用戶基本資料、照片、視頻等)受權給第三方應用訪問。此外,OAuth2.0協議是OAuth協議的升級版,如今已經逐漸成爲單點登陸(SSO)和用戶受權的標準。php
不知道你們有沒有發現,目前主流的互聯網網站除了可使用「用戶名+密碼」模式和「手機號+驗證碼」模式登陸外,不少還提供了第三方帳號登陸,好比最多見的QQ登陸、微博登陸、百度帳號登陸、GitHub登陸。而這些第三方登陸方式就是採用了OAuth2.0協議實現。html
第一,用戶再也不須要註冊大量帳號。在之前,咱們每使用一個新的網站或者APP就須要註冊一個帳號,創建一套新的帳戶體系才能使用網站 / APP提供的服務。可是如今咱們只須要擁有幾個主流應用的帳號,而後經過他們提供的第三方帳號登陸就可使用一個新的網站/APP了(固然,咱們也能夠不使用騰訊百度等公司提供的受權服務,開發本身的受權服務端,這方面的內容我將放在下篇文章中介紹)。java
第二,用於單點登陸。若是某個公司有不少個須要用戶登陸才能提供服務的子產品(好比:官網、M網站、APP、微信公衆號、使用同一套帳戶體系的產品一、產品2等等),這種狀況下爲每一個產品都開發一個登陸、受權模塊顯然是不太優雅,所以比較好的解決方案就是全部須要登陸的產品都請求同一個登陸受權中心,進行統一登陸受權處理。而OAuth2.0協議就能夠實現符合上述要求的單點登陸功能。git
第三,用於分佈式系統的權限控制。由於基於OAuth2.0協議得到的令牌(Access Token
)同時關聯了接入的第三方應用、受權用戶、權限範圍等信息。所以,在第三方應用拿着Token請求資源的時候,資源服務應用就能夠很容易根據其訪問權限返回相應的數據。web
Access Token
)。這種模式的特色是全部步驟都在瀏覽器中完成,Token對用戶可見,且請求令牌的時候不須要傳遞client_secret
進行客戶端認證。Access Token
)。採用Authorization Code獲取Access Token的受權驗證流程又被稱爲Web Server Flow,適用於全部有Server端的應用,如Web/Wap站點、有Server端的手機/桌面客戶端應用等。通常來講整體流程包含如下幾個步驟:spring
client_id
請求受權服務端,獲取Authorization Code。Authorization Code
、client_id
、client_secret
請求受權服務端,在驗證完Authorization Code
是否失效以及接入的客戶端信息是否有效(經過傳遞的client_id
和client_secret
信息和服務端已經保存的客戶端信息進行匹配)以後,受權服務端生成Access Token和Refresh Token並返回給客戶端。Access Token
請求資源服務應用,獲取須要的且在申請的Access Token
權限範圍內的資源信息。下面,我將經過基於受權碼模式的百度OAuth2.0受權來詳細介紹上面這三個步驟。固然,最後我會給出實際可運行的測試代碼。apache
申請地址:developer.baidu.com/console#app…json
接着須要記錄新建應用的API Key
和Secret Key
:api
以及須要在安全設置裏面配置登陸的回調地址:瀏覽器
注:若是隻是在瀏覽器中測試,能夠把回調地址改爲https://www.baidu.com
,這樣就能夠直觀地在瀏覽器中看到重定向的結果了,好比請求https://openapi.baidu.com/oauth/2.0/authorize?client_id=n1pRXWNYFQ1MQLzpDfHyovFb&redirect_uri=https://www.baidu.com&response_type=code&scope=basic&display=popup
,返回結果以下:
其獲取方式是經過重定向用戶瀏覽器(或手機/桌面應用中的瀏覽器組件)到http://openapi.baidu.com/oauth/2.0/authorize
地址,並帶上如下參數:
client_id
:必須參數,註冊應用時得到的API Key。response_type
:必須參數,此值固定爲「code」。redirect_uri
:必須參數,受權後要回調的URI,即接收Authorization Code的URI。scope
:非必須參數,以空格分隔的權限列表,若不傳遞此參數,表明請求用戶的默認權限。state
:非必須參數,用於保持請求和回調的狀態,受權服務器在回調時(重定向用戶瀏覽器到「redirect_uri」時),會在Query Parameter中原樣回傳該參數。OAuth2.0標準協議建議,利用state參數來防止CSRF攻擊。display
:非必須參數,登陸和受權頁面的展示樣式,默認爲「page」,具體參數定義請參考「自定義受權頁面」一節。force_login
:非必須參數,如傳遞「force_login=1」,則加載登陸頁時強制用戶輸入用戶名和口令,不會從cookie中讀取百度用戶的登錄狀態。confirm_login
:非必須參數,如傳遞「confirm_login=1」且百度用戶已處於登錄狀態,會提示是否使用已當前登錄用戶對應用受權。login_type
:非必須參數,如傳遞「login_type=sms」,受權頁面會默認使用短信動態密碼註冊登錄方式。例如:client_id
爲n1pRXWNYFa4MQLzpDfHyovFb
的應用要請求某個用戶的默認權限和email訪問權限,並在受權後需跳轉到http://localhost:7080/login
,同時但願在彈出窗口中展示用戶登陸、受權界面,則應用須要重定向用戶的瀏覽器到以下URL:
此時受權服務會根據應用傳遞參數的不一樣,爲用戶展示不一樣的受權頁面。若是用戶在此頁面贊成受權,受權服務則將重定向用戶瀏覽器到應用所指定的redirect_uri
,並附帶上表示受權服務所分配的Authorization Code
的code參數,以及state參數(若是請求authorization code時帶了這個參數)。
例如:繼續上面的例子,假設受權服務在用戶贊成受權後生成的 Authorization Code 爲71c279ccd145a3dff977b38e6a8e34b4
,則受權服務將會返回以下響應包以重定向用戶瀏覽器到http://localhost:7080/login
地址:
HTTP/1.1 302 Found Location: http://localhost:7080/login?code=71c279ccd145a3dff977b38e6a8e34b4
經過上面得到的Authorization Code
,接下來即可以用其換取一個Access Token
。獲取方式是:應用在其服務端程序中發送請求(推薦使用POST)到 百度OAuth2.0受權服務的https://openapi.baidu.com/oauth/2.0/token
地址,並帶上如下5個必須參數:
grant_type
:必須參數,此值固定爲authorization_code
。code
:必須參數,經過上面第一步所得到的Authorization Code
。client_id
:必須參數,應用的API Key。client_secret
:必須參數,應用的Secret Key。redirect_uri
:必須參數,該值必須與獲取Authorization Code
時傳遞的redirect_uri
保持一致。例如:
若參數無誤,服務器將返回一段JSON文本,包含如下參數:
access_token
:要獲取的Access Token。expires_in
:Access Token的有效期,以秒爲單位(30天的有效期)。refresh_token
:用於刷新Access Token 的 Refresh Token,全部應用都會返回該參數(10年的有效期)。scope
:Access Token最終的訪問範圍,即用戶實際授予的權限列表(用戶在受權頁面時,有可能會取消掉某些請求的權限)。session_key
:基於http調用Open API時所須要的Session Key,其有效期與Access Token一致。session_secret
:基於http調用Open API時計算參數簽名用的簽名密鑰。例如:
{
"expires_in": 2592000,
"refresh_token": "22.247946a05a327ia929b74354c3670cb2.315360000.1847863585.321432378-13484254",
"access_token": "21.e2eb8577t4a68a32y23b61300eda8811.2592000.1536795385.321432378-13484254",
"session_secret": "e8f9ee40de92862cc35c343n5da2fcfb",
"session_key": "9mnRIQsyTR+0yfB3liSUjqGvk8F369TRfHJidz9iA0wDg\/KDBKZtGHACpXfULPjeX1YBWkKAtHSG\/OLXYKQHCuO4Zg2JiBwFtA==",
"scope": "basic"
}
複製代碼
若請求錯誤,服務器將返回一段JSON文本,包含如下參數:
error
:錯誤碼,關於錯誤碼的詳細信息請參考百度OAuth2.0錯誤響應。error_description
:錯誤描述信息,用來幫助理解和解決發生的錯誤。使用上面獲得的Access Token
獲取百度用戶的基本資料,包括:用戶名、性別、是否實名認證、是否驗證手機號等等。
相關的REST API
接口能夠參考官方文檔:developer.baidu.com/wiki/index.…
請求示例(獲取用戶基本信息):
提示:下面只會給出關鍵代碼邏輯,完整可用代碼能夠參考:gitee.com/zifangsky/B…
首先建立兩個實體類,分別表示請求Access Token
的返回信息以及請求百度用戶基本資料的返回信息。
AuthorizationResponse.java:
package cn.zifangsky.model;
/** * Authorization返回信息 * * @author zifangsky * @date 2018/7/25 * @since 1.0.0 */
public class AuthorizationResponse {
/** * 要獲取的Access Token(30天的有效期) */
private String access_token;
/** * 用於刷新Access Token 的 Refresh Token(10年的有效期) */
private String refresh_token;
/** * Access Token最終的訪問範圍 */
private String scope;
/** * Access Token的有效期,以秒爲單位(30天的有效期) */
private Long expires_in;
/** * 基於http調用Open API時所須要的Session Key,其有效期與Access Token一致 */
private String session_key;
/** * 基於http調用Open API時計算參數簽名用的簽名密鑰 */
private String session_secret;
/** * 錯誤信息 */
private String error;
/** * 錯誤描述 */
private String error_description;
//省略setter和getter
@Override
public String toString() {
return "AuthorizationResponse{" +
"access_token='" + access_token + '\'' +
", refresh_token='" + refresh_token + '\'' +
", scope='" + scope + '\'' +
", expires_in=" + expires_in +
", session_key='" + session_key + '\'' +
", session_secret='" + session_secret + '\'' +
", error='" + error + '\'' +
", error_description='" + error_description + '\'' +
'}';
}
}
複製代碼
BaiduUser.java:
package cn.zifangsky.model;
/** * 百度返回的用戶基本信息 * * @author zifangsky * @date 2018/7/25 * @since 1.0.0 */
public class BaiduUser {
/** * 百度的userId */
private String userid;
/** * 用戶名 */
private String username;
/** * 用戶性別,0表示女性,1表示男性 */
private Integer sex;
/** * 用戶生日 */
private String birthday;
/** * 用戶描述 */
private String userdetail;
/** * 是否綁定手機號 */
private Integer is_bind_mobile;
/** * 是否已經實名認證 */
private Integer is_realname;
//省略setter和getter
@Override
public String toString() {
return "BaiduUser{" +
"userid='" + userid + '\'' +
", username='" + username + '\'' +
", sex=" + sex +
", birthday='" + birthday + '\'' +
", userdetail='" + userdetail + '\'' +
", is_bind_mobile=" + is_bind_mobile +
", is_realname=" + is_realname +
'}';
}
}
複製代碼
最後就是最關鍵的用戶登陸邏輯了:
package cn.zifangsky.controller;
import cn.zifangsky.common.Constants;
import cn.zifangsky.model.AuthorizationResponse;
import cn.zifangsky.model.BaiduUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.text.MessageFormat;
/** * 登陸 * @author zifangsky * @date 2018/7/9 * @since 1.0.0 */
@Controller
public class LoginController {
@Autowired
private RestTemplate restTemplate;
@Value("${baidu.oauth2.client-id}")
private String clientId;
@Value("${baidu.oauth2.scope}")
private String scope;
@Value("${baidu.oauth2.client-secret}")
private String clientSecret;
@Value("${baidu.oauth2.user-authorization-uri}")
private String authorizationUri;
@Value("${baidu.oauth2.access-token-uri}")
private String accessTokenUri;
@Value("${baidu.oauth2.resource.userInfoUri}")
private String userInfoUri;
/** * 登陸驗證(實際登陸調用認證服務器) * @author zifangsky * @date 2018/7/25 16:42 * @since 1.0.0 * @param request HttpServletRequest * @return org.springframework.web.servlet.ModelAndView */
@RequestMapping("/login")
public ModelAndView login(HttpServletRequest request){
//當前系統登陸成功以後的回調URL
String redirectUrl = request.getParameter("redirectUrl");
//當前系統請求認證服務器成功以後返回的Authorization Code
String code = request.getParameter("code");
//最後重定向的URL
String resultUrl = "redirect:";
HttpSession session = request.getSession();
//當前請求路徑
String currentUrl = request.getRequestURL().toString();
//code爲空,則說明當前請求不是認證服務器的回調請求,則重定向URL到百度OAuth2.0登陸
if(StringUtils.isBlank(code)){
//若是存在回調URL,則將這個URL添加到session
if(StringUtils.isNoneBlank(redirectUrl)){
session.setAttribute("redirectUrl",redirectUrl);
}
resultUrl += authorizationUri + MessageFormat.format("?client_id={0}&response_type=code&scope=basic&display=popup&redirect_uri={1}"
,clientId,currentUrl);
}else{
//1. 經過Authorization Code獲取Access Token
AuthorizationResponse response = restTemplate.getForObject(accessTokenUri + "?client_id={1}&client_secret={2}&grant_type=authorization_code&code={3}&redirect_uri={4}"
,AuthorizationResponse.class
, clientId, clientSecret, code,currentUrl);
//2. 若是正常返回
if(response != null && StringUtils.isNoneBlank(response.getAccess_token())){
System.out.println(response);
//2.1 將Access Token存到session
session.setAttribute(Constants.SESSION_ACCESS_TOKEN,response.getAccess_token());
//2.2 再次查詢用戶基礎信息,並將用戶ID存到session
BaiduUser baiduUser = restTemplate.getForObject(userInfoUri + "?access_token={1}"
,BaiduUser.class
,response.getAccess_token());
if(baiduUser != null && StringUtils.isNoneBlank(baiduUser.getUserid())){
System.out.println(baiduUser);
session.setAttribute(Constants.SESSION_USER_ID,baiduUser.getUserid());
}
}
//3. 從session中獲取回調URL,並返回
redirectUrl = (String) session.getAttribute("redirectUrl");
session.removeAttribute("redirectUrl");
if(StringUtils.isNoneBlank(redirectUrl)){
resultUrl += redirectUrl;
}else{
resultUrl += "/user/userIndex";
}
}
return new ModelAndView(resultUrl);
}
}
複製代碼
上面代碼裏面的註釋已經很詳細了,這裏我就很少作解釋了,詳細代碼能夠自行參考上面給出的示例源碼。本篇文章到此結束,我將在下篇文章中介紹如何本身手動實現OAuth2.0受權服務端,敬請期待!
參考: