自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

通過實例理解OpenID身份認證

網(wǎng)絡 網(wǎng)絡管理
OIDC利用OAuth 2.0流程進行身份認證,通過額外返回的ID_TOKEN提供EU身份信息,很好地滿足了RP對EU身份管理的需求。

在《通過實例理解OAuth2[1]》一文中,我們以實例方式講解了OAuth2授權(quán)碼模式(Authorization Code)模式的工作原理。實例中的照片沖印服務經(jīng)過用戶(tonybai)的授權(quán)后,使用用戶提供的code(實則是由授權(quán)服務器分配并通過用戶的瀏覽器重定向到照片沖印服務的)到授權(quán)服務器換取了access token,并最終使用access token從云盤系統(tǒng)中讀取到了用戶的照片信息。

不過,拿到了access token的照片沖印服務并不知道這個access token代表的是云盤服務上的哪個用戶,要不是云盤服務在照片list接口返回了用戶名(tonybai),照片沖印服務還需要自己為授權(quán)給它的用戶創(chuàng)建一個臨時的用戶id標識。當tonybai用戶一周后再次訪問照片沖印服務時,照片沖印服務還需要再走一次OAuth2授權(quán)流程,這對用戶的體驗并不好。

從照片沖印服務角度來說,它希望在用戶第一次使用服務并授權(quán)時,就能得到用戶身份信息,將用戶加入到自己的用戶體系中,并通過類似基于會話的身份認證機制[2]在用戶后續(xù)使用服務時自動識別并認證用戶身份。這樣,既可以避免用戶額外單獨注冊賬號的不佳體驗,又可以避免用戶下次使用服務時繁瑣地授權(quán)過程。

然而,盡管OAuth 2.0是一個需要用戶交互的安全協(xié)議,但它終歸不是身份認證協(xié)議。但很多像照片沖印服務這樣的應用還有通過像云盤系統(tǒng)這一的大廠應用進行用戶身份認證的強烈需求,于是有很多廠商都制定了各自專用的標準,比如Facebook、Twitter、LinkedIn和GitHub等,但這些都是專用協(xié)議,缺乏標準性,開發(fā)者要逐一開發(fā)和適配。

于是OpenID基金會[3]基于OAuth2.0制定了OpenID Connect(簡稱OIDC)[4]這樣的開放身份認證協(xié)議標準,可以在不同廠商之間通用。

在這篇文章中,我們就來介紹一下基于OpenID的身份認證原理,有了上一篇OAuth2做鋪墊,OIDC理解起來就非常容易了。

1. OpenID Connect(OIDC)簡介

OpenID Connect是一個開放標準,由OpenID基金會于2014年2月發(fā)布。它定義了一種使用OAuth 2.0執(zhí)行用戶身份認證的互通方式。由于該協(xié)議的設計具有互通性,一個OpenID客戶端應用可以使用同一套協(xié)議語言與不同的身份提供者交互,而不需要為每一個身份提供者實現(xiàn)一套有細微差別的協(xié)議。OpenID Connect直接基于OAuth 2.0構(gòu)建,并保持了OAuth2.0的兼容性。現(xiàn)實世界中,在多數(shù)情況下,OIDC都會與保護其他API的OAuth基礎架構(gòu)部署在一起。

我們在學習OAuth 2.0[5]時,首先了解了該協(xié)議涉及的幾個實體,如Client、Authorization Server、Resource Server、Resource owner、Protected resouce等,以及它們的交互流程。知道了這些也就掌握了OAuth2的內(nèi)核。以此為鑒,我們學習OIDC協(xié)議,也從了解都有哪些實體參與了協(xié)議交互,以及它們的具體交互流程開始。

OpenID Connect是一個協(xié)議套件(OpenID Connect Protocol Suite[6]),涉及Core、Discovery、Dynamic Client Registration等:

圖片圖片

不過這里我們僅聚焦OpenID Connect的core 1.0協(xié)議規(guī)范[7]。

就像OAuth2.0支持四種授權(quán)模式一樣,OIDC基于這四種模式,整合出了三種身份認證類型:

  • Authentication using the Authorization Code Flow
  • Authentication using the Implicit Flow
  • Authentication using the Hybrid Flow

