JWT

learn about JWT

Posted by lily on May 7, 2023

初识JWT

在用户访问资源过程中,服务器需要解决两个问题:身份验证和授权。

  1. 身份验证解决“你是谁”的问题
  2. 授权解决“你能做什么”的问题。 JWT 是一种用于在双方(例如客户端和服务器)之间安全传输信息的轻量级、自包含的 Token。 —

    JWT组成部分

    JWT 由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。

  3. 头部(Header):头部包含 Token 类型(通常为 “JWT”)和使用的加密算法。头部是 Base64Url 编码的 JSON 字符串,例如:

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

    其中,alg 表示使用的加密算法(在这里为 HMAC-SHA256),typ 表示 Token 类型。

  4. 负载(Payload):负载是包含一系列声明(claim)的 JSON 字符串,这些声明包含关于用户的信息和其他数据。负载中的数据称为声明(claim),分为三种类型:注册声明、公共声明和私有声明。负载也是 Base64Url 编码的 JSON 字符串。例如:

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

    其中,sub 是用户标识(subject),name 是私有声明,iat 是 JWT 的签发时间(issued at),exp 是 JWT 的过期时间(expiration time)。

  5. 签名(Signature):签名是通过将头部(Header)、负载(Payload)和一个密钥使用加密算法进行加密得到的。签名的目的是确保 JWT 的完整性和防止篡改。例如,对于 HMAC-SHA256 算法:

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

    其中,secret_key 是服务器保存的密钥,用于签名和验证 JWT。

将头部、负载和签名通过点(.)连接起来,得到完整的 JWT。例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.MzFjODE3MTJkZmIzZjY4ZDU2N2QwODg4NTIyNTA0ODI

JWT特点

而JWT(JSON Web Tooken),是一种用于授权而非身份验证的 JSON 网络令牌。它取代了用于用户识别的会话和 cookie。

Detailed Summary for What Is JWT and Why Should You Use JWT by Merlin

00:00 了解JWT授权

  • JWT 用于授权,而不是认证
  • JWT 使用 JSON 网络令牌而不是 cookie 进行授权

02:04 两种类型的身份验证:基于会话和基于 JWT

  • 基于会话的身份验证将用户信息存储在服务器内存中,并使用 cookie 将唯一 ID 发送回浏览器。
  • 基于 JWT 的身份验证创建一个包含用户信息的 JSON Web 令牌并将其发送回浏览器,服务器上不存储任何信息。

04:02 JWT 将用户信息存储在令牌中,使其更具可扩展性和效率。

  • JWT 使用密钥验证令牌的完整性。
  • 用户信息存储在令牌中,无需服务器端查找。

05:58 JSON Web Tokens 包含三个部分:标头、有效负载和签名。

  • 标头确定用于编码和解码令牌的算法。
  • Payload 存储信息,可以包括主题和发布时间等公共字段。

07:48 令牌过期和验证对于安全性至关重要。

  • 令牌到期可防止未经授权的访问。
  • 令牌验证确保数据完整性。

09:34 JWT 确保数据的完整性和真实性

  • 标头和有效负载组合在一起并使用密钥进行哈希处理
  • 篡改数据导致不同的散列,表明无效数据

11:22 使用 JWT 允许在不同的服务器之间进行无缝身份验证

  • JWT 允许在客户端存储用户信息,避免将其存储在单独的服务器上
  • 在服务器之间共享相同的密钥允许在不需要用户重新登录的情况下进行身份验证

13:11 JWT 允许用户通过任何服务器或微服务进行身份验证,只要他们拥有相同的密钥即可。

  • JWT 令牌由具有相同密钥的所有服务器识别。
  • 用户存储在客户端上,因此无论负载均衡器或架构如何,他们都可以通过任何服务器或微服务进行身份验证。

为什么要使用JWT而不是传统的会话和cookie?

这句话表示的是传统的基于 session 和 cookie 的鉴权方式存在一定的安全风险,并且在实现跨域和多应用间的鉴权时较为复杂。下面分别举例说明:

  1. 安全风险:

    • Session劫持:攻击者可能通过各种手段获取用户的 session ID(例如,通过监听网络传输、获取浏览器的存储等),然后伪装成用户发起请求。一旦攻击者获取了用户的 session ID,他们就可以执行未经授权的操作。

    • 跨站请求伪造(CSRF):攻击者利用用户在其他网站的 cookie(包含 session ID),构造一个伪造的请求,诱导用户在没有意识到的情况下执行某些操作。由于浏览器会自动将 cookie 附加到同源请求中,服务器很难区分这些请求是否合法。

  2. 跨域和应用之间的鉴权难以实现:

    • Cookie 受同源策略限制:浏览器的同源策略规定,只有同源的请求才会附加 cookie。在跨域请求的情况下,cookie 不会被自动附加,使得基于 cookie 的 session 机制难以实现跨域鉴权。

    • Session 与应用的耦合:Session 通常依赖于特定的应用服务器和内存进行存储,这使得在多个应用或者微服务架构之间共享 session 变得复杂。例如,你需要为不同的服务或应用配置单点登录(SSO)或其他集中式的会话管理解决方案,这会增加开发和维护的复杂性。

