详解 JWT(SpringBoot 整合 JWT 示例)

​ 翻看了我之前写的 JWT 的笔记的时候感觉因为理解不够深入有很多地方没有解释清楚,今天学习 API 签名认证的时候有了更深的理解,所以决定重写一下之前的笔记。
这里附上原本笔记的地址,欢迎大家批评指正:https://blog.csdn.net/weixin_74895237/article/details/134042538

什么是 JWT?

先来看一下官方文档对 JWT 的介绍:

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于作为 JSON 对象在各方之间安全地传输信息。此信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
尽管 JWT 可以加密以在各方之间提供保密性,但我们将重点关注签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则对其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还会证明只有持有私钥的一方才是签名的一方。
下面,我们来尝试理解一下这两段话。

为什么需要 JWT?

想要理解第一段话,就需要知道 JWT 是被用来做什么的

  • JWT(JSON Web Token),是一种开放的标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全的传递信息,是一种轻量级的自包含的可验证的令牌,通常用于身份验证授权机制。

JWT 的特点

其中轻量级和可验证是比较好理解的,这里来说一下自包含的含义:

“自包含”(self-contained)意味着令牌本身包含了所有必要的信息,而不需要依赖于额外的信息或状态。具体来说,JWT的信息被存储在令牌本身的数据部分中,并通过签名进行验证。因此,接收方可以通过 **解码 **JWT的数据部分并验证签名,来获取和验证令牌中的信息而无需访问服务器或其他存储

这是通过 JWT 组成部分中的载荷(Payload)来实现的,这里面包含了实际的信息,例如用户的身份、权限等,这样就可以实现自包含,后面会更加详细的介绍 JWT 的三个部分。

常用功能的举例

JWT 通常用于实现身份验证和授权机制,下面我们来看一下这两个功能:‘

身份验证

JWT可以用作认证机制,允许用户在登录后获取系统生成的令牌,然后后面继续请求服务的时候将该令牌放在请求头中用于后续的请求。
服务器可以验证JWT的签名,从而确认请求的发起者是经过身份验证的用户,比如可以通过解密来拿取我们放入载荷中的用户信息。这避免了在每个请求中都需要重新验证用户的凭据

为了更好的理解这段话,这里我们来回顾一下之前学过的 Session 技术是如何实现的身份验证:

  1. 用户登录: 用户提供用户名和密码,服务器验证这些凭据的有效性。
  2. 创建 Session: 如果用户名和密码验证通过,服务器会创建一个会话(Session),并为用户分配一个唯一的会话标识(通常是一个 session ID)。
  3. 存储会话信息: 服务器将用户的身份信息存储在会话中,可以存储在内存中、数据库中或其他持久化存储中。
  4. 返回 Session ID: 服务器将生成的 session ID 返回给客户端,通常通过设置一个名为 “sessionID” 的 Cookie 或者其他方式。
  5. 后续请求: 客户端在后续的请求中通过发送 session ID,服务器通过该 ID 查找对应的会话,并验证用户的身份。


Session 技术的验证过程通常需要依赖服务器端存储,因为会话信息通常存储在服务器端。这就意味着在每个请求中,服务器都需要查找和验证 session ID,以确保用户的身份是有效的,所以需要在每个请求中重新验证用户凭据
再来看 JWT,JWT 的自包含性允许在每个请求中不需要查找服务器端存储,因为所有验证和授权所需的信息都包含在 JWT 中。这使得 JWT 在分布式系统和前后端分离的应用中更具灵活性。

授权机制

JWT可以包含用户的权限信息或角色信息,允许服务器在每个请求中使用这些信息来控制对资源的访问。因为JWT的信息可以被解码,服务端可以轻松获取其中的授权信息。

这个机制同样是 JWT 的自包含性来辅助实现的。

通过上面的案例相信能帮助你更好的理解 JWT 可以用来做什么,下面我们来看看 JWT 的结构。

JWT 的结构

一个 JWT 主要由三部分组成:头部(Header)载荷(Payload)签名(Signature)。

image.png

结构介绍与示例

头部(Header)

头部通常由两部分组成,指明类型和使用的签名算法。通常,它的结构是一个 JSON 对象,例如:

{
  "alg": "HS256",   // 签名算法,例如 HMAC SHA-256
  "typ": "JWT"      // 令牌类型
}

载荷(Payload)

