OAuth2.0協議入門(一):OAuth2.0協議的基本概念以及使用受權碼模式(authorization code)實現百度帳號登陸

一 OAuth2.0協議的基本概念

(1)OAuth2.0協議

OAuth協議,是一種受權協議,不涉及具體的代碼,只是表示一種約定的流程和規範。OAuth協議通常用於用戶決定是否把本身在某個服務商上面的資源(好比:用戶基本資料、照片、視頻等)受權給第三方應用訪問。此外,OAuth2.0協議是OAuth協議的升級版,如今已經逐漸成爲單點登陸(SSO)和用戶受權的標準。php

不知道你們有沒有發現,目前主流的互聯網網站除了可使用「用戶名+密碼」模式和「手機號+驗證碼」模式登陸外,不少還提供了第三方帳號登陸,好比最多見的QQ登陸微博登陸百度帳號登陸GitHub登陸。而這些第三方登陸方式就是採用了OAuth2.0協議實現。html

CSDN的登陸界面

(2)爲何使用OAuth2.0協議?

第一,用戶再也不須要註冊大量帳號。在之前,咱們每使用一個新的網站或者APP就須要註冊一個帳號,創建一套新的帳戶體系才能使用網站 / APP提供的服務。可是如今咱們只須要擁有幾個主流應用的帳號,而後經過他們提供的第三方帳號登陸就可使用一個新的網站/APP了(固然,咱們也能夠不使用騰訊百度等公司提供的受權服務,開發本身的受權服務端,這方面的內容我將放在下篇文章中介紹)。java

第二,用於單點登陸。若是某個公司有不少個須要用戶登陸才能提供服務的子產品(好比:官網、M網站、APP、微信公衆號、使用同一套帳戶體系的產品一、產品2等等),這種狀況下爲每一個產品都開發一個登陸、受權模塊顯然是不太優雅,所以比較好的解決方案就是全部須要登陸的產品都請求同一個登陸受權中心,進行統一登陸受權處理。而OAuth2.0協議就能夠實現符合上述要求的單點登陸功能。git

第三,用於分佈式系統的權限控制。由於基於OAuth2.0協議得到的令牌(Access Token)同時關聯了接入的第三方應用、受權用戶、權限範圍等信息。所以,在第三方應用拿着Token請求資源的時候,資源服務應用就能夠很容易根據其訪問權限返回相應的數據。web

(3)OAuth2.0協議涉及到的幾個重要角色

  • 受權服務端應用(Authorization Server):服務提供商提供的專門用於處理受權的服務端應用,好比上面介紹的QQ登陸、微博登陸,固然也能夠搭建本身的受權服務端。
  • 資源服務應用(Resource Server):服務提供商存放用戶及其餘資源的應用,通常用於接口的形式返回第三方應用請求的資源。它能夠與受權服務端屬於同一個應用,也能夠分別屬於不一樣的應用。
  • 用戶(User):用戶在受權服務端登陸,受權服務端記錄了用戶的帳戶體系。固然,有的網站會在你經過第三方帳號第一次登陸成功後,要求綁定你的手機號並建立暱稱,這就是他們在建立本身的帳戶體系(跟OAuth2.0協議無關,這裏不做展開)了。
  • 接入的第三方應用(Third-party Application):接入認證的第三方應用又被稱爲「客戶端」,好比一個普通的網站、APP。

(4)幾種受權模式

  • 受權碼模式(authorization code):這是功能最完整,流程最嚴密的模式。如今主流的使用OAuth2.0協議受權的服務提供商都採用了這種模式,我在下面舉例也將採起這種模式。
  • 簡化模式(implicit):跳過了請求受權碼(Authorization Code)的步驟,直接經過瀏覽器向受權服務端請求令牌(Access Token)。這種模式的特色是全部步驟都在瀏覽器中完成,Token對用戶可見,且請求令牌的時候不須要傳遞client_secret進行客戶端認證。
  • 密碼模式(resource owner password credentials):用戶向第三方客戶端提供本身在受權服務端的用戶名和密碼,客戶端經過用戶提供的用戶名和密碼向受權服務端請求令牌(Access Token)。

(5)受權碼模式(authorization code)受權的流程

採用Authorization Code獲取Access Token的受權驗證流程又被稱爲Web Server Flow,適用於全部有Server端的應用,如Web/Wap站點、有Server端的手機/桌面客戶端應用等。通常來講整體流程包含如下幾個步驟:spring

  1. 經過client_id請求受權服務端,獲取Authorization Code
  2. 經過Authorization Codeclient_idclient_secret請求受權服務端,在驗證完Authorization Code是否失效以及接入的客戶端信息是否有效(經過傳遞的client_idclient_secret信息和服務端已經保存的客戶端信息進行匹配)以後,受權服務端生成Access TokenRefresh Token並返回給客戶端。
  3. 客戶端經過獲得的Access Token請求資源服務應用,獲取須要的且在申請的Access Token權限範圍內的資源信息。

下面,我將經過基於受權碼模式的百度OAuth2.0受權來詳細介紹上面這三個步驟。固然,最後我會給出實際可運行的測試代碼。apache

二 使用受權碼模式實現百度帳號登陸

(1)在百度開發者中心新建一個應用

申請地址:developer.baidu.com/console#app…json

接着須要記錄新建應用的API KeySecret Keyapi

新建應用
新建應用

以及須要在安全設置裏面配置登陸的回調地址:瀏覽器

配置登陸的回調地址
配置登陸的回調地址

注:若是隻是在瀏覽器中測試,能夠把回調地址改爲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,返回結果以下:

受權回調示例

(2)獲取Authorization Code

其獲取方式是經過重定向用戶瀏覽器(或手機/桌面應用中的瀏覽器組件)到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_idn1pRXWNYFa4MQLzpDfHyovFb的應用要請求某個用戶的默認權限和email訪問權限,並在受權後需跳轉到http://localhost:7080/login,同時但願在彈出窗口中展示用戶登陸、受權界面,則應用須要重定向用戶的瀏覽器到以下URL:

openapi.baidu.com/oauth/2.0/a…

響應數據包格式:

此時受權服務會根據應用傳遞參數的不一樣,爲用戶展示不一樣的受權頁面。若是用戶在此頁面贊成受權,受權服務則將重定向用戶瀏覽器到應用所指定的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

(3)經過Authorization Code獲取Access Token

經過上面得到的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保持一致。

例如:

openapi.baidu.com/oauth/2.0/t…

響應數據包格式:

若參數無誤,服務器將返回一段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:錯誤描述信息,用來幫助理解和解決發生的錯誤。

(4)使用Access Token獲取百度用戶的基本資料

使用上面獲得的Access Token獲取百度用戶的基本資料,包括:用戶名、性別、是否實名認證、是否驗證手機號等等。

相關的REST API接口能夠參考官方文檔:developer.baidu.com/wiki/index.…

請求示例(獲取用戶基本信息)

openapi.baidu.com/rest/2.0/pa…

(5)在普通Java Web項目中實現百度OAuth2.0受權登陸

提示:下面只會給出關鍵代碼邏輯,完整可用代碼能夠參考: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受權服務端,敬請期待!

參考:

相關文章
相關標籤/搜索