SpringBoot+JWT+Redis实现单点登录功能

正文

大家可上我的git上下载源码,可以的话请给个星星,谢谢了。

代码结构图

SpringBoot+JWT+Redis实现单点登录功能_第1张图片

代码,从上往下

AESSecretUtil:

package com.jwt.demo;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;

/**
 * @author : HJH
 * @Description : AES加密工具类
 * @date  2019/8/2 15:27
 */
public class AESSecretUtil {

    /**
     * 秘钥的大小
     */
    private static final int KEYSIZE = 128;

    /**
     * @param data  待加密内容
     * @param key   加密秘钥
     * @deprecated  AES加密
     * @return 加密后的数组
     */
    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;
    }

    /**
     * @param data 待加密内容
     * @param key  加密秘钥
     * @deprecated AES加密
     * @return 返回String
     */
    public static String encryptToStr(String data, String key) {

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


    /**
     * @param data - 待解密字节数组
     * @param key  - 秘钥
     * @deprecated : AES解密
     */
    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;
    }

    /**
     * @param enCryptdata  待解密字节数组
     * @param key          秘钥
     * @deprecated : AES解密
     * @return 返回String
     */
    public static String decryptToStr(String enCryptdata, String key) {
        return StringUtils.isNotBlank(enCryptdata) ? new String(decrypt(parseHexStr2Byte(enCryptdata), key)) : null;
    }

    /**
     * @param buf - 二进制数组
     * @deprecated : 将二进制转换成16进制
     */
    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();
    }

    /**
     * @param hexStr - 16进制字符串
     * @deprecated : 将16进制转换为二进制
     */
    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));
    }

}

Base64Util

package com.jwt.demo;


/**
 * @author : HJH
 * @Description : Base64转码解码工具类
 * @date : 2019/8/2 15:27
 */
public class Base64Util {

    //字母表
    private static char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray();

    private static byte[] codes = new byte[256];

    static {
        for (int i = 0; i < 256; i++) {
            codes[i] = -1;
            // LoggerUtil.debug(i + "&" + codes[i] + " ");
        }
        for (int i = 'A'; i <= 'Z'; i++) {
            codes[i] = (byte) (i - 'A');
            // LoggerUtil.debug(i + "&" + codes[i] + " ");
        }

        for (int i = 'a'; i <= 'z'; i++) {
            codes[i] = (byte) (26 + i - 'a');
            // LoggerUtil.debug(i + "&" + codes[i] + " ");
        }
        for (int i = '0'; i <= '9'; i++) {
            codes[i] = (byte) (52 + i - '0');
            // LoggerUtil.debug(i + "&" + codes[i] + " ");
        }
        codes['+'] = 62;
        codes['/'] = 63;
    }

    /**
     * @param data
     * @deprecated : 功能:编码字符串
     */
    public static String encode(String data) {
        return new String(encode(data.getBytes()));
    }

    /**
     * @param data
     * @deprecated : 功能:解码字符串
     */
    public static String decode(String data) {
        return new String(decode(data.toCharArray()));
    }

    /**
     * @param data - 字节数组
     * @deprecated : 功能:编码byte[]
     */
    public static char[] encode(byte[] data) {
        char[] out = new char[((data.length + 2) / 3) * 4];
        for (int i = 0, index = 0; i < data.length; i += 3, index += 4) {
            boolean quad = false;
            boolean trip = false;

            int val = (0xFF & (int) data[i]);
            val <<= 8;
            if ((i + 1) < data.length) {
                val |= (0xFF & (int) data[i + 1]);
                trip = true;
            }
            val <<= 8;
            if ((i + 2) < data.length) {
                val |= (0xFF & (int) data[i + 2]);
                quad = true;
            }
            out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 1] = alphabet[val & 0x3F];
            val >>= 6;
            out[index + 0] = alphabet[val & 0x3F];
        }
        return out;
    }

    /**
     * @param data - 字节数组
     * @deprecated : 功能:解码字节数组
     */
    public static byte[] decode(char[] data) {

        int tempLen = data.length;
        for (int ix = 0; ix < data.length; ix++) {
            if ((data[ix] > 255) || codes[data[ix]] < 0) {
                --tempLen; // ignore non-valid chars and padding
            }
        }
        // calculate required length:
        // -- 3 bytes for every 4 valid base64 chars
        // -- plus 2 bytes if there are 3 extra base64 chars,
        // or plus 1 byte if there are 2 extra.

        int len = (tempLen / 4) * 3;
        if ((tempLen % 4) == 3) {
            len += 2;
        }
        if ((tempLen % 4) == 2) {
            len += 1;

        }
        byte[] out = new byte[len];

        int shift = 0; // # of excess bits stored in accum
        int accum = 0; // excess bits
        int index = 0;

        // we now go through the entire array (NOT using the 'tempLen' value)
        for (int ix = 0; ix < data.length; ix++) {
            int value = (data[ix] > 255) ? -1 : codes[data[ix]];

            if (value >= 0) { // skip over non-code
                accum <<= 6; // bits shift up by 6 each time thru
                shift += 6; // loop, with new bits being put in
                accum |= value; // at the bottom.
                if (shift >= 8) { // whenever there are 8 or more shifted in,
                    shift -= 8; // write them out (from the top, leaving any
                    out[index++] = // excess at the bottom for next iteration.
                            (byte) ((accum >> shift) & 0xff);
                }
            }
        }

        // if there is STILL something wrong we just have to throw up now!
        if (index != out.length) {
            throw new Error("Miscalculated data length (wrote " + index
                    + " instead of " + out.length + ")");
        }

        return out;
    }

}