载荷包含有关声明(claims)的信息。声明是关于实体(通常是用户)和其他数据的声明。有三种类型的声明:注册声明、公共声明和私有声明。例如:

{
  "sub": "1234567890",   // 主题
  "name": "John Doe",
  "iat": 1516239022      // 签发时间
}

载荷是 JWT 的主体内容,它默认给我们提供了七个默认字段,入上面的示例就用到了其中的两个默认字段,除了默认字段我们还可以定义自己的私有字段,可以把用户的信息放在载荷中,实现自包含性。

iss(Issuer): 表示令牌的发行者。
sub(Subject): 表示令牌的主题,即令牌所代表的用户。
aud(Audience): 表示令牌的受众,即预期的接收者。
exp(Expiration Time): 表示令牌的过期时间。
nbf(Not Before): 表示令牌生效的时间。
iat(Issued At): 表示令牌的签发时间。
jti(JWT ID: 表示令牌的唯一标识。

下面给出一个具体的示例:

{
  "iss": "your_issuer",          // 发行者
  "sub": "user123",              // 主题,用户ID或用户名
  "iat": 1636857600,             // 签发时间
  "exp": 1636861200,             // 过期时间
  "roles": ["user", "admin"]     // 用户角色信息
}

需要注意的是 JWT 的默认情况下是未加密的,头部和载荷都是 Base64 编码的 JSON 字符串,是可读的,但它们并没有经过加密。签名部分使用密钥和哈希算法进行签名,但它不加密整个令牌的内容。
JWT 的设计目标之一是简化令牌的传递和验证过程,而不是提供加密级别的安全性。如果需要加密令牌的内容,可以选择使用 JWE(JSON Web Encryption)来提供额外的安全性,但这会增加复杂性。
需要注意的是,未加密不代表 JWT 是不安全的,只是不要在 JWT 中传递用户的私密信息,比如密码,JWT 的安全性主要是由密钥保障的,签名使用密钥进行生成,只有知道密钥的一方才能验证令牌的正确性。

签名(Signature)

为了验证消息的完整性,将编码后的头部、编码后的载荷和秘钥一起进行签名。签名的算法在头部中被指定。例如,使用 HMAC SHA-256 算法:

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

最终生成的 JWT

最终,将编码后的头部、编码后的载荷和签名通过点号连接在一起,形成最终的 JWT:

base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature

JWT 的验证过程

  1. 客户端向服务器发送请求(比如登录且登录成功)。
  2. 服务器通过信息编辑好令牌并将其返回给服务端。
  3. 客户端每次请求你的资源(如开放 API)都会带上这个令牌来用于标识信息等功能。

SpringBoot 整合 JWT

了解了 JWT 的好处,下面给出一个利用 JWT 实现登录功能的示例,利用这个案例可以帮助我们更好的理解 JWT 的使用。

引入依赖

使用 JWT 首先要有能够创建 JWT 令牌的方法,我们不能每次都去在线的生成工具中去编辑生成我们的 JWT,因此要有相关的依赖协助我们生成。

这里我们使用 JJWT 来实现 JWT 令牌的生成和验证,使用它使用人数最多的 0.91 版本

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt/0.9.1 Maven 仓库地址
https://github.com/jwtk/jjwt GitHub 开源地址

测试功能

密钥的生成

class JjwtTestApplicationTests {

    @Test
    void jwtTest() {
        // 设置位于头部的签名算法
        SignatureAlgorithm signatureAlgorithm =SignatureAlgorithm.HS256;
        String secretKey = "jwtTest";
        // 利用 claim 来设定私有声明
        Long id = 1L;
        Map<String, Object> claim = new HashMap<>();
        claim.put("userId", id);
        JwtBuilder builder = Jwts.builder()
        .setClaims(claim)
        .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8));
        String compact = builder.compact();
        System.out.println(compact);
    }
}

测试成功,输出信息

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.91B00ttDwbLvDkm_bB5TKD8ZowWDM48xGAx468yqYQA

密钥的解密

尝试对上面生成的密钥进行解密

    Claims body = Jwts.parser()
    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
    .parseClaimsJws(compact)
    .getBody();
    System.out.println(body.get("userId"));

JJWT 提供的方法

