繼續這一個系列,基于Token的WEB后臺登錄認證機制(并講解cookie和session機制)。每個后端不得不解決的認證問題。
本系列:
(一)J2EE項目系列(三)--Spring Data JPA+Spring+SpringMVC+Maven快速開發(1)項目架構
(二) J2EE項目系列(三)--Spring Data JPA+Spring+SpringMVC+Maven快速開發(2)多個第三方服務端接入之云旺IM
(三) Java-解決實現JPA的hibernate自動建表的編碼問題
文章結構:(1)JWT(一種基于 token 的認證方案)介紹并介紹其他幾大認證機制;(2)cookie和session機制;(3)Token機制相對于Cookie機制的好處;(4)JWT的Java實現;
一、JWT(一種基于 token 的認證方案)介紹:
(1)概述:JWT就是一種Token的編碼算法,服務器端負責根據一個密碼和算法生成Token,然后發給客戶端,客戶端只負責后面每次請求都在HTTP header里面帶上這個Token,服務器負責驗證這個Token是不是合法的,有沒有過期等,并可以解析出subject和claim里面的數據。
(2)相關問題:
1.為什么用JWT?
JWT只通過算法實現對Token合法性的驗證,不依賴數據庫,Memcached的等存儲系統,因此可以做到跨服務器驗證,只要密鑰和算法相同,不同服務器程序生成的Token可以互相驗證。
2.JWT Token不需要持久化在任何NoSQL中,不然背離其算法驗證的初心
3.在退出登錄時怎樣實現JWT Token失效呢?
退出登錄, 只要客戶端端把Token丟棄就可以了,服務器端不需要廢棄Token。
4.怎樣保持客戶端長時間保持登錄狀態?
服務器端提供刷新Token的接口, 客戶端負責按一定的邏輯刷新服務器Token。
5.服務器端是否應該從JWT中取出userid用于業務查詢?
REST API是無狀態的,意味著服務器端每次請求都是獨立的,即不依賴以前請求的結果,因此也不應該依賴JWT token做業務查詢, 應該在請求報文中單獨加個userid 字段。
為了做用戶水平越權的檢查,可以在業務層判斷傳入的userid和從JWT token中解析出的userid是否一致, 有些業務可能會允許查不同用戶的數據。
(3)其他幾大認證機制:
1.HTTP Basic Auth:
HTTP Basic Auth簡單點說明就是每次請求API時都提供用戶的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供用戶名密碼即可,但由于有把用戶名密碼暴露給第三方客戶端的風險,在生產環境下被使用的越來越少。因此,在開發對外開放的RESTful API時,盡量避免采用HTTP Basic Auth
2.OAuth(開放授權):
是一個開放的授權標準,允許用戶讓第三方應用訪問該用戶在某一web服務上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。
OAuth允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每一個令牌授權一個特定的第三方系統(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶可以授權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非所有內容。
這里寫圖片描述
這種基于OAuth的認證機制適用于個人消費者類的互聯網產品,如社交類APP等應用,但是不太適合擁有自有認證權限管理的企業應用;
3.Cookie Auth:
Cookie認證機制就是為一次請求認證在服務端創建一個Session對象,同時在客戶端的瀏覽器端創建了一個Cookie對象;通過客戶端帶上來Cookie對象來與服務器端的session對象匹配來實現狀態管理的。默認的,當我們關閉瀏覽器的時候,cookie會被刪除。但可以通過修改cookie 的expire time使cookie在一定時間內有效;
二、cookie和session機制:
(1)概述:
Cookie和Session是為了在無狀態的HTTP協議之上維護會話狀態,使得服務器可以知道當前是和哪個客戶在打交道。
Session是在服務端保存的一個數據結構,用來跟蹤用戶的狀態,這個數據可以保存在集群、數據庫、文件中;
Cookie是客戶端保存用戶信息的一種機制,用來記錄用戶的一些信息,也是實現Session的一種方式。
因為HTTP協議是無狀態的,即每次用戶請求到達服務器時,HTTP服務器并不知道這個用戶是誰、是否登錄過等。現在的服務器之所以知道我們是否已經登錄,是因為服務器在登錄時設置了瀏覽器的Cookie!Session則是借由Cookie而實現的更高層的服務器與瀏覽器之間的會話。
(2)cookie實現機制:
Cookie是由客戶端保存的小型文本文件,其內容為一系列的鍵值對。 Cookie是由HTTP服務器設置的,保存在瀏覽器中, 在用戶訪問其他頁面時,會在HTTP請求中附上該服務器之前設置的Cookie。
Cookie的傳遞流程:
1.瀏覽器向某個URL發起HTTP請求(可以是任何請求,比如GET一個頁面、POST一個登錄表單等)
2.對應的服務器收到該HTTP請求,并計算應當返回給瀏覽器的HTTP響應。(HTTP響應包括請求頭和請求體兩部分)
3.在響應頭加入Set-Cookie字段,它的值是要設置的Cookie。
4.瀏覽器收到來自服務器的HTTP響應。
5.瀏覽器在響應頭中發現Set-Cookie字段,就會將該字段的值保存在內存或者硬盤中。(Set-Cookie字段的值可以是很多項Cookie,每一項都可以指定過期時間Expires。 默認的過期時間是用戶關閉瀏覽器時。)
6.瀏覽器下次給該服務器發送HTTP請求時, 會將服務器設置的Cookie附加在HTTP請求的頭字段Cookie中。(瀏覽器可以存儲多個域名下的Cookie,但只發送當前請求的域名曾經指定的Cookie, 這個域名也可以在Set-Cookie字段中指定)。)
7.服務器收到這個HTTP請求,發現請求頭中有Cookie字段, 便知道之前就和這個用戶打過交道了.
8.過期的Cookie會被瀏覽器刪除。
總之,服務器通過Set-Cookie響應頭字段來指示瀏覽器保存Cookie, 瀏覽器通過Cookie請求頭字段來告訴服務器之前的狀態。 Cookie中包含若干個鍵值對,每個鍵值對可以設置過期時間。
Cookie 的安全隱患:
Cookie提供了一種手段使得HTTP請求可以附加當前狀態, 現今的網站也是靠Cookie來標識用戶的登錄狀態的:
1.用戶提交用戶名和密碼的表單,這通常是一個POST HTTP請求。
2.服務器驗證用戶名與密碼,如果合法則返回200(OK)并設置Set-Cookie為authed=true。
3.瀏覽器存儲該Cookie。
4.瀏覽器發送請求時,設置Cookie字段為authed=true。
5.服務器收到第二次請求,從Cookie字段得知該用戶已經登錄。 按照已登錄用戶的權限來處理此次請求。
問題是什么??風險是什么??
我們知道可以發送HTTP請求的不只是瀏覽器,很多HTTP客戶端軟件(包括curl、Node.js)都可以發送任意的HTTP請求,可以設置任何頭字段。 假如我們直接設置Cookie字段為authed=true并發送該HTTP請求, 服務器豈不是被欺騙了?這種攻擊非常容易,Cookie是可以被篡改的!
Cookie 防篡改機制:
服務器可以為每個Cookie項生成簽名,由于用戶篡改Cookie后無法生成對應的簽名, 服務器便可以得知用戶對Cookie進行了篡改。
例子:一個簡單的校驗過程:
1.在服務器中配置一個不為人知的字符串(我們叫它Secret),比如:x$sfz32。
2.當服務器需要設置Cookie時(比如authed=false),不僅設置authed的值為false, 在值的后面進一步設置一個簽名,最終設置的Cookie是authed=false|6hTiBl7lVpd1P。
3.簽名6hTiBl7lVpd1P是這樣生成的:Hash('x$sfz32'+'true')。 要設置的值與Secret相加再取哈希。
4.用戶收到HTTP響應并發現頭字段Set-Cookie: authed=false|6hTiBl7lVpd1P。
5.用戶在發送HTTP請求時,篡改了authed值,設置頭字段Cookie: authed=true|???。 因為用戶不知道Secret,無法生成簽名,只能隨便填一個。
6.服務器收到HTTP請求,發現Cookie: authed=true|???。服務器開始進行校驗: Hash('true'+'x$sfz32'),便會發現用戶提供的簽名不正確。
通過給Cookie添加簽名,使得服務器得以知道Cookie被篡改。但是!!還是有風險!
因為Cookie是明文傳輸的, 只要服務器設置過一次authed=true|xxxx我不就知道true的簽名是xxxx了么, 以后就可以用這個簽名來欺騙服務器了。因此Cookie中最好不要放敏感數據。 一般來講Cookie中只會放一個Session Id,而Session存儲在服務器端。
(3)session的實現機制:
1.概述:Session 是存儲在服務器端的,避免了在客戶端Cookie中存儲敏感數據。 Session 可以存儲在HTTP服務器的內存中,也可以存在內存數據庫(如redis)中, 對于重量級的應用甚至可以存儲在數據庫中。
例子:存儲在redis中的Session為例,考察如何驗證用戶登錄狀態的問題。
1.用戶提交包含用戶名和密碼的表單,發送HTTP請求。
2.服務器驗證用戶發來的用戶名密碼。
3.如果正確則把當前用戶名(通常是用戶對象)存儲到redis中,并生成它在redis中的ID。
這個ID稱為Session ID,通過Session ID可以從Redis中取出對應的用戶對象, 敏感數據(比如authed=true)都存儲在這個用戶對象中。
4.設置Cookie為sessionId=xxxxxx|checksum并發送HTTP響應, 仍然為每一項Cookie都設置簽名。
5.用戶收到HTTP響應后,便看不到任何敏感數據了。在此后的請求中發送該Cookie給服務器。
6.服務器收到此后的HTTP請求后,發現Cookie中有SessionID,進行放篡改驗證。
7.如果通過了驗證,根據該ID從Redis中取出對應的用戶對象, 查看該對象的狀態并繼續執行業務邏輯。
實現上述過程,在Web應用中可以直接獲得當前用戶。 相當于在HTTP協議之上,通過Cookie實現了持久的會話。這個會話便稱為Session。
三、Token認證機制相對于Cookie等機制的好處:
1. 支持跨域訪問: Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提是傳輸的用戶認證信息通過HTTP頭傳輸。(垮域訪問:兩個域名之間不能跨過域名來發送請求或者請求數據)
2.無狀態(也稱:服務端可擴展行):Token機制在服務端不需要存儲session信息,因為Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質存儲狀態信息.
3.更適用CDN: 可以通過內容分發網絡請求你服務端的所有資料(如:javascript,HTML,圖片等),而你的服務端只要提供API即可.
4.去耦: 不需要綁定到一個特定的身份驗證方案。Token可以在任何地方生成,只要在你的API被調用的時候,你可以進行Token生成調用即可.
5.更適用于移動應用: 當你的客戶端是一個原生平臺(iOS, Android,Windows 8等)時,Cookie是不被支持的(你需要通過Cookie容器進行處理),這時采用Token認證機制就會簡單得多。
6. CSRF:因為不再依賴于Cookie,所以你就不需要考慮對CSRF(跨站請求偽造)的防范。
7.性能: 一次網絡往返時間(通過數據庫查詢session信息)總比做一次HMACSHA256計算 的Token驗證和解析要費時得多.
8.不需要為登錄頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要為登錄頁面做特殊處理.
9.基于標準化:你的API可以采用標準化的 JSON Web Token (JWT). 這個標準已經存在多個后端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
四、JWT的Java實現:
概述:一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。這里我們只使用簡單的載荷,并將JSON對象進行base64編碼得到token
過程:登錄為例子
第一次認證:第一次登錄,用戶從瀏覽器輸入用戶名/密碼,提交后到服務器的登錄處理的Action層(controller)
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;
(1)使用JWT的包:maven導入
<!--JSON WEB TOKEN -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
(2)一個生成token的工具類:
public class JavaWebToken {
private static Logger log = Logger.getLogger(JavaWebToken.class);
private static Key getKeyInstance() {
// return MacProvider.generateKey();
//We will sign our JavaWebToken with our ApiKey secret
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("APP");
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
return signingKey;
}
public static String createJavaWebToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance()).compact();
}
public static Map<String, Object> verifyJavaWebToken(String jwt) {
try {
Map<String, Object> jwtClaims =
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody();
return jwtClaims;
} catch (Exception e) {
log.error("json web token verify failed");
return null;
}
}
}
(3)一個從request拿去session,并且解密session得到token得到用戶id的類
public class AuthUtil {
private static Map<String, Object> getClientLoginInfo(HttpServletRequest request) throws Exception {
Map<String, Object> r = new HashMap<>();
String sessionId = request.getHeader("sessionId");
if (sessionId != null) {
r = decodeSession(sessionId);
return r;
}
throw new Exception("session解析錯誤");
}
public static Long getUserId(HttpServletRequest request) throws Exception {
return Long.valueOf((Integer)getClientLoginInfo(request).get("userId"));
}
/**
* session解密
*/
public static Map<String, Object> decodeSession(String sessionId) {
try {
return verifyJavaWebToken(sessionId);
} catch (Exception e) {
System.err.println("");
return null;
}
}
}
使用例子:
登錄的時候把信息放進session,存到map里,再交由JWT得到token保存起來
這里寫圖片描述
//登錄
@RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.POST}, produces = "text/html;charset=UTF-8")
public String login(String account) {
User user = userService.login(account);
DTO dto = new DTO();
if (user == null) {
dto.code = "-1";
dto.msg = "Have not registered";
} else {
//把用戶登錄信息放進Session
Map<String, Object> loginInfo = new HashMap<>();
loginInfo.put("userId", user.getId());
String sessionId = JavaWebToken.createJavaWebToken(loginInfo);
System.out.println("sessionID"+sessionId);
dto.data = sessionId;
}
return JSON.toJSONString(dto);
}
用戶登錄以后,其他的用戶性知道的操作就可以使用token進行了,安全快捷方便:
這里寫圖片描述
//修改昵稱
@RequestMapping(value = "/updateName", method = {RequestMethod.GET, RequestMethod.POST})
public String updateName(HttpServletRequest request, String name) {
DTO dto = new DTO();
try {
//從session拿到token,再解密得到userid
Long userId = AuthUtil.getUserId(request);
boolean userIsExist = userService.updateName(userId, name);
if (userIsExist == false) {
dto.code = "-1";
dto.msg = "Have not updateAvatar";
}
} catch (Exception e) {
e.printStackTrace();
}
return JSON.toJSONString(dto);
}