If you still don’t know what JWT is, you can refer to the following article, portal:

JWT Single Sign-on (Introduction)

The following is the analysis of the application in the actual project. First, take a look at the general data flow diagram:

First, the idea of implementation

1. At the beginning of the project, I encapsulated a JWTHelper toolkit, which mainly provided methods to generate JWT, parse JWT and verify JWT, as well as some encryption related operations. I will introduce codes in the form of codes later. After the toolkit is written, I will upload the package to the private server and can rely on download at any time.

2. Next, I rely on the JWTHelper toolkit in my client project and add an Interceptor Interceptor that intercepts interfaces that need to verify login. Verify JWT validity in interceptor and reset the new value of JWT in Response;

3. Finally, the JWT server relies on the JWT toolkit. In the login method, it is necessary to call the JWT generation method after the login verification is successful to generate a JWT token and set it into the header of Response. The following part of the code is introduced

Two, code implementation

1. JwtHelper utility class

/** * JWT utility class *@author zhangzhixiang
 */
@Slf4j
@SuppressWarnings("restriction")
public class JwtHelper {

    /** * JWT is generated in the following format: A.B. Ca -header header information B-payload C-signature signature information ** Is generated by encrypting header and payload@paramUserId userId *@paramUserName userName *@paramIdentities client information (variable length parameter), currently contains browser information, which is used to verify client interceptors and prevent cross-domain unauthorized access *@return* /
    public static String generateJWT(String userId, String userName, String... identities) {
        // Select SHA-256 for signature algorithm
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // Get the current system time
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        // Decodes the BASE64SECRET constant string into a byte array using base64
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
        // Use the HmacSHA256 signature algorithm to generate a HS256 signature Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        // Add the parameters that make up JWT
        Map<String, Object> headMap = new HashMap<String, Object>();
        // Header { "alg": "HS256", "typ": "JWT" }
        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("type"."JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                // Payload { "userId": "1234567890", "userName": "vic", }
                // Encrypted customer number
                .claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
                // Client name
                .claim("userName", userName)
                // Client browser information
                .claim("userAgent", identities[0])
                // Signature
                .signWith(signatureAlgorithm, signingKey);
        // Add the Token expiration time
        if (SecretConstant.EXPIRESSECOND >= 0) {
            long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /** * Parsing JWT returns Claims object **@param jsonWebToken
     * @return* /
    public static Claims parseJWT(String jsonWebToken) {
        Claims claims = null;
        try {
            if (StringUtils.isNotBlank(jsonWebToken)) {
                / / parsing JWT
                claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
                        .parseClaimsJws(jsonWebToken).getBody();
            } else {
                log.warn("[JWTHelper]- JSON Web Token is empty"); }}catch (Exception e) {
            log.error("[JWTHelper]-JWT resolution exception: Token timeout or invalid token");
        }
        return claims;
    }

    /** * Return json string demo: * {"freshToken":"A.B.C","userName":"vic","userId":"123", "UserAgent ":" XXXX "} * freshToken- refreshed JWT userName- Client name userId- Client number userAgent- Client browser information * *@param jsonWebToken
     * @return* /
    public static String validateLogin(String jsonWebToken) {
        Map<String, Object> retMap = null;
        Claims claims = parseJWT(jsonWebToken);
        if(claims ! =null) {
            // Decrypt the customer number
            String decryptUserId = AESSecretUtil.decryptToStr((String) claims.get("userId"), SecretConstant.DATAKEY);
            retMap = new HashMap<String, Object>();
            // Encrypted customer number
            retMap.put("userId", decryptUserId);
            // Client name
            retMap.put("userName", claims.get("userName"));
            // Client browser information
            retMap.put("userAgent", claims.get("userAgent"));
            / / refresh JWT
            retMap.put("freshToken", generateJWT(decryptUserId, (String) claims.get("userName"),
                    (String) claims.get("userAgent"), (String) claims.get("domainName")));
        } else {
            log.warn("[JWTHelper]-JWT resolves claims null");
        }
        returnretMap ! =null ? JSONObject.toJSONString(retMap) : null; }}Copy the code

2. AES encryption tool class

/** * AES encryption tool class *@author zhangzhixiang
 */
public class AESSecretUtil {
    /** * The size of the key */
    private static final int KEYSIZE = 128;

    /** * AES encryption **@paramData Indicates the content to be encrypted@paramKey Indicates the secret key *@return* /
    public static byte[] encrypt(String data, String key) {
        if (StringUtils.isNotBlank(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                // Select a fixed algorithm, in order to avoid different Java implementations of different algorithms, generate different keys, resulting in decryption failure
                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");// Create a password
                byte[] byteContent = data.getBytes("utf-8");
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);/ / initialization
                byte[] result = cipher.doFinal(byteContent);
                return result; / / encryption
            } catch(Exception e) { e.printStackTrace(); }}return null;
    }

    /** * AES encryption. Mandatory String **@paramData Indicates the content to be encrypted@paramKey Indicates the secret key *@return* /
    public static String encryptToStr(String data, String key) {

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

    /** * AES decryption **@paramData Array of bytes to be decrypted *@paramThe key secret key *@return* /
    public static byte[] decrypt(byte[] data, String key) {
        if (ArrayUtils.isNotEmpty(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                // Select a fixed algorithm, in order to avoid different Java implementations of different algorithms, generate different keys, resulting in decryption failure
                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");// Create a password
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);/ / initialization
                byte[] result = cipher.doFinal(data);
                return result; / / encryption
            } catch(Exception e) { e.printStackTrace(); }}return null;
    }

    /** * AES decryption. Mandatory String **@paramEnCryptdata Array of bytes to be decrypted@paramThe key secret key *@return* /
    public static String decryptToStr(String enCryptdata, String key) {
        return StringUtils.isNotBlank(enCryptdata) ? new String(decrypt(parseHexStr2Byte(enCryptdata), key)) : null;
    }

    /** * convert binary to hexadecimal **@paramBuf binary array * */
    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();
    }