其中Authentication using the Authorization Code Flow這種基于OAuth2授權(quán)碼流程的身份認證方案應該是使用最為廣泛的,本文也將基于這個流程對OIDC進行理解,并賦以實例。

1.1 OIDC協(xié)議中的實體與交互流程圖

下面是OIDC規(guī)范中給出的通用的身份認證流程圖,這個圖是高度抽象的,適合上面三個flow:

圖片圖片

通過這個圖,我們先來認識參與OIDC流程中的三個實體:

  • RP(Relying Party)

圖的最左端是一個叫RP的實體,如果對應到OAuth2.0那篇文章中的示例,這個RP對應的就是示例中的照片沖印服務,也就是OAuth2.0中的Client,即需要用戶(EU)授權(quán)的那個實體。

  • OP(OpenID Provider)

OP對應的是OAuth2.0中的Authorization Server+Resource Server,不同的是在OIDC這個特殊場景下,Resource Server中存儲的resource就是用戶的身份信息。

  • EU(End User)

EU,顧名思義就是使用RP服務的用戶,它對應OAuth2.0中的Resource Owner。

結(jié)合這些實體、上面的抽象流程圖以及OAuth2授權(quán)碼模式的交互圖,我畫一下OIDC基于授權(quán)碼模式進行身份認證的實體間的交互圖,這里我們依舊以用戶使用照片沖印服務為例:

圖片圖片

上圖就是一個基于授權(quán)碼流程的OIDC協(xié)議流程,是不是趕腳跟OAuth 2.0中的授權(quán)碼模式的流程幾乎完全一致啊!

唯一的區(qū)別就是授權(quán)服務器(OP)在返回access_token的同時,還多返回了一個ID_TOKEN,我們稱這個ID_TOKEN為ID令牌,這個令牌是OIDC身份認證的關(guān)鍵。

1.2 ID_TOKEN的組成

從上圖中,我們看到ID_TOKEN與普通的OAuth access_token一起提供給Client(RP)使用,與access_token不同的是,RP是需要對ID_TOKEN進行解析的。那么這個ID_TOKEN究竟是什么呢?在OIDC協(xié)議中,ID_TOKEN是一個經(jīng)過簽名的JWT[8],

OIDC協(xié)議規(guī)范規(guī)定了該jwt應該包含的字段信息,包括必選的(REQUIRED)與可選的(OPTIONAL),在這里我們了解下面的必選字段信息即可:

  • iss

令牌的頒發(fā)者,其值就是身份認證服務(OP)的URL,比如:http://open.my-yunpan.com:8081/oauth/token,不包含問號作為前綴的查詢參數(shù)等。

  • sub

令牌的主題標識符,其值是最終用戶(EU)在身份認證服務(OP)內(nèi)部的唯一且永不重新分配的標識符。

  • aud

令牌的目標受眾,其值是Client(RP)的標識,必須包含RP的OAuth 2.0客戶端ID(client_id),也可以包含其他受眾的標識符。

  • exp

過期時間,過期后ID_TOKEN將會失效。其值是一個JSON number,表示從1970-01-01T0:0:0Z開始(以 UTC 度量)到過期日期/時間為止的秒數(shù)。

  • iat

認證時間,即版本ID_TOKEN的時間,其值是一個JSON number,表示從1970-01-01T0:0:0Z開始(以 UTC 度量)到認證日期/時間為止的秒數(shù)。

注:如果客戶端(RP)向身份認證服務器(OP)注冊過公鑰,則可以使用客戶端公鑰對該JWT進行非對稱簽名校驗,或者可以使用客戶端密鑰對該JWT進行對稱簽名。這種方式可以提高客戶端的安全等級,因為可以避免在網(wǎng)絡上傳遞密鑰。

在上面圖中使用access_token獲取user_info的環(huán)節(jié)中,RP可以通過ID_TOKEN中的sub(EU唯一標識符)到授權(quán)服務器的userinfo端點換取用戶的基本信息,這樣在RP自己的頁面上展示EU的標識時就不可以不用9XDF-AABB-001ACFE這樣的唯一標識符(sub),而是用TonyBai這樣的可理解的字符串了。

注:OpenID Connect使用一個特殊的權(quán)限范圍值openid來控制對UserInfo端點的訪問。OpenID Connect定義了一組標準化的OAuth權(quán)限范圍,對 應于用戶屬性的子集,比如profile 、email 、phone 、address等。

