SpringBoot 2.x 使用 JWT(JSON Web Token)


一、跨域認證遇到的問題

由於多終端的出現,很多的站點通過 web api restful 的形式對外提供服務,采用了前后端分離模式進行開發,因而在身份驗證的方式上可能與傳統的基於 cookieSession 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 位或者 128token ,放入 Redis 中,然后返回給客戶端並存儲)。

    ③、用戶第一次登錄成功時,需要先自行生成 token,然后將 token 返回到瀏覽器並存儲在 cookie 中,
    並在 Redis 服務器上以 tokenkey,用戶信息作為 value 保存。后續用戶再操作,可以通過 HttpServletRequest 對象直接讀取 cookie 中的 token,並在 Redis 中取得相對應的用戶數據進行比較(用戶每次訪問都攜帶此 token,服務端去 Redis 中校驗是否有此用戶即可)。

    ④、 缺點:必須部署 Redis,每次必須訪問 RedisIO 開銷特別大。

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

    https://www.cnblogs.com/jpfss/p/10929458.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM