springCloudAlibaba整合JWT

一、jwt简介
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于 Json 的开放标准。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。
JWT 由三部分构成,第一部分称为头部(Header),第二部分称为消息体(Payload),第三部分是签名(Signature)。
JWT 生成的 Token 格式为:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)

头部的信息通常由两部分内容组成,令牌的类型和使用的签名算法,比如下面的代码:
{"alg": "HS256", "typ": "JWT"}
消息体中可以携带一些你需要的信息,比如用户 ID。因为你得知道这个 Token 是哪个用户的,比如下面的代码: { "userId": "1", "admin": true,"userAgent":"PostmanRuntime/7.1.1"}
签名是用来判断消息在传递的路上是否被篡改,从而保证数据的安全性,格式如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
通过这三部分就组成了我们的 Json Web Token。
更多介绍可以查看 JWT 官网 https://jwt.io/introduction/

二、使用场景
在前后分离场景下,越来越多的项目使用token作为接口的安全机制,APP端或者WEB端(使用VUE、REACTJS等构建)使用token与后端接口交互,以达到安全的目的。

三、思路整理


登录流程图

四、springCloudAlibaba引入JWT的依赖

   <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency> 

五、代码

工具类:

@Slf4j
public class JwtUtils {
    /**
     * JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认60分钟
     */
    public static final long JWT_WEB_TTL = 60* 60 * 1000;
    /**
     * 将jwt令牌保存到header中的key
     */
    public static final String JWT_HEADER_KEY = "auth_token";

    // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
    private static final String JWT_SECRET = "xxxxx";// JWT密匙 c
    private static  final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key

    static {
        byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
        JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    } 
  
    /**
     * 检查Token是否合法
     * @param token
     * @return JWTResult
     */
    public static JWTResult checkToken(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(JWT_KEY).parseClaimsJws(token).getBody();
            String sub = claims.get("sub", String.class);
            return new JWTResult(true, sub, "合法请求", ResponseCode.SUCCESS_CODE.getCode());
        } catch (ExpiredJwtException e) {
            // 在解析JWT字符串时,如果‘过期时间字段’已经早于当前时间,将会抛出ExpiredJwtException异常,说明本次请求已经失效
            return new JWTResult(false, null, "token已过期", ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
        }catch (Exception e) {
            return new JWTResult(false, null, "非法请求", ResponseCode.NO_AUTH_CODE.getCode());
        }
    }

    /**
     * 刷新令牌
     * @param token
     * @return
     */
    public static ResponseData refreshToken(String token){
        log.info("刷新令牌token:{}",token);
        JSONObject resultMap = new JSONObject();
        try {
            byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
            SecretKeySpec JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
            Claims claims = Jwts.parser().setSigningKey(JWT_KEY).parseClaimsJws(token).getBody();
            //Claims claims = Jwts.parserBuilder().setSigningKey(JWT_KEY).build().parseClaimsJws(token).getBody();
            Date expiration = claims.getExpiration(); //过期时间
            Date issuedAt = claims.getIssuedAt(); //token签发时间
            resultMap.put("userAgent",claims.get("userAgent"));
            Object userId = claims.get("userId");
            long nowTime = System.currentTimeMillis();
            //如果当前的token已经过期则需要重新登录
            if(nowTime > expiration.getTime()){
                log.info("用户userId:{}令牌token:{}已经过期,需要重新登录",userId,token);
                return  ResponseData.fail("token过期",ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
            }
            long startTime = issuedAt.getTime();
            long pastTime =  nowTime - startTime;
            if(pastTime > 30*60*1000){
                log.info("用户userId:{}令牌token:{}已经存在超过30分钟,每30分钟刷新一次令牌",userId,token);
                //获取新的token ,将原先的token进行失效
                String newToken = createToken(claims,  JwtUtils.JWT_WEB_TTL);
                resultMap.put("token",newToken);
                log.info("用户userId:{}令牌token:{}已经存在超过30分钟,每30分钟刷新一次令牌,新令牌:{}",userId,token,newToken);
                return ResponseData.ok(resultMap);
            }
            String sub = claims.get("sub", String.class);
            resultMap.put("token",token);
            log.info("用户userId:{}令牌token:{}",userId,token);
            return ResponseData.ok(resultMap);
        } catch (ExpiredJwtException e) {
            log.info("令牌token:{},过期了需要重新登录",token);
            return  ResponseData.fail("token过期",ResponseCode.TOKEN_TIMEOUT_CODE.getCode());
        } catch (Exception e) {
           log.error("刷新令牌出现异常:{}",e);
        }
        return null;
    }


    /**
     * 创建JWT令牌,签发时间为当前时间
     *
     * @param claims
     *            创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
     * @param ttlMillis
     *            JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间
     * @return jwt令牌
     */
    public static String createToken(Map<String, Object> claims, long ttlMillis) {
        byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
        SecretKeySpec JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        // 生成JWT的时间,即签发时间
        long nowMillis = System.currentTimeMillis();
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(UUID.randomUUID().toString().replace("-", ""))// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(new Date(nowMillis))//jwt的签发时间
                .signWith(SIGNATURE_ALGORITHM,JWT_KEY)//设置签名使用的签名算法和签名使用的秘钥
                .setExpiration(new Date(nowMillis + ttlMillis));//设置JWT的过期时间
        return builder.compact();
    }

}

public class RSAUtils {
    /**
     * * 生成公钥和私钥
     * * @throws NoSuchAlgorithmException  *
     */
    public static HashMap<String, Object> getKeys()
            throws NoSuchAlgorithmException { 
        HashMap<String, Object> map = new HashMap<String, Object>();
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        map.put("public", publicKey);
        map.put("private", privateKey);
        return map;
    }

