1. 程式人生 > 實用技巧 >使用JWT實現單點登入(完全跨域方案)單點登入SSO技術選型

使用JWT實現單點登入(完全跨域方案)單點登入SSO技術選型

首先介紹一下什麼是JSON Web Token(JWT)?

官方文件是這樣解釋的:JSON Web Token(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON物件安全地傳輸資訊。此資訊可以通過數字簽名進行驗證和信任。JWT可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
雖然JWT可以加密以在各方之間提供保密,但只將專注於簽名令牌。簽名令牌可以驗證其中包含的宣告的完整性,而加密令牌則隱藏其他方的宣告。當使用公鑰/私鑰對簽署令牌時,簽名還證明只有持有私鑰的一方是簽署私鑰的一方。

通俗來講,JWT是一個含簽名並攜帶使用者相關資訊的加密串,頁面請求校驗登入介面時,請求頭中攜帶JWT串到後端服務,後端通過簽名加密串匹配校驗,保證資訊未被篡改。校驗通過則認為是可靠的請求,將正常返回資料。

什麼情況下使用JWT比較適合?

  • 授權:這是最常見的使用場景,解決單點登入問題。因為JWT使用起來輕便,開銷小,服務端不用記錄使用者狀態資訊(無狀態),所以使用比較廣泛;
  • 資訊交換:JWT是在各個服務之間安全傳輸資訊的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。

JWT的結構體是什麼樣的?

JWT由三部分組成,分別是頭資訊、有效載荷、簽名,中間以(.)分隔,如下格式:

xxx.yyy.zzz
  • 1

header(頭資訊)
由兩部分組成,令牌型別(即:JWT)、雜湊演算法(HMAC、RSASSA、RSASSA-PSS等),例如:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • 1
  • 2
  • 3
  • 4

然後,這個JSON被編碼為Base64Url,形成JWT的第一部分。

Payload(有效載荷)
JWT的第二部分是payload,其中包含claims。claims是關於實體(常用的是使用者資訊)和其他資料的宣告,claims有三種類型: registered, public, and private claims。
Registered claims:這些是一組預定義的claims,非強制性的,但是推薦使用, iss(發行人), exp(到期時間), sub(主題), aud(觀眾)等;
Public claims:自定義claims,注意不要和JWT登錄檔中屬性衝突,

這裡可以檢視JWT登錄檔
Private claims:這些是自定義的claims,用於在同意使用這些claims的各方之間共享資訊,它們既不是Registered claims,也不是Public claims。
以下是payload示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  • 1
  • 2
  • 3
  • 4
  • 5

然後,再經過Base64Url編碼,形成JWT的第二部分;

注意:對於簽名令牌,此資訊雖然可以防止篡改,但任何人都可以讀取。除非加密,否則不要將敏感資訊放入到Payload或Header元素中。

Signature
要建立簽名部分,必須採用編碼的Header,編碼的Payload,祕鑰,Header中指定的演算法,並對其進行簽名。
例如,如果要使用HMAC SHA256演算法,將按以下方式建立簽名:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
  • 1
  • 2
  • 3
  • 4

簽名用於驗證訊息在此過程中未被篡改,並且,在使用私鑰簽名令牌的情況下,它還可以驗證JWT的請求方是否是它所宣告的請求方。
輸出是三個由點分隔的Base64-URL字串,可以在HTML和HTTP環境中輕鬆傳遞,與SAML等基於XML的標準相比更加緊湊。
例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • 1
  • 2
  • 3

JWT工作機制?

在身份驗證中,當用戶使用其憑據成功登入時,將返回JSON Web Token(即:JWT)。由於令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,不應將令牌保留的時間超過要求。理論上超時時間越短越好。

每當使用者想要訪問受保護的路由或資源時,使用者代理應該使用Bearer模式傳送JWT,通常在Authorization header中。標題內容應如下所示:

Authorization: Bearer <token>
  • 1

在某些情況下,這可以作為無狀態授權機制。伺服器的受保護路由將檢查Authorization header中的有效JWT ,如果有效,則允許使用者訪問受保護資源。如果JWT包含必要的資料,則可以減少查詢資料庫或快取資訊。
如果在Authorization header中傳送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。

注意:使用簽名令牌,雖然他們無法更改,但是令牌中包含的所有資訊都會向用戶或其他方公開。這意味著不應該在令牌中放置敏感資訊。

使用JWT的好處是什麼?

相比Simple Web Tokens (SWT)(簡單Web令牌)andSecurity Assertion Markup Language Tokens (SAML)(安全斷言標記語言令牌);

  • JWT比SAML更簡潔,在HTML和HTTP環境中傳遞更方便;
  • 在安全方面,SWT只能使用HMAC演算法通過共享金鑰對稱簽名。但是,JWT和SAML令牌可以使用X.509證書形式的公鑰/私鑰對進行簽名。與簽名JSON的簡單性相比,使用XML數字簽名可能會存在安全漏洞;
  • JSON解析成物件相比XML更流行、方便。

以下是我實際專案中的應用分析

首先看一下大致的架構及流程圖:

主要有以下三步:

  • 專案一開始我先封裝了一個JWTHelper工具包(GitHub下載),主要提供了生成JWT、解析JWT以及校驗JWT的方法,其他還有一些加密相關操作,稍後我會以程式碼的形式介紹下程式碼。工具包寫好後我將打包上傳到私服,能夠隨時依賴下載使用;
  • 接下來,我在客戶端專案中依賴JWTHelper工具包,並新增Interceptor攔截器,攔截需要校驗登入的介面。攔截器中校驗JWT有效性,並在response中重新設定JWT的新值;
  • 最後在JWT服務端,依賴JWT工具包,在登入方法中,需要在登入校驗成功後呼叫生成JWT方法,生成一個JWT令牌並且設定到response的header中。

以下是部分程式碼分享:

  • JwtHelper工具類:
/**
 * @Author: Helon
 * @Description: JWT工具類
 * 參考官網:https://jwt.io/
 * JWT的資料結構為:A.B.C三部分資料,由字元點"."分割成三部分資料
 * A-header頭資訊
 * B-payload 有效負荷 一般包括:已註冊資訊(registered claims),公開資料(public claims),私有資料(private claims)
 * C-signature 簽名信息 是將header和payload進行加密生成的
 * @Data: Created in 2018/7/19 14:11
 * @Modified By:
 */
public class JwtHelper {

    private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);

    /**
     * @Author: Helon
     * @Description: 生成JWT字串
     * 格式:A.B.C
     * A-header頭資訊
     * B-payload 有效負荷
     * C-signature 簽名信息 是將header和payload進行加密生成的
     * @param userId - 使用者編號
     * @param userName - 使用者名稱
     * @param identities - 客戶端資訊(變長引數),目前包含瀏覽器資訊,用於客戶端攔截器校驗,防止跨域非法訪問
     * @Data: 2018/7/28 19:26
     * @Modified By:
     */
    public static String generateJWT(String userId, String userName, String ...identities) {
        //簽名演算法,選擇SHA-256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //獲取當前系統時間
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        //將BASE64SECRET常量字串使用base64解碼成位元組陣列
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
        //使用HmacSHA256簽名演算法生成一個HS256的簽名祕鑰Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //新增構成JWT的引數
        Map<String, Object> headMap = new HashMap<>();
        /*
            Header
            {
              "alg": "HS256",
              "typ": "JWT"
            }
         */
        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                /*
                    Payload
                    {
                      "userId": "1234567890",
                      "userName": "John Doe",
                    }
                 */
                //加密後的客戶編號
                .claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
                //客戶名稱
                .claim("userName", userName)
                //客戶端瀏覽器資訊
                .claim("userAgent", identities[0])
                //Signature
                .signWith(signatureAlgorithm, signingKey);
        //新增Token過期時間
        if (SecretConstant.EXPIRESSECOND >= 0) {
            long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /**
     * @Author: Helon
     * @Description: 解析JWT
     * 返回Claims物件
     * @param jsonWebToken - JWT
     * @Data: 2018/7/28 19:25
     * @Modified By:
     */
    public static Claims parseJWT(String jsonWebToken) {
        Claims claims = null;
        try {
            if (StringUtils.isNotBlank(jsonWebToken)) {
                //解析jwt
                claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
                        .parseClaimsJws(jsonWebToken).getBody();
            }else {
                logger.warn("[JWTHelper]-json web token 為空");
            }
        } catch (Exception e) {
            logger.error("[JWTHelper]-JWT解析異常:可能因為token已經超時或非法token");
        }
        return claims;
    }

    /**
     * @Author: Helon
     * @Description: 校驗JWT是否有效
     * 返回json字串的demo:
     * {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
     * freshToken-重新整理後的jwt
     * userName-客戶名稱
     * userId-客戶編號
     * userAgent-客戶端瀏覽器資訊
     * @param jsonWebToken - JWT
     * @Data: 2018/7/24 15:28
     * @Modified By:
     */
    public static String validateLogin(String jsonWebToken) {
        Map<String, Object> retMap = null;
        Claims claims = parseJWT(jsonWebToken);
        if (claims != null) {
            //解密客戶編號
            String decryptUserId = AESSecretUtil.decryptToStr((String)claims.get("userId"), SecretConstant.DATAKEY);
            retMap = new HashMap<>();
            //加密後的客戶編號
            retMap.put("userId", decryptUserId);
            //客戶名稱
            retMap.put("userName", claims.get("userName"));
            //客戶端瀏覽器資訊
            retMap.put("userAgent", claims.get("userAgent"));
            //重新整理JWT
            retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));
        }else {
            logger.warn("[JWTHelper]-JWT解析出claims為空");
        }
        return retMap!=null?JSONObject.toJSONString(retMap):null;
    }

    public static void main(String[] args) {
       String jsonWebKey = generateJWT("123", "Judy",
               "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
       System.out.println(jsonWebKey);
       Claims claims =  parseJWT(jsonWebKey);
        System.out.println(claims);
       System.out.println(validateLogin(jsonWebKey));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • AES加密工具類:
/**
 * @Author: Helon
 * @Description: AES加密工具類
 * @Data: Created in 2018/7/28 18:38
 * @Modified By:
 */
public class AESSecretUtil {

    /**祕鑰的大小*/
    private static final int KEYSIZE = 128;
    
    /**
     * @Author: Helon
     * @Description: AES加密
     * @param data - 待加密內容
     * @param key - 加密祕鑰
     * @Data: 2018/7/28 18:42
     * @Modified By:
     */
    public static byte[] encrypt(String data, String key) {
        if(StringUtils.isNotBlank(data)){
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                //選擇一種固定演算法,為了避免不同java實現的不同演算法,生成不同的金鑰,而導致解密失敗
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 建立密碼器
                byte[] byteContent = data.getBytes("utf-8");
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化
                byte[] result = cipher.doFinal(byteContent);
                return result; // 加密
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * @Author: Helon
     * @Description: AES加密,返回String
     * @param data - 待加密內容
     * @param key - 加密祕鑰
     * @Data: 2018/7/28 18:59
     * @Modified By:
     */
    public static String encryptToStr(String data, String key){

        return StringUtils.isNotBlank(data)?parseByte2HexStr(encrypt(data, key)):null;
    }


    /**
     * @Author: Helon
     * @Description: AES解密
     * @param data - 待解密位元組陣列
     * @param key - 祕鑰
     * @Data: 2018/7/28 19:01
     * @Modified By:
     */
    public static byte[] decrypt(byte[] data, String key) {
        if (ArrayUtils.isNotEmpty(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                //選擇一種固定演算法,為了避免不同java實現的不同演算法,生成不同的金鑰,而導致解密失敗
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 建立密碼器
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化
                byte[] result = cipher.doFinal(data);
                return result; // 加密
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * @Author: Helon
     * @Description: AES解密,返回String
     * @param enCryptdata - 待解密位元組陣列
     * @param key - 祕鑰
     * @Data: 2018/7/28 19:01
     * @Modified By:
     */
    public static String decryptToStr(String enCryptdata, String key) {
        return StringUtils.isNotBlank(enCryptdata)?new String(decrypt(parseHexStr2Byte(enCryptdata), key)):null;
    }

    /**
     * @Author: Helon
     * @Description: 將二進位制轉換成16進位制
     * @param buf - 二進位制陣列
     * @Data: 2018/7/28 19:12
     * @Modified By:
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * @Author: Helon
     * @Description: 將16進位制轉換為二進位制
     * @param hexStr - 16進位制字串
     * @Data: 2018/7/28 19:13
     * @Modified By:
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length()/2];
        for (int i = 0;i< hexStr.length()/2; i++) {
            int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
            int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

    public static void main(String[] args) {
        String ss = encryptToStr("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJ1c2VyTmFtZSI6Ikp1ZHkiLCJleHAiOjE1MzI3Nzk2MjIsIm5iZiI6MTUzMjc3NzgyMn0.sIw_leDZwG0pJ8ty85Iecd_VXjObYutILNEwPUyeVSo", SecretConstant.DATAKEY);
        System.out.println(ss);
        System.out.println(decryptToStr(ss, SecretConstant.DATAKEY));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 所使用的常量類:
/**
 * @Author: Helon
 * @Description: JWT使用常量值
 * @Data: Created in 2018/7/27 14:37
 * @Modified By:
 */
public class SecretConstant {

    //簽名祕鑰 自定義
    public static final String BASE64SECRET = "***********";

    //超時毫秒數(預設30分鐘)
    public static final int EXPIRESSECOND = 1800000;

    //用於JWT加密的密匙 自定義
    public static final String DATAKEY = "************";

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 客戶端pom依賴:
        <!--jwt工具類-->
        <dependency>
            <groupId>com.chtwm.component</groupId>
            <artifactId>jwt-helper</artifactId>
            <version>xxx</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 客戶端攔截器:
/**
 * @Author: Helon
 * @Description: 校驗是否登入攔截器
 * @Data: Created in 2018/7/30 14:30
 * @Modified By:
 */
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        //首先從請求頭中獲取jwt串,與頁面約定好存放jwt值的請求頭屬性名為User-Token
        String jwt = httpServletRequest.getHeader("User-Token");
        log.info("[登入校驗攔截器]-從header中獲取的jwt為:{}", jwt);
        //判斷jwt是否有效
        if(StringUtils.isNotBlank(jwt)){
            //校驗jwt是否有效,有效則返回json資訊,無效則返回空
            String retJson = JwtHelper.validateLogin(jwt);
            log.info("[登入校驗攔截器]-校驗JWT有效性返回結果:{}", retJson);
            //retJSON為空則說明jwt超時或非法
            if(StringUtils.isNotBlank(retJson)){
                JSONObject jsonObject = JSONObject.parseObject(retJson);
                //校驗客戶端資訊
                String userAgent = httpServletRequest.getHeader("User-Agent");
                if (userAgent.equals(jsonObject.getString("userAgent"))) {
                    //獲取重新整理後的jwt值,設定到響應頭中
                    httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
                    //將客戶編號設定到session中
                    httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
                    return true;
                }else{
                    log.warn("[登入校驗攔截器]-客戶端瀏覽器資訊與JWT中存的瀏覽器資訊不一致,重新登入。當前瀏覽器資訊:{}", userAgent);
                }
            }else {
                log.warn("[登入校驗攔截器]-JWT非法或已超時,重新登入");
            }
        }
        //輸出響應流
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hmac", "");
        jsonObject.put("status", "");
        jsonObject.put("code", "4007");
        jsonObject.put("msg", "未登入");
        jsonObject.put("data", "");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 客戶端攔截器在XML檔案中配置:
	<!--攔截器配置-->
    <mvc:interceptors>
        <mvc:interceptor>
	        <!--需攔截url配置-->
            <mvc:exclude-mapping path="/api/aa/bb/**" />
            <mvc:exclude-mapping path="/api/aa/cc/test" />
            <bean id="validateLoginInterceptor" class="com.xxx.ValidateLoginInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到此,後臺服務的配置已經完成,下一步就需要前端頁面將JWT令牌從response響應頭中取出,然後存入localstorage或cookie中。但是遇到跨域場景,處理起來就會比較複雜,因為一旦在瀏覽器中跨域將獲取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是獲取不到的,所以我選擇了一種頁面跨域的方式進行處理,使用iframe+H5的postMessage(參考博文),具體我使用程式碼分享的方式來分析。

  • 前端頁面js程式碼(服務端):
    /**CURD本地儲存資訊 start**/
          (function(doc,win){
              var fn=function(){};
              fn.prototype={
                  /*本地資料儲存 t:cookie有效時間,單位s; domain:cookie儲存所屬的domain域*/
                  setLocalCookie: function (k, v, t,domain) {
                      //如果當前瀏覽器不支援localStorage將儲存在cookie中
                      typeof window.localStorage !== "undefined" ? localStorage.setItem(k, v) :
                          (function () {
                              t = t || 365 * 12 * 60 * 60;
                              domain=domain?domain:".jwtserver.com";
                              document.cookie = k + "=" + v + ";max-age=" + t+";domain="+domain+";path=/";
                          })()
                  },
                  /*獲取本地儲存資料*/
                  getLocalCookie: function (k) {
                      k = k || "localDataTemp";
                      return typeof window.localStorage !== "undefined" ? localStorage.getItem(k) :
                          (function () {
                              var all = document.cookie.split(";");
                              var cookieData = {};
                              for (var i = 0, l = all.length; i < l; i++) {
                                  var p = all[i].indexOf("=");
                                  var dataName = all[i].substring(0, p).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");
                                  cookieData[dataName] = all[i].substring(p + 1);
                              }
                              return cookieData[k]
                          })();
                  },
                  /*刪除本地儲存資料*/
                  clearLocalData: function (k) {
                      k = k || "localDataTemp";
                      typeof window.localStorage !== "undefined" ? localStorage.removeItem(k) :
                          (function () {
                              document.cookie = k + "=temp" + ";max-age=0";
                          })()
                  },
                  init:function(){
                      this.bindEvent();
                  },
                  //事件繫結
                  bindEvent:function(){
                      var _this=this;
                      win.addEventListener("message",function(evt){
                          if(win.parent!=evt.source){return}
                          var options=JSON.parse(evt.data);
                          if(options.type=="GET"){
                              var data=tools.getLocalCookie(options.key);
                              win.parent.postMessage(data, "*");
                          }
                          options.type=="SET"&&_this.setLocalCookie(options.key,options.value);
                          options.type=="REM"&&_this.clearLocalData(options.key);
                      },false)
                  }
              };
              var tools=new fn();
              tools.init();
          })(document,window);
          /**CURD本地儲存資訊 end**/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 前端頁面js程式碼(客戶端):
        //頁面初始化向iframe域名傳送訊息
        window.onload = function() {
            console.log('get key value......................')
            window.frames[0].postMessage(JSON.stringify({type:"GET",key:"User-Token"}),'*');
        }
        //監聽message資訊,接收從iframe域下獲取到的token資訊,然後儲存到localstorage或cookie中
        window.addEventListener('message', function(e) {
            console.log('listen.....');
            var data = e.data;
            console.log(data);
            if(data != null){
                localStorage.setItem("User-Token", data);
            }
        }, false);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

總結:
優點:在非跨域環境下使用JWT機制是一個非常不錯的選擇,實現方式簡單,操作方便,能夠快速實現。由於服務端不儲存使用者狀態資訊,因此大使用者量,對後臺服務也不會造成壓力;
缺點:跨域實現相對比較麻煩,安全性也有待探討。因為JWT令牌返回到頁面中,可以使用js獲取到,如果遇到XSS攻擊令牌可能會被盜取,在JWT還沒超時的情況下,就會被獲取到敏感資料資訊。