一、跨域認證遇到的問題
由於多終端的出現,很多的站點通過 web api restful
的形式對外提供服務,采用了前后端分離模式進行開發,因而在身份驗證的方式上可能與傳統的基於 cookie
的 Session Id
的做法有所不同,除了面臨跨域提交 cookie
的問題外,更重要的是,有些終端可能根本不支持 cookie
。
JWT(JSON Web Token)
是一種身份驗證及授權方案,簡單的說就是調用端調用 api
時,附帶上一個由 api
端頒發的 token
,以此來驗證調用者的授權信息。
一般流程是下面這樣:
1. 用戶向服務器發送用戶名和密碼。
2. 服務器驗證通過后,在當前對話(session)里面保存相關數據,比如用戶角色、登錄時間等等。
3. 服務器向用戶返回一個 session_id,寫入用戶的 Cookie。
4. 用戶隨后的每一次請求,都會通過 Cookie,將 session_id 傳回服務器。
5. 服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。
這種模式的問題在於擴展性不好。單機沒有問題,如果是服務器集群、跨域的服務導向架構或者用戶禁用了 cookie
,就不行了。
二、解決方案
1. 單機和分布式應用下登錄校驗,session 共享
-
單機和多節點
tomcat
應用登錄檢驗①、單機
tomcat
應用登錄,sesssion
保存在瀏覽器和應用服務器會話之間,用戶登錄成功后,服務端會保證一個session
,也會給客戶端一個sessionId
,客戶端會把sessionId
保存在cookie
中,用戶每次請求都會攜帶這個sessionId
。②、多節點
tomcat
應用登錄,開啟session
數據共享后,每台服務器都能夠讀取session
。缺點是每個session
都是占用內存和資源的,每個服務器節點都需要同步用戶的數據,即一個數據需要存儲多份到每個服務器,當用戶量到達百萬、千萬級別的時,占用資源就嚴重,用戶體驗特別不好!! -
分布式應用中
session
共享①、真實的應用不可能單節點部署,所以就有個多節點登錄
session
共享的問題需要解決。tomcat
支持session
共享,但是有廣播風暴;用戶量大的時候,占用資源就嚴重,不推薦②、
Reids
集群,存儲登陸的token
,向外提供服務接口,Redis
可設置過期時間(服務端使用UUID
生成隨機64
位或者128
位token
,放入Redis
中,然后返回給客戶端並存儲)。③、用戶第一次登錄成功時,需要先自行生成
token
,然后將token
返回到瀏覽器並存儲在cookie
中,
並在Redis
服務器上以token
為key
,用戶信息作為value
保存。后續用戶再操作,可以通過HttpServletRequest
對象直接讀取cookie
中的token
,並在Redis
中取得相對應的用戶數據進行比較(用戶每次訪問都攜帶此token
,服務端去Redis
中校驗是否有此用戶即可)。④、 缺點:必須部署
Redis
,每次必須訪問Redis
,IO
開銷特別大。
2. 最終解決方案:使用 JWT 實現 Token 認證
-
JWT 的原理
服務器認證以后,生成一個 JSON 對象發回給用戶,以后用戶與服務端通信的時候,都要發回這個 JSON 對象。服務器完全只靠這個對象認定用戶身份。為了防止用戶篡改數據,服務器在生成這個對象的時候,會加上簽名。也就是說服務器就不保存任何 session 數據了,即服務器變成無狀態了,從而比較容易實現擴展。
簡單來說,就是通過一定規范來生成 token,然后可以通過解密算法逆向解密 token,這樣就可以獲取用戶信息
-
優點和缺點
優點:生產的 token 可以包含基本信息,比如 id、用戶昵稱、頭像等信息,避免再次查庫;存儲在客戶端,不占用服務端的內存資源
缺點:token 是經過 base64 編碼,所以可以解碼,因此 token 加密前的對象不應該包含敏感信息(如用戶權限,密碼等)
-
JWT 格式組成:頭部+負載+簽名 ( header + payload + signature )
頭部:主要是描述簽名算法。
負載:主要描述是加密對象的信息,如用戶的 id 等,也可以加些規范里面的東西,如 iss 簽發者,exp 過期時間,sub 面向的用戶。
簽名:主要是把前面兩部分進行加密,防止別人拿到 token 進行base 解密后篡改 token。
3. 案例圖設計
三、代碼演示案例
-
pom.xml 文件引入依賴和實體類
<!-- 依賴可以減少實體類 getter/setter等方法書寫 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- JWT相關 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> ==================================================================================== @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String openid; private String name; private String headImg; private String phone; private String sign; private Integer sex; private String city; private Date createTime; }
-
生成 JWT 工具類
public class JwtUtil { // 主題 public static final String SUBJECT = "RookieLi"; // 秘鑰 public static final String SECRETKEY = "Rookie666"; // 過期時間 public static final long EXPIRE = 1000 * 60 * 60 * 24 * 7; //過期時間,毫秒,一周 // 生成 JWT public static String geneJsonWebToken(User user) { if (user == null || user.getId() == null || user.getName() == null || user.getHeadImg() == null) { return null; } String token = Jwts.builder() .setSubject(SUBJECT) .claim("id", user.getId()) .claim("name", user.getName()) .claim("img", user.getHeadImg()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRETKEY).compact(); return token; } // 校驗 JWT public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(SECRETKEY). parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); } return null; } }
-
測試 JWT 工具類
public class JwtUtilTest { @Test public void testGeneJwt(){ User user = new User(); user.setId(999); user.setHeadImg("I'm busy"); user.setName("Rookie"); String token = JwtUtil.geneJsonWebToken(user); System.out.println(token); } @Test public void testCheck(){ // 下面此 token 字符串是上面的結果生成的,每次不一樣,不是寫死的 String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJSb29raWVMaSIsImlkIjo5OTksIm5hbWUiOiJSb29raWUiLCJpbWciOiJJJ20gYnVzeSIsImlhdCI6MTU2NzMxNjk4NywiZXhwIjoxNTY3OTIxNzg3fQ.FJh41VwVh2gh5-_cOG0SOgoO3dR_ZcK9VWNNskWqKl0"; Claims claims = JwtUtil.checkJWT(token); if(claims != null){ String name = (String)claims.get("name"); String img = (String)claims.get("img"); int id =(Integer) claims.get("id"); System.out.println(name); System.out.println(img); System.out.println(id); }else{ System.out.println("非法token"); } } }
參考博客:
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html