Guide the attention

Follow me, a loser programmer who still has dreams, and share high quality programming blogs with you every day.

The following is a rough data flow diagram based on the application analysis in the actual project:

JWT SSO

There are three main steps:

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 code describes the JwtHelper utility class:

package org.vic.common.util; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.vic.common.constant.SecretConstant; import org.vic.common.web.ValidateLoginInterceptor; import com.alibaba.fastjson.JSONObject; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.extern.slf4j.Slf4j; /** * @author vic ** / @slf4j @suppressWarnings ("restriction") public class JwtHelper {/** * generate JWT string format: A.b.ca -header header information b-payload c-signature signature information * it is generated by encrypting the header and payload. * * @param userId userId * @param userName userName * @param IDENTITIES client information (variable length parameter), currently contains browser information, which is used to verify the client interceptor and prevent unauthorized cross-domain access * @return*/ public static String generateJWT(String userId, String userName, String... Identities) {// SignatureAlgorithm, select sha-256 SignatureAlgorithm SignatureAlgorithm = signaturealgorithm. HS256; Long nowTimeMillis = system.currentTimemillis (); Date now = new Date(nowTimeMillis); // Decode the BASE64SECRET constant string into a byte array using base64 byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET); SigningKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); Map<String, Object> headMap = new HashMap<String, Object>(); // 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": "vic"} // Encrypted customer id. Claim ("userId", AESSecretUtil encryptToStr (userId, SecretConstant DATAKEY)) / / customer name. The claim ("userName", userName) // Client browser information. Claim ("userAgent", identities[0]) // Signature .signWith(signatureAlgorithm, signingKey); // Add the Token expiration timeif (SecretConstant.EXPIRESSECOND >= 0) {
            long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        returnbuilder.compact(); } /** * Parsing JWT returns Claims object ** @param jsonWebToken * @return
     */
    public static Claims parseJWT(String jsonWebToken) {
        Claims claims = null;
        try {
            if(stringutils.isnotBlank (jsonWebToken)) {// Parse 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");
        }
        returnclaims; } /** * 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) {/ / decryption 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");
        }
        return retMap != null ? JSONObject.toJSONString(retMap) : null;
    }
}

Copy the code

AES encryption tool class:

package org.vic.common.util; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.vic.common.constant.SecretConstant; import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; /** * AES encryption tool ** @author vic ** / public class AESSecretUtil {** * private static final int KEYSIZE = 128; /** * AES encryption ** @param data Content to be encrypted * @param key Encryption key * @return
     */
    public static byte[] encrypt(String data, String key) {
        if (StringUtils.isNotBlank(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); 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);returnresult; } catch (Exception e) {e.printStackTrace(); }}returnnull; } /** * AES Encryption. Mandatory String ** @param data Content to be encrypted * @param key Encryption key * @return
     */
    public static String encryptToStr(String data, String key) {

        returnStringUtils.isNotBlank(data) ? parseByte2HexStr(encrypt(data, key)) : null; } /** * AES decrypt ** @param data to decrypt byte array * @param key secret key * @return
     */
    public static byte[] decrypt(byte[] data, String key) {
        if (ArrayUtils.isNotEmpty(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); 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 cipher.init(cipher. DECRYPT_MODE, secretKeySpec); Byte [] result = cipher.dofinal (data); byte[] result = cipher.dofinal (data);returnresult; } catch (Exception e) {e.printStackTrace(); }}returnnull; } /** * AES Encryption, Mandatory String ** @param enCryptdata Array to be decrypted * @param key Secret key * @return
     */
    public static String decryptToStr(String enCryptdata, String key) {
        returnStringUtils.isNotBlank(enCryptdata) ? new String(decrypt(parseHexStr2Byte(enCryptdata), key)) : null; } 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());
        }
        returnsb.toString(); } /** * convert hexadecimal to binary ** @param hexStr 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

Required constant classes:

package org.vic.common.constant; /** * JWT constant ** @author vic ** / public interface SecretConstant {// Public static final String BASE64SECRET ="ZW]4l5JH[m6Lm)LaQEjpb!4E0lRaG("; Public static final int EXPIRESSECOND = 1800000; Public static final String DATAKEY = public static final String DATAKEY ="u^3y6SPER41jm*fn";
}

Copy the code

Client interceptor:

package org.vic.common.web; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.vic.common.constant.GlobalConstant; import org.vic.common.util.JwtHelper; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; @author vic ** / @slf4j public class ValidateLoginInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws the Exception {/ / first obtain JWT String from the request header, and page agreement good for JWT value request header attribute named User - Token String JWT. = it getHeader ("User-Token");
        log.info("[Login verification interceptor]- JWT from header is :{}", jwt); // Determine whether JWT is validif(stringutils.isnotBlank (JWT)){// If JWT is valid, json information is returned; if no, empty String retJson = jwthelper.validatelogin (JWT); log.info("[Login verify interceptor]- Verify JWT validity result :{}", retJson); // If retJSON is empty, JWT times out or is invalidif(StringUtils.isNotBlank(retJson)){ JSONObject jsonObject = JSONObject.parseObject(retJson); / / check client information String userAgent. = it getHeader ("User-Agent");
                if (userAgent.equals(jsonObject.getString("userAgent"))) {/ / access to refresh after JWT values, set the response header httpServletResponse. SetHeader ("User-Token", jsonObject.getString("freshToken")); / / set the customer number to it in the session. The getSession (). The 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"); 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 configuration of the background service has been completed. Next, the front-end page needs to fetch the JWT token from the response header and store it in Localstorage or Cookie. 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, using postMessage of IFrame +H5. Specifically, I use code sharing to analyze.

/** start**/ (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 supportlocalStorage 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";
                      returntypeof 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);
                              }
                              returncookieData[k] })(); }, /* Delete locally stored 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 bindingbindEvent: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 Specifies the information stored locally. End **/Copy the code

Front-end page JS code (client) :

// The page initializes to send a message 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 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.

My website

My official website guan2ye.com

My blog.csdn.net/chenjianand CSDN address…

My address book Jane www.jianshu.com/u/9b5d1921c…

My githubgithub.com/javanan

My code cloud address is gitee.com/jamen/

Ali cloud coupons promotion.aliyun.com/ntms/yunpar…

** Personal wechat official account: Dou_zhe_wan ** Welcome to follow