JWT学习

268 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

JWT学习

JWT官网: jwt.io/ JWT(Java版)的github地址:github.com/jwtk/jjwt


1. 概述

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).**定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。**因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。


2. JWT工作流程

1646114931416

  1. 用户使用账号和面发出post请求;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器。

3. 优点

  • **1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 **
  • **2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库 **
  • 3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
  • 4.不需要在服务端保存会话信息,特别适用于分布式微服务

4. 为什么Token可以防止伪造

问题

csrf中伪造的请求可以在不得到cookie的情况下在请求头中带上cookie,为什么token不能在未知的情况下在伪造请求中发送,发送请求时不是都是浏览器提供吗?

解答

这么来说吧,Cookies是浏览器自动添加的。这是CSRF攻击的原理。

但是token是无法由浏览器添加的,无法被跨站得到的,这是CSRF的前提,CSRF中的CS就是Cross Site,跨站。 token存在的意义就是,获取token时,将token放在跨站无法得到的数据之中,而验证token时,token必须出现在指定位置。

所以token在验证时就肯定不存在cookies之中,也就不会被浏览器自动带上


5. JWT的三部分

三部分说明
Header 头部头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载负载 (类似于飞机上承载的物品)
Signature 签证签名/签证

5.1 结构 A . B . C

JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。 就像这样: A . B . C

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

5.2 第一部分——头部header

完整的头部就像下面这样的JSON:

{

​ 'typ': 'JWT', ​ 'alg': 'HS256'

}

jwt的头部承载两部分信息:

声明类型,这里是jwt 声明加密的算法 通常直接使用 HMAC SHA256

A部分

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分(也就是A部分)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9


5.3 第二部分——负载payload

负载包含三部分

标准中注册的声明 公共的声明 私有的声明

标准声明

iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

B部分

定义一个payload:

{ ​ "sub": "1234567890", ​ "name": "John Doe", ​ "admin": true }

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9


5.4 第三部分——签证signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的) payload (base64后的) secret (秘钥) 秘钥仅服务器知道

C部分

这个部分需要base64加密后的headerbase64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); 
// TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

得到的signature就是C部分

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

解析JWT

1646114943767


6. JWT的Java使用

可参考文章

blog.csdn.net/qq_37636695…

6.1 依赖包JJWT

<!--        jwt依赖包-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

秘钥

// 秘钥
private static final String jwtToken = "Ysl Love";

6.2 创建Token

/***
 * 生成Token 利用userId生成唯一的Token
 * @param userId
 * @return
 */
public static String createToken(Integer userId){

    // B部分
    Map<String,Object> claims = new HashMap<>();
    claims.put("userId",userId);
    // claims.put("password","1203456");

    JwtBuilder jwtBuilder = Jwts.builder()
            .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
            .setClaims(claims)                            // body数据,要唯一,自行设置
 //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
            .setIssuedAt(new Date())                      // 设置签发时间
            .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));     // 设置一天的有效时间

    String token = jwtBuilder.compact();                  //压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
    return token;

}

6.3 解析Token

    /**
     * 解析token
     * @param token
     * @return
     */
    public static Map<String, Object> checkToken(String token){
        try {
//            jwtToken是签名秘钥 和生成的签名的秘钥一模一样
            Jwt parse = Jwts.parser()       //得到DefaultJwtParser
                    .setSigningKey(jwtToken)//设置签名的秘钥
                    .parse(token);

            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

main测试

public static void main(String[] args) {
    String token = createToken(123);
    System.out.println(token);
    Map<String, Object> stringObjectMap = checkToken(token);
    System.out.println(stringObjectMap.toString());

    Integer userId = (Integer)stringObjectMap.get("userId");
    System.out.println(userId);
}

2022.3.29遇到疑惑——关于JWT合法性检测问题

6.4 检测JWT合法性

接受到JWT后,会⾸先对头部和载荷的内容⽤同⼀算法再次签名。

那么服务器应⽤是怎么知道我们⽤的是哪⼀种算法呢?别忘了,我们在JWT的头部中已经⽤alg字段指明了我们的加密算法了。

如果服务器应⽤对头部和载荷再次以同样方法签名之后发现,⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。

测试代码

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param SECRET 秘钥
     * @return 是否正确
     */
    public static boolean verify(String token) {
        /**
        * 	根据头部的算法再次对头部和载荷进行同样的签名
        */
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            log.info("jwt的verify校验通过");
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

文章👇

jwt进行token校验 -详细版

JWTUtil

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import edu.zhku.zkdcms.shiro.cache.PropertiesUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @date: 2022/3/30
 * @FileName: JWTUtil
 * @author: Yan
 * @Des:
 */

@Slf4j
public class JWTUtil {


    // 过期时间30分钟
    public static final long EXPIRE_TIME =  24*60*60;

    private final static String SECRET = "***#&*dcms^@(";

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param SECRET 秘钥
     * @return 是否正确
     */
    public static boolean verify(String token, String username,String roleId,String currentTimeMillis) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .withClaim("roleId",roleId.toString())
                    .withClaim("currentTimeMillis",currentTimeMillis)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            log.info("jwt的verify校验通过");
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param SECRET 秘钥
     * @return 是否正确
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            log.info("jwt的verify校验通过");
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getClaim(String token,String claim) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            log.info("在jwt解析后,拿到的数据{}",jwt.getClaim(claim).asString());
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @param currentTimeMillis 当前时间戳
     * @param SECRET 秘钥
     * @return 加密的token
     */
    public static String sign(String username,String roleId,String currentTimeMillis) {

        Date date = new Date(System.currentTimeMillis()+ EXPIRE_TIME*1000);
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withClaim("roleId",roleId.toString())
                .withClaim("currentTimeMillis",currentTimeMillis)
                .withExpiresAt(date)
                .sign(algorithm);
    }
}

使用拦截器校验Token

/**
 * 添加拦截器,拦截请求,校验token
 */
public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HashMap<String, Object> map = new HashMap<>();
        String token = request.getHeader("token");  //从request中获取到请求头中的token,进行解析校验
        try {
            jwtUtil.verifyToken(token);//调用token解析的工具类进行解析
            return true;  //请求放行
        } catch (SignatureVerificationException e) {
            e.printStackTrace();
            map.put("msg", "签名不一致异常");
        } catch (TokenExpiredException e) {
            e.printStackTrace();
            map.put("msg", "令牌过期异常");
        } catch (AlgorithmMismatchException e) {
            e.printStackTrace();
            map.put("msg", "算法不匹配异常");
        } catch (InvalidClaimException e) {
            e.printStackTrace();
            map.put("msg", "失效的payload异常");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("msg", "token无效");
        }
        //map异常的数据要返回给客户端需要转换成json格式  @ResponseBody 内置了jackson
        String resultJson = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print(resultJson);
        return false;  //异常不放行
    }
}