了解了OIDC的身份認證流程以及ID_TOKEN的組成后,我們就算對OIDC有個直觀的認知了,接下來我們用一個實例來加深一下對OIDC身份認證的理解。

2. OIDC實例

如果你理解了《通過實例理解OAuth2[9]》一文中的實例,那么理解本篇文章中的OIDC實例將是輕而易舉的事情。前面說過,OIDC建構(gòu)在OAuth2之上,與OAuth2兼容,因此,這里的OIDC實例也改自OAuth2一文中的實例。

與OAuth2一文實例相比,OIDC實例中去掉了云盤服務(my-yunpan),僅保留了下面結(jié)構(gòu):

$tree -L 2 -F oidc-examples 
oidc-examples
├── my-photo-print/
│   ├── go.mod
│   ├── go.sum
│   ├── home.html
│   ├── main.go
│   └── profile.html
└── open-my-yunpan/
    ├── go.mod
    ├── go.sum
    ├── main.go
    └── portal.html

其中my-photo-print是照片沖印服務,也是oidc實例中的RP實體,而open-my-yunpan扮演著云盤授權(quán)服務,是oidc實例中的OP實體。在編寫和運行服務之前,我們同樣要先修改一下本機(MacOS或Linux)的/etc/hosts文件:

127.0.0.1 my-photo-print.com
127.0.0.1 open.my-yunpan.com

注:在演示下面步驟前,請先進入到oidc-examples的兩個目錄下,通過go run main.go啟動各個服務程序(每個程序一個終端窗口)。

2.1 用戶使用my-photo-print.com照片沖印服務

按照流程,用戶首先通過瀏覽器打開照片沖印服務的首頁:http://my-photo-print.com:8080,如下圖:

圖片圖片

這與OAuth2一文中的實例并無什么差別,該頁面也是由my-photo-print/main.go中的homeHandler提供的,它的home.html渲染模板也基本沒有變化,因此這里就不贅述了。

當用戶選擇并點擊“使用云盤賬號登錄”時,瀏覽器將打開云盤授權(quán)服務(OP)的首頁(http://open.my-yunpan.com:8081/oauth/portal)。

2.2 使用open.my-yunpan.com進行授權(quán),包括openid權(quán)限

云盤授權(quán)服務的首頁還是“老樣子”,唯一的差別就是請求的權(quán)限包含了一項openid(有my-photo-print的home.html帶過來的):

圖片圖片

這個頁面同樣由open.my-yunpan.com的portalHandler提供,它的邏輯與oauth2的實例相比沒有變化,這里也羅列其代碼了。

當用戶(EU)填寫用戶名和密碼后,點擊“授權(quán)”,瀏覽器便會向云盤授權(quán)服務的"/oauth/authorize"發(fā)起post請求以獲取code,負責"/oauth/authorize"端點的authorizeHandler會對用戶進行身份認證,通過后,它會分配code并向瀏覽器返回重定向的應答,重定向的地址就是照片沖印服務的回調(diào)地址:http://my-photo-print.com:8080/cb?code=xxx&state=yyy。

2.3 獲取access token以及id_token,并用用戶唯一標識獲取用戶基本信息(profile)

這個重定向相當于用戶瀏覽器向http://my-photo-print.com:8080/cb?code=xxx&state=yyy發(fā)起請求,為照片沖印服務提供code,該請求由my-photo-print的oauthCallbackHandler處理:

// oidc-examples/my-photo-print/main.go

// callback handler,用戶(EU)拿到code后調(diào)用該handler
func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Println("oauthCallbackHandler:", *r)

 code := r.FormValue("code")
 state := r.FormValue("state")

 // check state
 mu.Lock()
 _, ok := stateCache[state]
 if !ok {
  mu.Unlock()
  fmt.Println("not found state:", state)
  w.WriteHeader(http.StatusBadRequest)
  return
 }
 delete(stateCache, state)
 mu.Unlock()

 // fetch access_token and id_token with code
 accessToken, idToken, err := fetchAccessTokenAndIDToken(code)
 if err != nil {
  fmt.Println("fetch access_token error:", err)
  return
 }
 fmt.Println("fetch access_token ok:", accessToken)

 // parse id_token
 mySigningKey := []byte("iamtonybai")
 claims := jwt.RegisteredClaims{}
 _, err = jwt.ParseWithClaims(idToken, &claims, func(token *jwt.Token) (interface{}, error) {
  return mySigningKey, nil
 })
 if err != nil {
  fmt.Println("parse id_token error:", err)
  return
 }

 // use access_token and userID to get user info
 up, err := getUserInfo(accessToken, claims.Subject)
 if err != nil {
  fmt.Println("get user info error:", err)
  return
 }
 fmt.Println("get user info ok:", up)

 mu.Lock()
 userProfile[claims.Subject] = up
 mu.Unlock()

 // 設置cookie
 cookie := http.Cookie{
  Name:   "my-photo-print.com-session",
  Value:  claims.Subject,
  Domain: "my-photo-print.com",
  Path:   "/profile",
 }
 http.SetCookie(w, &cookie)
 w.Header().Add("Location", "/profile")
 w.WriteHeader(http.StatusFound) // redirect to /profile
}

