所有分类
  • 所有分类
  • 未分类

JWT–使用与实例

简介

说明

本文介绍JWT知识。包括:什么是JWT、JWT的消息组成、JWT实战(测试java-jwt和jjwt两个工具类)。

JWT是常用的TOKEN工具,本文介绍JWT知识。包括:什么是JWT、JWT的消息组成、JWT实战(java-jwt和jjwt两种客户端都测试一下)。

官网

JSON Web Tokens – jwt.io      (可生成/解析 Token)

JWT简介

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。JWT将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证;应用场景如用户登录。

JWT可通过URL、POST参数或HTTP header发送,数据量小;负载中可包含用户信息,避免多次查询数据库。 

JWT定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。     

JWT请求流程

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

Cookie+Session与JWT对比

Cookie+Session

用户登录认证中,因为http是无状态的,所以采用session方式。用户登录成功,服务端会保存一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。

Cookie+session这种模式通常是保存在服务器内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。

JWT

服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。简单便捷,无需通过Redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验。

JWT优点

  1. 占资源少
    1. 不在服务端保存信息。
  2. 扩展性
    1. 分布式中,Session需要做多机数据共享,通常存在数据库或者Redis里面。而JWT不需要。
  3. 跨语言
    1. Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;

JWT缺点

  1. 无法废弃已颁布的令牌
    1. JWT在到期之前会始终有效,无法中途废弃。例如:在payload中存储了一些信息,当信息需要更新时,重新签发一个JWT,但旧的jwt还没过期,旧的JWT仍然可以登录,登录后服务端从JWT中拿到的信息就是过时的。
      1. 解决方案:服务端部署额外的逻辑,例如:设置黑名单,一旦签发了新的JWT,旧的就加入黑名单(比如存到Redis里面),避免被再次使用。(违背JWT初衷)
  2. 过期
    1. Cookie续签方案一般都是框架自带的,如:Session有效期30分钟,若30分钟内有访问,有效期刷新至30分钟。对于JWT,改变JWT的有效时间,就要签发新的JWT。
      1. 解决方案1:每次请求刷新JWT。即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。
      2. 解决方案2:每次请求检查Token过期时间,如果即将过期,则重新生成一个新Token返回给客户端。

JWT消息构成

一个token分3部分,按顺序为:头部(header)、载荷(payload)、签证(signature)。三部分之间用.号做分隔。例如(编码后的格式):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

上边解码后结果如下图:

header

简介

{
  "alg": "HS256",
  "typ": "JWT"
}

包含两个信息:token类型和采用的加密算法。

token类型:本处是jwt 
加密算法:通常直接使用 HMAC SHA256

加密算法

加密算法是单向函数散列算法,常见的有:MD5、SHA、HAMC。

  • MD5(message-digest algorithm 5)信息-摘要 算法
    • 广泛用于加解密,常用于文件校验。(每个文件都能生成唯一的MD5值)
  • SHA (Secure Hash Algorithm)安全散列算法
    • 数字签名等密码学应用中重要的工具,安全性高于MD5
  • HMAC (Hash Message Authentication Code)散列消息鉴别码
    • 基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

JWT验证和签名使用的算法列表如下:

JWS算法名称
HS256HMAC256
HS384HMAC384
HS512HMAC512
RS256RSA256
RS384RSA384
RS512RSA512
ES256ECDSA256
ES384ECDSA384
ES512ECDSA512

payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

载荷:存放有效信息,存放的是claim(声明)。有两种声明:1.标准的声明 2.自定义的声明

不应该在JWT的payload存储敏感信息(比如密码),因为该部分是通过base64编码的,客户端可解码。payload的内容对客户端来说是可见的,但客户端无法修改 payload 中的信息或对其进行加密操作。在 JWT 的设计中,payload 部分是公开的,主要用于传递数据,而真正的安全性是 Signature 部分的校验,用来确保 token 的完整性和真实性。

标准的声明 

含义
issIssuer。 jwt签发者
subSubject。用户。一般是用户名
audAudience。接收jwt的一方。一般写用户id
expExpiration Time。过期时间戳(必须要大于签发时间)
nbfNot Before。生效时间戳。该时间之前,该jwt都是不可用的。
iat Issued At。签发时间戳。
jti JWT ID。JWT的唯一标识。 主要用来作为一次性token,从而回避重放攻击。

自定义的声明