JwtHelper

package com.jwt.demo;

import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author  HJH
 * @Description : JWT工具类
 * 
参考官网:https://jwt.io/ *
JWT的数据结构为:A.B.C三部分数据,由字符点"."分割成三部分数据 *
A-header头信息 *
B-payload 有效负荷 一般包括:已注册信息(registered claims),公开数据(public claims),私有数据(private claims) *
C-signature 签名信息 是将header和payload进行加密生成的 * @date 2019/8/2 15:27 */ public class JwtHelper { private static Logger logger = LoggerFactory.getLogger(JwtHelper.class); /** * @param userId - 用户编号 * @param userName - 用户名 * @param identities - 客户端信息(变长参数),目前包含浏览器信息,用于客户端拦截器校验,防止跨域非法访问 * @Author: Helon * @deprecated 生成JWT字符串 *
格式:A.B.C *
A-header头信息 *
B-payload 有效负荷 *
C-signature 签名信息 是将header和payload进行加密生成的 * @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 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(); } /** * @param jsonWebToken - 页面传过来的token * @deprecated: 解析JWT * @return Claims对象 */ 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; } /** * @param jsonWebToken - 页面传过来的token * @deprecated: 校验JWT是否有效 * @return json字符串的demo:
* {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"} *
freshToken-刷新后的jwt *
userName-客户名称 *
userId-客户编号 *
userAgent-客户端浏览器信息 */ public static String validateLogin(String jsonWebToken) { Map 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)); } }

SecretConstant

package com.jwt.demo;

/**
 * @author  HJH
 * @Description  : JWT使用常量值
 * @date  2019/8/2 15:27
 */
public class SecretConstant {

    //签名秘钥
    public static final String BASE64SECRET = "ZW]4l5JH[m6Lm)LaQEjpb!4E0lRaG(";

    //超时毫秒数(默认30分钟)
    public static final int EXPIRESSECOND = 1800000;

    //用于JWT加密的密匙
    public static final String DATAKEY = "u^3y6SPER41jm*fn";

}

ValidateLoginInterceptor

package com.jwt.demo;



import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author HJH
 * @Description : 校验是否登录拦截器
 * @date 2019/8/2 14:45
 */
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor {

    private String SESSION_CUSTOMER_NO_KEY = "session_customer_no_key";

    @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(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 {

    }
}


pom.xml



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.6.RELEASE
         
    
    com.jwt
    demo
    0.0.1-SNAPSHOT
    demo
    Demo project for Spring Boot

    
        1.8
        3.3.0
        0.9.0
        1.2.47
        3.7
        1.6.4
        4.12
        1.11
        1.16.16
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
        
            com.auth0
            java-jwt
            ${java-jwt}
        
        
            io.jsonwebtoken
            jjwt
            ${jsonwebtoken.jjwt}
        
        
            com.alibaba
            fastjson
            ${alibaba.fastjson}
        
        
            org.apache.commons
            commons-lang3
            ${apache.commons-lang3}
        

        
        
            org.slf4j
            slf4j-log4j12
            ${slf4j.version}
        
        
            org.slf4j
            slf4j-api
        
        
        
            junit
            junit
            ${junit.version}
            test
        
        
        
            commons-codec
            commons-codec
            ${apache.commons-codec}
        
        
        
            org.projectlombok
            lombok
            ${lombok.version}
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    



注意

因为使用了@Slf4j注解,需要安装Lombok
SpringBoot+JWT+Redis实现单点登录功能_第2张图片

结语

代码中并没有存token,请根据实际情况处理token。

最后,祝大家开开心心每一天

你可能感兴趣的:(JAVA)