這個handler中做了很多工作。首先是使用code像授權(quán)服務器換取access token和id_token,授權(quán)服務器負責頒發(fā)token的是tokenHandler:

// oidc-examples/open-yunpan/main.go

func tokenHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Println("tokenHandler:", *r)

 // check client_id and client_secret
 user, password, ok := r.BasicAuth()
 if !ok {
  fmt.Println("no authorization header")
  w.WriteHeader(http.StatusNonAuthoritativeInfo)
  return
 }

 mu.Lock()
 v, ok := validClients[user]
 if !ok {
  fmt.Println("not found user:", user)
  mu.Unlock()
  w.WriteHeader(http.StatusNonAuthoritativeInfo)
  return
 }
 mu.Unlock()

 if v != password {
  fmt.Println("invalid password")
  w.WriteHeader(http.StatusNonAuthoritativeInfo)
  return
 }

 // check code and redirect_uri
 code := r.FormValue("code")
 redirect_uri := r.FormValue("redirect_uri")

 mu.Lock()
 ac, ok := codeCache[code]
 if !ok {
  fmt.Println("not found code:", code)
  mu.Unlock()
  w.WriteHeader(http.StatusNotFound)
  return
 }
 mu.Unlock()

 if ac.redirectURI != redirect_uri {
  fmt.Println("invalid redirect_uri:", redirect_uri)
  w.WriteHeader(http.StatusBadRequest)
  return
 }

 var authResponse struct {
  AccessToken string `json:"access_token"`
  IDToken     string `json:"id_token,omitempty"`
  ExpireIn    int    `json:"expires_in"`
 }

 // generate access_token
 authResponse.AccessToken = randString(16)
 authResponse.ExpireIn = 3600
 now := time.Now()
 expired := now.Add(10 * time.Minute)
 claims := jwt.RegisteredClaims{
  Issuer:    "http://open.my-yunpan.com:8091/oauth/token",
  Subject:   ac.userID,
  Audience:  jwt.ClaimStrings{user}, //client_id
  IssuedAt:  &jwt.NumericDate{now},
  ExpiresAt: &jwt.NumericDate{expired},
 }

 if strings.Contains(ac.scopeTxt, "openid") {
  // generate id_token if contains openid
  mySigningKey := []byte("iamtonybai")
  jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  authResponse.IDToken, _ = jwtToken.SignedString(mySigningKey)
 }

 respData, _ := json.Marshal(&authResponse)
 w.Write(respData)
}

我們看到tokenHandler先是對客戶端(client)憑據(jù)做了校驗,接下來驗證code,如果code通過驗證,則會分配access_token,并根據(jù)scope中是否包含openid決定是否分配id_token,這里我們的權(quán)限授權(quán)中包含了openid,于是tokenHandler將id_token(一個jwt)一并生成并返回給client。

而拿到access_token和id_token的my-photo-print的oauthCallbackHandler會解析id_token,提取其中的有效信息,比如subject等,并用access_token和id_token中的subject(用戶的唯一ID)去授權(quán)服務獲取用戶(EU)的基礎身份信息(姓名、主頁、郵箱等),并將用戶的唯一ID作為cookie存入用戶的瀏覽器。最后讓瀏覽器重定向到my-photo-print的profile頁面。

請注意:這里僅是為了簡便起見,生產(chǎn)環(huán)境請考慮更為安全的會話機制。