    /**
     * * 使用模和指数生成RSA公钥
     * * @param modulus  模
     * * @param exponent  指数   *
     *
     * @return
     */
    public static RSAPublicKey getPublicKey(String modulus, String exponent) {
        try {
            BigInteger b1 = new BigInteger(modulus);
            BigInteger b2 = new BigInteger(exponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2);
            return (RSAPublicKey) keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * * 使用模和指数生成RSA私钥
     * * /None/NoPadding
     * * @param modulus
     * 模   * @param
     * exponent指数   * @return
     */
    public static RSAPrivateKey getPrivateKey(String modulus, String exponent) {
        try {
            BigInteger b1 = new BigInteger(modulus);
            BigInteger b2 = new BigInteger(exponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2);
            return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * * 公钥加密   *
     * * @param data
     * * @param publicKey
     * * @return
     * * @throws
     * Exception
     */
    public static String encryptByPublicKey(String data, RSAPublicKey publicKey)
            throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        // 模长
        int key_len = publicKey.getModulus().bitLength() / 8;
        // 加密数据长度 <= 模长-11
        String[] datas = splitString(data, key_len - 11);
        String mi = "";
        // 如果明文长度大于模长-11则要分组加密
        for (String s : datas) {
            mi += bcd2Str(cipher.doFinal(s.getBytes()));
        }
        return mi;
    }

    /**
     * * 私钥解密   *
     * * @param data
     * * @param privateKey
     * * @return
     * * @throws
     * Exception
     */
    public static String decryptByPrivateKey(String data,
                                             RSAPrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        // 模长
        int key_len = privateKey.getModulus().bitLength() / 8;
        byte[] bytes = data.getBytes();
        byte[] bcd = ASCII_To_BCD(bytes, bytes.length);
        // System.err.println(bcd.length);
        // 如果密文长度大于模长则要分组解密
        String ming = "";
        byte[][] arrays = splitArray(bcd, key_len);
        for (byte[] arr : arrays) {
            ming += new String(cipher.doFinal(arr));
        }
        return ming;
    }

    /**
     * * ASCII码转BCD码   *
     */
    public static byte[] ASCII_To_BCD(byte[] ascii, int asc_len) {
        byte[] bcd = new byte[asc_len / 2];
        int j = 0;
        for (int i = 0; i < (asc_len + 1) / 2; i++) {
            bcd[i] = asc_to_bcd(ascii[j++]);
            bcd[i] = (byte) (((j >= asc_len) ? 0x00 : asc_to_bcd(ascii[j++])) + (bcd[i] << 4));
        }
        return bcd;
    }

    public static byte asc_to_bcd(byte asc) {
        byte bcd;

        if ((asc >= '0') && (asc <= '9'))
            bcd = (byte) (asc - '0');
        else if ((asc >= 'A') && (asc <= 'F'))
            bcd = (byte) (asc - 'A' + 10);
        else if ((asc >= 'a') && (asc <= 'f'))
            bcd = (byte) (asc - 'a' + 10);
        else
            bcd = (byte) (asc - 48);
        return bcd;
    }

    /**
     * * BCD转字符串
     */
    public static String bcd2Str(byte[] bytes) {
        char temp[] = new char[bytes.length * 2], val;

        for (int i = 0; i < bytes.length; i++) {
            val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f);
            temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0');

            val = (char) (bytes[i] & 0x0f);
            temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0');
        }
        return new String(temp);
    }

    /**
     * * 拆分字符串
     */
    public static String[] splitString(String string, int len) {
        int x = string.length() / len;
        int y = string.length() % len;
        int z = 0;
        if (y != 0) {
            z = 1;
        }
        String[] strings = new String[x + z];
        String str = "";
        for (int i = 0; i < x + z; i++) {
            if (i == x + z - 1 && y != 0) {
                str = string.substring(i * len, i * len + y);
            } else {
                str = string.substring(i * len, i * len + len);
            }
            strings[i] = str;
        }
        return strings;
    }

    /**
     * *拆分数组
     */
    public static byte[][] splitArray(byte[] data, int len) {
        int x = data.length / len;
        int y = data.length % len;
        int z = 0;
        if (y != 0) {
            z = 1;
        }
        byte[][] arrays = new byte[x + z][];
        byte[] arr;
        for (int i = 0; i < x + z; i++) {
            arr = new byte[len];
            if (i == x + z - 1 && y != 0) {
                System.arraycopy(data, i * len, arr, 0, y);
            } else {
                System.arraycopy(data, i * len, arr, 0, len);
            }
            arrays[i] = arr;
        }
        return arrays;
    }
}

拦截器:

@Configuration
public class LoginConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册TestInterceptor拦截器
        InterceptorRegistration registration = registry.addInterceptor(new  ValidateLoginInterceptor());
        registration.addPathPatterns("/**");                      //所有路径都被拦截
        registration.excludePathPatterns(                         //添加不拦截路径
                "/user/login",           //登录 
        );
    }
}

@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        //首先从请求头中获取jwt串,与页面约定好存放jwt值的请求头属性名为author_token
        String jwt = httpServletRequest.getHeader(JwtUtils.JWT_HEADER_KEY);
        log.info("[登录拦截器]-从header中获取的jwt为:{}", jwt);
        //判断jwt是否有效
        if(StringUtils.isNotBlank(jwt)){
            //校验jwt是否有效,有效则返回json信息,无效则返回空
          //  JWTResult jwtResult = JwtUtils.checkToken(jwt);
            ResponseData responseData = JwtUtils.refreshToken(jwt);
            log.info("[登录拦截器]-校验JWT有效性返回结果:{}", responseData);
            //retJSON为空则说明jwt超时或非法
            if(responseData != null && responseData.getCode() == 200 ){
                Object data = responseData.getData();
                String s = JSONObject.toJSONString(data);
                JSONObject jsonObject = JSONObject.parseObject(s);
                log.info("[登录拦截器]-校验JWT有效性,解析返回data:{}", jsonObject);
                //校验客户端信息
                String userAgent = httpServletRequest.getHeader("User-Agent");
                if (userAgent.equals(jsonObject.getString("userAgent"))) {
                    //获取刷新后的jwt值,设置到响应头中
                    httpServletResponse.setHeader(JwtUtils.JWT_HEADER_KEY, jsonObject.getString("token"));
                    //将客户编号设置到session中
                    httpServletRequest.getSession().setAttribute("user_id", 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 {

    }

}

登录接口

 @RequestMapping("/login")
    public Result login(String userName ,String password,HttpServletResponse response,HttpServletRequest request){
        //todo--根据userName,parssword进行登录验证
        Integer userId = 1381 ;
        Map<String,Object> claims=new HashMap<String, Object>();
        claims.put("userId",userId);
        String userAgent = request.getHeader("User-Agent");
        claims.put("userAgent",userAgent);
      //  JwtUtils jwtUtils = new JwtUtils();
        String jwt = JwtUtils.createToken(claims, JwtUtils.JWT_WEB_TTL);
        response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
        return Result.success(jwt);
    }

总结:
此案例通过jwt获取token,通过拦截器校验,生成token的userAgent和当前访问的userAgent是否一致,以及token是否合法,实现登录逻辑

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容