这里可以写任何非敏感内容,比如:用户id、用户名、邮箱。

signature

简介

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  
your-256-bit-secret
)

Signature 部分是对前两部分的签名,防止数据篡改。

1.指定一个密钥(secret)。这个secret保存在服务端,服务端根据这个密钥生成token和进行验证,要保护好,不能泄露给用户(客户端)。

2.使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名(Signature)。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
your-256-bit-secret
)

3.把 Header(base64加密后的)、Payload(base64加密后的)、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,返回给用户。

实战

java-jwt的github:https://github.com/auth0/java-jwt
jjwt的github:https://github.com/jwtk/jjwt

创建项目

引入依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.5</version>
</dependency>

说明:这两个引入一个即可。本处为了测试这两个库的用法,所以都引入了。 

token实现库

token在java中共有6个实现库,见:JSON Web Tokens – jwt.io

下边介绍常用的两个:

  1. java-jwt(推荐)
  2. jjwt(不太推荐)
    1. 不太推荐的原因:版本都是0.xx,还没发布1.xx版本。

工具类

本处会测试两个库:java-jwt和jjwt,所以写一个接口,两个实现类。

接口

package com.example.demo.util;

public interface JwtUtil {
    String createToken(String userId);

    boolean verifyToken(String token);

    String getUserIdByToken(String token);
}

java-jwt实现类

package com.example.demo.util.impl;
 
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.demo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.util.Date;
 
@Slf4j
public class JwtUtilJavaJwtImpl implements JwtUtil {
    // 过期时间
    private final Duration TTL = Duration.ofSeconds(5);
 
    // 密钥(java-jwt对长度没有限制)
    private final String SECRET = "123456";
 
    // 创建Token
    @Override
    public String createToken(String userId) {
        try {
            Date date = new Date(System.currentTimeMillis() + TTL.toMillis());
            Algorithm algorithm = calculateKey();
            return JWT.create()
                    .withAudience(userId)  // 将 user id 保存到 token 里面
                    .withExpiresAt(date)   // date之后,token过期
                    // 自定义私有的payload的key-value
                    // .withClaim("userName", "Tony")
                    .sign(algorithm);      // token 的密钥
        } catch (Exception e) {
            log.error("创建token失败", e);
            return null;
        }
    }
 
    // 校验token
    @Override
    public boolean verifyToken(String token) {
        // 这里可以加一个非空校验
 
        try {
            verifyAndParse(token);
            return true;
        } catch (Exception e) {
            log.error("解析token失败", e);
            return false;
        }
    }
 
    // 根据token获取userId
    @Override
    public String getUserIdByToken(String token) {
        // 这里可以加一个非空校验
        try {
            DecodedJWT decodedJWT = verifyAndParse(token);
            // 不要这样写,如果这么写,token失效时不报错!
            // DecodedJWT decodedJWT = JWT.decode(token);

            // 可获得所有标准Claim
            // String subject = decodedJWT.getSubject();
 
            // 可获得自定义Claim数据
            // Map<String, Claim> claims = decodedJWT.getClaims();
            // Map<String, String> claimMap = new HashMap<>();
            // for (Map.Entry<String, Claim> entry : claims.entrySet()) {
            //     claimMap.put(entry.getKey(), entry.getValue().asString());
            // }
 
            // 可获得过期日期
            // Date expiresAt = decodedJWT.getExpiresAt();
 
            return decodedJWT.getAudience().get(0);
        } catch (Exception e) {
            log.error("解析token失败", e);
            return null;
        }
    }
 
    private Algorithm calculateKey() {
        return Algorithm.HMAC256(SECRET);
    }

    private DecodedJWT verifyAndParse(String token) {
        Algorithm algorithm = calculateKey();
        JWTVerifier verifier = JWT.require(algorithm)
                // .withIssuer("auth0")
                // .withClaim("username", username)
                .build();

        return verifier.verify(token);
    }
}

jjwt实现类

package com.example.demo.util.impl;

import com.example.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ClaimsBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
 
@Slf4j
public class JwtUtilJJwtImpl implements JwtUtil {
    // 过期时间
    private final Duration TTL = Duration.ofSeconds(5);
 
    // 密钥(jjwt对长度有限制,必须大于32个字符)
    private final String SECRET = "123456789123456789123456789123456789";
 
    // 加密算法
    private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;
 