与之相比,基于 JWT 的鉴权具有更好的安全性和跨域支持。JWT 可以使用签名防止篡改,请求头中的 JWT 不受同源策略的限制,可以实现跨域鉴权。同时,JWT 无状态的特性使得在多应用或者微服务架构中共享鉴权信息变得更简单。

JWT的工作原理

在实际应用中,JWT 的使用流程如下:

  1. 用户通过提供用户名和密码登录系统。
  2. 服务器验证用户提供的凭据,如果验证通过,服务器将生成一个包含认证信息的 JWT,并将其发送给客户端。
  3. 客户端在收到 JWT 后将其存储在本地,例如在 localStorage、sessionStorage 或者 Cookie 中。
  4. 客户端在后续的请求中将 JWT 附加到请求头中,通常使用 Authorization 头,例如:Authorization: Bearer <JWT>
  5. 服务器收到请求后,首先提取请求头中的 JWT。然后,服务器使用相同的加密算法和密钥验证签名。如果签名验证通过,说明 JWT 是有效的且未被篡改。
  6. 服务器接着检查 JWT 的元数据,例如过期时间、签发者等。如果 JWT 仍然有效,服务器将提取负载(Payload)中的认证信息(如用户 ID),然后执行相应的业务逻辑。
  7. 如果 JWT 已过期或无效,服务器将拒绝请求。客户端可能需要重新登录以获取新的 JWT。 图片流程

JWT(JSON Web Token)在验证信息时不需要查询数据库或者硬编码信息在 Java 代码中。JWT 是一种自包含的 Token,它将认证信息(如用户 ID)、元数据(如过期时间、签发者)以及一个签名组合在一起。签名是为了确保 Token 的完整性和安全性,在创建和验证 JWT 时使用。

JWT 由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。这三部分通过点(.)分隔,以 Base64Url 编码表示。验证 JWT 的过程如下:

  1. 提取 JWT:当客户端发起请求时,会在请求头中携带 JWT,通常放在 Authorization 请求头中,例如:Authorization: Bearer <JWT>。服务器首先需要提取请求头中的 JWT。

  2. 解码和验证签名:服务器解码 JWT,提取头部(Header)和负载(Payload),然后使用预先设定的密钥(对称加密,如 HMAC)或密钥对(非对称加密,如 RSA 或 ECDSA)验证签名。如果签名验证通过,说明 JWT 是由服务器颁发的且未被篡改。

  3. 检查元数据:服务器检查 JWT 的元数据,如过期时间、签发者等,以确认 JWT 仍然有效。如果 JWT 已过期或元数据无效,服务器将拒绝请求。

  4. 提取认证信息:验证成功后,服务器从 JWT 的负载(Payload)中提取认证信息(如用户 ID),然后执行相应的业务逻辑。

整个验证过程不需要查询数据库,因为 JWT 中已经包含了认证信息和签名。在创建 JWT 时,服务器会使用密钥(对称加密)或密钥对(非对称加密)对 Token 进行签名。在验证 JWT 时,服务器只需要使用相同的密钥(对称加密)或公钥(非对称加密)来验证签名,无需访问数据库或硬编码信息。

SpringBoo+JWT+shiro实现权限控制

程序逻辑

  1. 我们 POST 用户名与密码到 /login 进行登入,如果成功返回一个加密 token,失败的话直接返回 401 错误。
  2. 之后用户访问每一个需要权限的网址请求必须在 header 中添加 Authorization 字段,例如 Authorization: token ,token 为密钥。
  3. 后台会进行 token 的校验,如果有误会直接返回 401。

具体实现设计

  1. pom配置引入JWT依赖
  2. 连接数据源(模拟查询数据库角色权限)
  3. 定义MVC结构用于测试
  4. 配置JWT
    1. 定义JWT加密、校验工具类Util
  5. 定义异常类

  1. pom.xml配置依赖 ```xml
io.jsonwebtoken jjwt 0.9.0

```Java
public class JWTUtil {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            /*
            这行代码是使用HMAC256算法创建一个算法实例,
            其中参数secret是用于生成密钥的秘密字符串。
            HMAC256是一种基于哈希函数的消息认证码算法,
            用于计算和验证消息完整性和真实性。
            在使用HMAC256算法进行加密或验证时,需要提供一个密钥。
            */
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}