JWT详解

【参考文章JWT详解
【参考Java使用JWT进行登录校验

JWT是什么

JWT全称:Json Web Token

在介绍JWT之前,我们先来回顾一下利用token进行用户身份验证的流程:

  1. 客户端使用用户名和密码请求登录
  2. 服务端收到请求,验证用户名和密码
  3. 验证成功后,服务端会签发一个token,再把这个token返回给客户端
  4. 客户端收到token后可以把它存储起来,比如放到cookie中
  5. 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
  6. 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据

而JWT就是上述流程当中token的一种具体实现方式,其全称是JSON Web Token
官网地址:https://jwt.io/

image

JWT组成结构

JWT本质上就是一个字符串,由三部分组成,通过.将各部分连接成一个字符串;
类似 aaaa.bbbb.cccc,其中 aaaa表示头部(Header),bbbb表示载荷(Payload),cccc为签名(Signature)
image

JWT头是一个描述JWT元数据的JSON对象

  • alg属性表示【签名使用的算法】,默认为HMAC SHA256(写为HS256);
  • typ属性表示令牌的类型,JWT令牌统一写为JWT。
    最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据(不含敏感数据如电话、userId、密码等)。 JWT指定了七个【默认】字段供选择

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

例如:

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

注意,默认情况下JWT是未加密的,因为只是采用base64算法进行编码,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容;
因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些【非敏感】的信息

Signature

签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希(该过程不可逆),以确保数据不会被篡改。

  1. 首先,需要指定一个密钥(secret)。该密码仅仅为【保存在服务器】中,并且不能向用户公开。
  2. 然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

在计算出签名哈希后,JWT头,有效载荷、签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象

JWT验证

  1. 客户端需要请求服务器后端接口时,发送JWT给服务器
  2. 服务器拿到JWT后,解析JWT字符串,拿出Payload
  3. 然后根据自己内部保存的密钥secret,对Payload进行求哈希
  4. 最后判断生成的签名与客户端发来的JWT中的签名是否一致,是则说明传输过程中Payload没用被篡改,否则说明数据被篡改。

Java中使用JWT

官网推荐了6个Java使用JWT的开源库,其中比较推荐使用的是java-jwtjjwt-root

java-jwt

引入依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

生成JWT

public class JWTTest {
    @Test
    public void testGenerateToken(){
        // 指定token过期时间为10秒
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 10);

        String token = JWT.create()
                .withHeader(new HashMap<>())  // Header
                .withClaim("userId", 20)  // Payload
                .withClaim("userName", "ajream")
                .withExpiresAt(calendar.getTime())  // 过期时间
                .sign(Algorithm.HMAC256("fjareuw"));  // 签名用的密钥secret

        System.out.println(token);
    }
}

解析JWT


public void testResolveToken(){
    // 创建解析对象,使用的算法和secret要与创建token时保持一致
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("fjareuw")).build();
    // 解析指定的token
    DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImJhb2JhbyIsImV4cCI6MTU5OTkyMjUyOCwidXNlcklkIjoyMX0.YhA3kh9KZOAb7om1C7o3vBhYp0f61mhQWWOoCrrhqvo");
    // 获取解析后的token中的payload信息
    Claim userId = decodedJWT.getClaim("userId");
    Claim userName = decodedJWT.getClaim("userName");
    System.out.println(userId.asInt());
    System.out.println(userName.asString());
    // 输出超时时间
    System.out.println(decodedJWT.getExpiresAt());
}

实际开发中使用JWT

在实际的SpringBoot项目中,一般我们可以用如下流程做登录:

  1. 生成payload:在登录验证通过后,拿到前端用户名和密码,然后生成一个随机的字符串作为【密钥key】的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间
  2. 生成JWT:根据第一步生成的payload生成JWT并返回给前端用户,前端把JWT保存下来。
  3. 前端之后每次请求都在请求头中的【Authorization字段】中携带JWT字符串
  4. 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload,获取用户信息,如果能获取到就说明用户已经登录

JWT登录拦截器:

public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String JWT = request.getHeader("Authorization");
        try {
            // 1.校验JWT字符串
            DecodedJWT decodedJWT = JWTUtils.decode(JWT);
            // 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}

posted @ 2022-05-15 22:33  aJream  阅读(381)  评论(0)    收藏  举报