    // 生成token(通过用户Id)
    @Override
    public String createToken(String userId) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + TTL.toMillis());
 
        // 可自定义claim
        Map<String, String> claimMap = new HashMap<>();
        claimMap.put("key1", "value1");
 
        ClaimsBuilder claimsBuilder = Jwts.claims();
        claimsBuilder.audience().add(userId);
        claimsBuilder.add(claimMap);
        Claims claims = claimsBuilder.build();
 
        SecretKey secretKey = calculateKey();
 
        return Jwts.builder()
                .issuedAt(nowDate)
                .expiration(expireDate)
                .claims(claims)
                .signWith(secretKey, ALGORITHM)
                .compact();
    }
 
    @Override
    public boolean verifyToken(String token) {
        // 这里可以加一个非空校验
 
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            log.error("校验token失败", e);
            return false;
        }
    }
 
    // 从token中获取用户id
    @Override
    public String getUserIdByToken(String token) {
        Claims claims = parseToken(token);
        // 可获得过期时间
        //claims.getExpiration();
 
        Set<String> audienceSet = claims.getAudience();
        return audienceSet.iterator().next();
    }
 
    // 获取token中注册信息
    private Claims parseToken(String token) {
        // 这里可以加一个非空校验
 
        return Jwts.parser()
                .verifyWith(calculateKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
 
    private SecretKey calculateKey() {
        return Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
    }
}

测试

1.测试java-jwt

package com.knife.example.auth;

import com.knife.example.auth.impl.JwtUtilJavaJwtImpl;

public class Demo {

    public static void main(String[] args) {
        JwtUtil jwtUtil = new JwtUtilJavaJwtImpl();
        String token = jwtUtil.createToken("12");

        System.out.println("生成的token:" + token);

        boolean verifySuccess = jwtUtil.verifyToken(token);
        System.out.println("token验证结果:" + verifySuccess);

        String userId = jwtUtil.getUserIdByToken(token);
        System.out.println("从token读出userId:" + userId);

        // 睡眠,等待token超时
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        verifySuccess = jwtUtil.verifyToken(token);
        System.out.println("token验证结果:" + verifySuccess);
    }
}

结果

生成的token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMiIsImV4cCI6MTcxMTA5NzM1N30.pyFIlnGCeCaYYSuQerbMxjWSuG4kn9lo7wZ6wAyDX04
token验证结果:true
从token读出userId:12
token验证结果:false

2.测试jjwt

package com.knife.example.auth;

import com.knife.example.auth.impl.JwtUtilJJwtImpl;

public class Demo {

    public static void main(String[] args) {
        JwtUtil jwtUtil = new JwtUtilJJwtImpl();
        String token = jwtUtil.createToken("12");

        System.out.println("生成的token:" + token);

        boolean verifySuccess = jwtUtil.verifyToken(token);
        System.out.println("token验证结果:" + verifySuccess);

        String userId = jwtUtil.getUserIdByToken(token);
        System.out.println("从token读出userId:" + userId);

        // 睡眠,等待token超时
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        verifySuccess = jwtUtil.verifyToken(token);
        System.out.println("token验证结果:" + verifySuccess);
    }
}

结果

生成的token:eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTEwOTc2NDUsImV4cCI6MTcxMTA5NzY1MCwiYXVkIjpbIjEyIl0sImtleTEiOiJ2YWx1ZTEifQ.N5ABZ9PnxkaLqTCahJ4XqI6lPHausKCOfv-zNoR5pww
token验证结果:true
从token读出userId:12
16:54:16.402 [main] ERROR com.knife.example.auth.impl.JwtUtilJJwtImpl - 校验token失败
io.jsonwebtoken.ExpiredJwtException: JWT expired 6396 milliseconds ago at 2024-03-22T08:54:10.000Z. Current time: 2024-03-22T08:54:16.396Z. Allowed clock skew: 0 milliseconds.
	at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:682)
	at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:362)
	at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)
	at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36)
	at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29)
	at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:821)
	at com.knife.example.auth.impl.JwtUtilJJwtImpl.parseToken(JwtUtilJJwtImpl.java:82)
	at com.knife.example.auth.impl.JwtUtilJJwtImpl.verifyToken(JwtUtilJJwtImpl.java:58)
	at com.knife.example.auth.Demo.main(Demo.java:26)
token验证结果:false
0

评论0

请先

显示验证码
没有账号?注册  忘记密码?

社交账号快速登录