这里我们来说一下 JJWT 为我们提供的方法,这些方法可以帮助我们自由的创建 JWT
标准声明(Standard Claims):

  1. setIssuer(String iss):
    • 设置 JWT 的签发者(Issuer)。
  2. setSubject(String sub):
    • 设置 JWT 的主题(Subject)。
  3. setAudience(String aud):
    • 设置 JWT 的接收方(Audience)。
  4. setExpiration(Date exp):
    • 设置 JWT 的过期时间。
  5. setNotBefore(Date nbf):
    • 设置 JWT 的生效时间。
  6. setIssuedAt(Date iat):
    • 设置 JWT 的签发时间。
  7. setId(String jti):
    • 设置 JWT 的唯一身份标识(JWT ID)。

自定义声明(Custom Claims):

  1. claim(String name, Object value):
    • 添加自定义声明。
  2. addClaims(Map<String, Object> claims):
    • 添加多个自定义声明。

自动声明要写在标准声明的前面,否则会覆盖写的标准声明
签名设置:

  1. signWith(Key key):
    • 使用给定的密钥对 JWT 进行签名。
  2. signWith(SignatureAlgorithm alg, Key key):
    • 使用给定的签名算法和密钥对 JWT 进行签名。

其他设置:

其他设置:

  1. setHeaderParam(String name, Object value):
    • 设置 JWT 头部的参数。
  2. compressWith(CompressionCodec compressionCodec):
    • 设置 JWT 的压缩算法。

配置类的书写

为了规范的管理和设置 JWT 的信息,这里我们先书写一个配置类。

配置类

@Component
@ConfigurationProperties(prefix = "jjwttest.jwt")
@Data
public class JwtProperties {
    private String SecretKey;
    private long Ttl;
    private String TokenName;
}

通过上面的三个注解我们可以实现在配置文件中设置这个类的属性,生成这个类并交给 spring 管理,这样我们就可以 GET 便捷的拿取我们设置的这些属性。

配置文件

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    secret-key: itcast
    # 设置jwt过期时间,单位为毫秒
   	ttl: 7200000
    # 设置前端传递过来的令牌名称
    token-name: token

构建 Util 类来帮助我们生成和解密

为了避免代码的的重复编写,我们可以写一个配置类来辅助我们使用 JJWT。

/**
*	生成 JWT 令牌的方法
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
    // 指定签名的时候使用的签名算法,也就是header那部分
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成JWT的时间
    long expMillis = System.currentTimeMillis() + ttlMillis;
    Date exp = new Date(expMillis);

    // 设置jwt的body
    JwtBuilder builder = Jwts.builder()
            // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
            .setClaims(claims)
            // 设置签名使用的签名算法和签名使用的秘钥
            .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
            // 设置过期时间
            .setExpiration(exp);
	// 压缩和签名 jwt
    return builder.compact();
}
/**
*	解密 JWT 令牌的方法
*/
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt,这里不要用错方法,是Jws
                .parseClaimsJws(token).getBody();
        return claims;
    }

JWT 拦截器

拦截器代码书写

这样每次可以对前端的请求头中的数据进行校验,这里我们使用 try-catch,如果捕获到错误说明传过来的令牌有问题。

@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    // 注入我们前面写好的配置类
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断当前拦截到的是Controller的方法还是其他资源
        // 注意,这时候登录方法也会拦截,我们后面要书写配置防止其拦截登录方法
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long userId = Long.valueOf(claims.get("userId").toString());
            //3、通过,放行
            	// 将用户 id 存在当前的线程中
            BaseContext.setCurrentId(empId);
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

拦截器配置

添加拦截器并且将登录方法排除在外。

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor

    /**
     * 注册自定义拦截器
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                // 将登录方法排除在外
                .excludePathPatterns("/user/login");
    }
}

书写登陆方法

做好了前期的准备,现在我们正式来书写登录的方法。

    /**
     * 登录方法
     */
    @PostMapping("/login")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {

        User user = userService.login(employeeLoginDTO);

        //登录成功后,生成 jwt 令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims);

        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .userName(user.getUsername())
                .name(user.getName())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }

总结

这样我们就实现了 JWT 的登录功能,首先,前端传过来登录信息会绕过拦截器,获取到自己的登录令牌后再执行除了登录后的 Controller 中的方法都会被拦截器拦截并且校验身份。
在配置 JWT 的时候,我们将用户 Id 信息存入到负载中,这样我们就可以方便的得到和验证用户的 id。
我们还书写了 JwtProperties 类来管理和设置 Jwt 的相关参数。
这样一个比较规范的注册功能就写好了,其中的一些名称字符串如 “userId” 可以通过设置常量类来使得书写更加的规范优雅。

  • 14
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值