    /** * converts hexadecimal to binary **@paramHexStr hexadecimal string *@return* /
    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);
        }
        returnresult; }}Copy the code

Constant class

/** * JWT constant value *@author zhangzhixiang
 */
public interface SecretConstant {
    // Sign the key
    public static final String BASE64SECRET = "ZW]4l5JH[m6Lm)LaQEjpb!4E0lRaG(";

    // Timeout milliseconds (default 30 minutes)
    public static final int EXPIRESSECOND = 1800000;

    // The key used for JWT encryption
    public static final String DATAKEY = "u^3y6SPER41jm*fn";
}
Copy the code

4. Client interceptor

/** * Validates whether to log in to interceptor *@author zhangzhixiang
 */
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor {


    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        // Get the JWT string from the request header, which is named user-token
        String jwt = httpServletRequest.getHeader("User-Token");
        log.info("[Login verification interceptor]- JWT from header is :{}", jwt);
        // Determine whether JWT is valid
        if(StringUtils.isNotBlank(jwt)){
            If JWT is valid, json information is returned; if JWT is invalid, nothing is returned
            String retJson = JwtHelper.validateLogin(jwt);
            log.info("[Login verify interceptor]- Verify JWT validity result :{}", retJson);
            // If retJSON is empty, JWT times out or is invalid
            if(StringUtils.isNotBlank(retJson)){
                JSONObject jsonObject = JSONObject.parseObject(retJson);
                // Verify client information
                String userAgent = httpServletRequest.getHeader("User-Agent");
                if (userAgent.equals(jsonObject.getString("userAgent"))) {
                    // Get the refreshed JWT value and set it to the response header
                    httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
                    // Set the client number to session
                    httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
                    return true;
                }else{
                    log.warn("[Login verification interceptor]- Client browser information is inconsistent with the browser information stored in JWT, log in again. Current browser information :{}", userAgent); }}else {
                log.warn("[Login verification interceptor]-JWT invalid or timed out, log in again"); }}// Outputs the response flow
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hmac"."");
        jsonObject.put("status"."");
        jsonObject.put("code"."100");
        jsonObject.put("msg"."Not logged in");
        jsonObject.put("data"."");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));
        return false;
    }

    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}}Copy the code

At this point, the background service configuration is complete.

The next step is for the front-end page to extract the JWT token from the response header and store it in Localstorage or cookies. However, in cross-domain scenarios, processing is more complicated because once cross-domain is available in the browser, the JWT token in LocalStorage is not available. For example, JWT in www.a.com domain cannot be obtained in www.b.com domain, so I choose a cross-domain page processing method and use iframe+H5 postMessage. Specifically, I use the code sharing method to analyze, and the code is as follows:

(function(doc,win){
    var fn=function(){};
    fn.prototype={
        /* Local data store t:cookie validity time, unit s; Domain: indicates the domain to which the cookie resides. Domain */
        setLocalCookie:function(k,v,t,domain){
            // If the current browser does not support localStorage, it will be stored in cookies
            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=/";
            })()
        },
        /* Get local storage data */
        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]
                })();
        },
        /* Delete local storage data */
        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();
        },
        // Event binding
        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=newfn(); tools.init(); }) (document.window);
Copy the code

Front-end page JS code (client) :

// The page initializes to send messages to the iframe domain
window.onload = function() {
    console.log('get key value...................... ')
    window.frames[0].postMessage(JSON.stringify({type:"GET".key:"User-Token"}),The '*');
}
// Listen for the message, receive the token information obtained from the iframe field, and store it in localstorage or 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);
Copy the code

Conclusion:

Advantages: Using the JWT mechanism is a good choice in a non-cross-domain environment. It is easy to implement, easy to operate, and fast to implement. Because the server does not store user status information, a large number of users will not cause pressure on the background service.

Disadvantages: Cross-domain implementation is relatively troublesome, security is also to be discussed. Since the JWT token is returned to the page, it can be obtained using JS. In case of XSS attack, the token may be stolen, and sensitive data information will be obtained before the JWT times out.

Note: Many people like the self-contained, tamper-proof nature of JWT, which dispense with the most annoying centralized tokens and makes it stateless. However, this is scenario-based. For example, how to deal with active Token revocation, how to dynamically control the validity period and how to dynamically switch the key. If there is no business requirement to actively revoke tokens, then the self-inclusion feature can be useful. It just depends on your business scenario.