profile頁面的處理函數(shù)為profileHandler:

// oidc-examples/my-photo-print/main.go

// user profile頁面
func profileHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Println("profileHandler:", *r)

 cookie, err := r.Cookie("my-photo-print.com-session")
 if err != nil {
  http.Error(w, "找不到cookie,請重新登錄", 401)
  return
 }
 fmt.Printf("found cookie: %#v\n", cookie)

 mu.Lock()
 pf, ok := userProfile[cookie.Value]
 if !ok {
  mu.Unlock()
  fmt.Println("not found user:", cookie.Value)
  // 跳轉(zhuǎn)到首頁
  http.Redirect(w, r, "/", http.StatusSeeOther)
  return
 }
 mu.Unlock()

 // 渲染照片頁面模板
 tmpl := template.Must(template.ParseFiles("profile.html"))
 tmpl.Execute(w, pf)
}

我們看到:該handler首先查找cookie中是否存在用戶ID,如果不存在,則重定向到登錄頁面,如果存在,則取出用戶唯一ID,并使用該ID查找用戶profile信息,最后展示到web頁面上:

到這里,我們看到:這種委托云盤授權(quán)服務對my-photo-print的用戶進行身份認證并拿到該用戶基本信息的機制,就是oidc。

注:一旦拿到云盤授權(quán)服務身份認證后的用戶信息,RP便可以使用各種身份認證機制來管理EU用戶,比如RP可以使用會話管理技術(shù)(例如使用會話標識符或瀏覽器cookie)來跟蹤EU的會話狀態(tài)。如果EU在同一會話期間訪問RP應用,RP可以通過會話標識符來識別EU,而無需再次進行身份驗證。

3. 小結(jié)

通過上面的內(nèi)容,我們對OpenID Connect(OIDC)有了更直觀的理解,這里做一個小結(jié):

  • OIDC是一套身份認證的開放標準協(xié)議,基于OAuth 2.0構(gòu)建,與OAuth 2.0兼容。
  • OIDC協(xié)議中主要涉及三個角色:RP(依賴方)、OP(身份提供方)、EU(最終用戶)。
  • EU通過RP使用OP進行身份認證后,RP可以獲得EU的身份信息。整個流程與OAuth 2.0的授權(quán)碼流程高度相似。
  • 關(guān)鍵的差別在于:OP返回的token中除了access_token外,還包含一個ID_TOKEN(JWT格式)。
  • RP通過解析ID_TOKEN可以獲得EU的唯一標識等信息,并通過access_token進一步獲取EU的詳細身份信息。
  • RP獲得EU身份信息后,可以通過各種機制識別和管理EU,無需EU重復身份驗證。

總的來說,OIDC利用OAuth 2.0流程進行身份認證,通過額外返回的ID_TOKEN提供EU身份信息,很好地滿足了RP對EU身份管理的需求。

文本涉及的源碼可以在這里[10]下載。

4. 參考資料

  • OIDC(OpenID Connect) Specification[11] - https://openid.net/specs/openid-connect-core-1_0.html
  • 利用OAuth 2.0實現(xiàn)一個OpenID Connect用戶身份認證協(xié)議[12] - https://time.geekbang.org/column/article/262672
責任編輯:武曉燕 來源: TonyBai
相關(guān)推薦

2023-10-26 08:19:34

2023-11-20 08:02:49

2009-06-27 10:59:04

2018-12-24 17:52:39

身份認證桌面登錄安全

2012-11-28 09:55:35

2023-10-31 07:37:02

2011-03-30 13:21:17

2014-04-22 10:15:38

vCenter SSO身份認證

2023-12-03 18:30:12

2010-09-08 12:38:45

Oracle

2022-02-10 22:56:56

區(qū)塊鏈物聯(lián)網(wǎng)人工智能

2013-11-15 09:43:02

2012-04-16 09:54:26

2010-09-06 14:03:06

PPP身份認證

2017-04-27 02:08:18

身份認證人工智能首都網(wǎng)絡安全日

2018-03-06 09:26:27

數(shù)據(jù)身份認證區(qū)塊鏈

2013-08-30 10:54:53

2018-07-05 14:52:05

2012-05-07 14:50:32

ASP.NET

2010-09-03 09:19:13

PPP身份認證
點贊
收藏

51CTO技術(shù)棧公眾號