JWT 官网:https://jwt.io/introduction/

什么是 JWT?

JSON WEB TOKEN,它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象安全地在各方之间传输信息。此信息可以验证和信任,因为它是数字签名。JWT 可以使用密钥(使用HMAC算法)或使用 RSA 或 ECDSA 进行公钥/私钥对进行签名

它有什么作用呢?(抄自JWT官网)

  • 授权:这是使用 JWT 的最常见方案。用户登录后,每个后续请求都将包括 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销小,并且能够轻松地跨不同的域使用。
  • 信息交换:JSON 网络令牌是各方之间安全传输信息的一种好方式。由于可以对JWT进行签名(例如,使用公钥/私钥对)可以确定发件人就是他们说的。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。

我们一般只去使用 授权

为什么使用 JWT

  • 解决Session的内存占用问题 (存储于客户端)
  • 解决各个服务端 Session共享问题

JWT 认证流程

生成Token

  1. 客户端携带自己的信息 请求后台认证端口
  2. 后台核对客户端提交的信息,将用户信息作为JWT 令牌的负载(Payload)与头部进行Base64 编码的拼接,形成一个Token。格式例如Head.Payload.Singurater 以 “.” 进行拼接,这里要看仔细
  3. Token生成后,将其作为字符串以登录成功的返回结果返回给前端。
  4. 前端将后台生成的结果 存储在 loacalStorage 或者 sessionStorage上 (如果,用户退出登录,可以选择删除浏览器的响应信息即可)

使用 Token

  1. 后续用户请求,前端需要将JWT 放入http请求的 Header 中的 Authorization 位 (可以解决 XSS 和 XSRF 问题)
  2. 后台每次接受到请求,都要检查 JWT 是否存在,并验证有效性(是否有效、是是否过期等等),通过验证,就可以正常访问了。

JWT 的优势

  • 简洁 :可以通过http请求中的head 放入jwt ,其数据量小,传输速度块
  • 自包含:负载中,可以存储一部分信息,可以减少数据库的查询次数
  • 跨语言:字符串格式,任何web形式 都支持
  • 使用与微服务,不需要考虑共享问题

JWT 的结构组成 (部分抄自官网)

  • 头部(Header)
  • 负载(Payload)
  • 签名(Signature)

标头通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法,如 HMAC SHA256 或 RSA。

例如:

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

然后,此 JSON编码为 Base64Url,以形成 JWT 的第一部分。

负载

令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常为用户)和其他数据的语句。有三种类型的索赔:已登记、公共私人索赔。

  • 已注册声明:这些是一组预定义声明,不是强制性的,但建议提供一组有用的、可互操作的索赔。其中一些是:iss(发行人)、exp(到期时间)、(主题)、aud(访问者)和其他。请注意,声明名称只有三个字符,只要 JWT 是紧凑的。
  • 公共声明:这些可以由使用JWT的人可以当即定义。但是,为了避免冲突,应在IANA JSON Web 令牌注册表中定义它们,或定义为包含抗冲突命名空间的 URI。
  • 私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,它们既不是已注册的,也不是公开声明。

示例有效负载可能是:

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

然后对有效负载进行 Base64Url编码,以形成 JSON Web 令牌的第二部分。

请注意,对于已签名的令牌,此信息虽然可防止篡改,但任何人都可以阅读。除非对 JWT 进行加密,否则不要将机密信息放在 JWT 的有效负载或标头元素中。

签名

要创建签名部分,您必须使用编码标头、编码有效负载、机密、标头中指定的算法,并签名。

例如,如果要使用 HMAC SHA256 算法,将采用以下方式创建签名:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证邮件未随之更改,对于使用私钥签名的令牌,它还可以验证 JWT 的发件人是否为它所说的发件人。

放在一起

输出是三个 Base64-URL 字符串,由点分隔,这些点可以在 HTML 和 HTTP 环境中轻松传递,但与基于 XML 的标准(如 SAML)相比,更紧凑。

下面显示了一个 JWT,它具有以前的标头和有效负载编码,并且它使用机密进行签名。Encoded JWT

如果要使用 JWT 并付诸实践,可以使用 jwt.io解码、验证和生成 JWT。

JAVA 使用 JWT

生成令牌

导包

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

调用 JWT.create() 方后面的链式调用,添加相关参数

    // 生成Token
    @Test
    public void createJwt(){
        HashMap<String,Object> hashMap = new HashMap<>();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,100);              //  设置超时时间 100s
        String token = JWT.create()
                .withHeader(hashMap)                            //  Header
                .withClaim("username", "张三")       //  Payload
                .withClaim("age", 20)               //  Payload
                .withExpiresAt(instance.getTime())          //  设置超时时间
                .sign(Algorithm.HMAC256("!Govbuy2021JWT~ZLK")); //  自己生成的签名,足够复杂就行,不要泄露就行

        System.out.println("Token is in next line:");
        System.out.println(token);

    }

结果 如图

额外知识:

如果 相同的负载 字段名,例如 “username” 一个叫张三 、一个叫李四,那么就会默认使用代码顺序下面的”username”,即Claims 中 只会存在一个 “username”

验证令牌

调用 JWT 验证对象 JWTVerifier 的 verify() 方法 解析

    @Test
    public void ValidateJwt(){
        JWTVerifier build = JWT.require(Algorithm.HMAC256("!Govbuy2021JWT~ZLK")).build();
        DecodedJWT verify = build.verify("" +
                "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTM2MTM3MjksImFnZSI6MjAsInVzZXJuYW1lIjoi5byg5LiJIn0.G2tQ5564EDx1V7yF4GT6tTCn5lLZHSSdOfpm3ae73w4");
        // 获取令牌内 包含的信息
        String username = verify.getClaim("username").asString();
        int age = verify.getClaim("age").asInt();
        System.out.println(username);
        System.out.println(age);
    }

如果令牌 超过设置的过期时间 ,就会报 令牌超时异常

com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on Thu Feb 18 09:38:12 CST 2021.

整合SpringBoot

创建 自己的工具类

package com.govbuy.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

/**
 * @author : zanglikun
 * @date : 2021/2/18 9:58
 * @Version: 1.0
 * @Desc : 费劲,没啥好说的
 */
public class JWTUtils {

    private static final String Sign = "!Govbuy2021JWT~ZLK";

    /**
     * 生成 令牌
     * @param map
     * @return
     */
    public static String getToken(Map<String, String> map) {

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 7);              //  设置超时时间 7 天

        // 创建 JWT Builder
        JWTCreator.Builder builder = JWT.create();
        // 设置 Payload
        map.forEach((k, v) -> {
            builder.withClaim(k, v);
        });

        // 超时时间 与 Sign
        String sign = builder.withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(Sign));


        return sign;
    }

    /**
     * 验证 Token 的合法性
 ,只要不抛异常,就算令牌 验证成功
     *
     * @param token
     */
    public static void verifyToken(String token) {
        JWTVerifier build = JWT.require(Algorithm.HMAC256(Sign)).build();
        DecodedJWT verify = build.verify(token);
        System.out.println("签名验证成功,会打印此话");
    }

    /**
     * 获取 Token中 信息,以后 直接 通过 DecodedJWT 进行 .getClaim.asXXX() 获取;
     * @return
     */
    public DecodedJWT test(String token) {
        return JWT.require(Algorithm.HMAC256(Sign)).build().verify(token);
    }

}

未来在单体架构,就放在 拦截器 放行登录接口就行,减少验签等代码的冗余,分布式架构,就放在网关里面即可。