web: jwt令牌构成、创建的基本流程及原理

一、JWT 的构成

1. 概念

json web token(JWT) 本质是一串含义验证信息的字符串,服务器根据JWT和密钥,经过加密算法,验证该字符串是否有效。

2. 构成

JWT由Header、Payload、Signature构成,每个部分都是一个token,如下:

它们的实际含义如下:

        Header:json字符串,指定加密算法(供Signature使用)和类型(一般写死为“JWT”)

        Payload:json字符串,包含通用信息(如发布者iss、发布时间戳iat、过期时间戳exp)和自定义属性(如uid)

        Signature:加密函数,输入“Header”、“Payload”、“密钥”,输出密文

3. 验证方式

STEP 1:客户端登录,传递用户名、密码,服务器端验证资格,成功后生成JWT(Header、Payload、Signature皆由服务器端生成)

STEP 2:服务器端返回JWT给客户端,客户端将JWT存储在本地或浏览器中

STEP 3:客户端请求资源,传递JWT,服务器端获得JWT后,使用Header指定的加密算法,输入“Header"、"Payload"、以及服务器端存储的”密钥“,计算Signature的部分,看看服务器端计算的Signature和客户端传递的Signature是否一致,一致则验证通过。

(图片来自https://www.bilibili.com/video/BV13t5PzDEzh

二、JWT的创建过程

1. 确定 Token 的基本信息(Claims 设计)

JWT 的本质是一个 带签名的 Claims 集合,生成前需明确:

(1)通用声明(Registered Claims)

常见、推荐使用的字段:

Claim含义是否必须
iss签发者(issuer)
sub主题(subject,一般是用户 ID / username)是(推荐)
aud接收方
iat签发时间是(推荐)
exp过期时间是(强烈推荐)
nbf生效时间
jtiJWT 唯一标识
(2)自定义声明(Private Claims)

用于业务识别,例如:

Claim含义是否必须
uid用户id
name用户名
原则:不放敏感信息(如密码)

2. 选择签名算法(Algorithm)

这是 JWT 安全性的核心。

算法类型示例特点
对称加密HS256 / HS512简单、性能高,签发与验证用同一密钥
非对称加密RS256 / ES256私钥签发,公钥验签,更安全

业务系统中最常见:

  • 单体 / 内部系统:HS256
  • 微服务 / 第三方接入:RS256

3. 准备密钥(Secret / Key)

  • HS256:一段足够复杂的字符串(≥ 256 bit)
  • RS256:RSA 私钥(签名)+ 公钥(验证)
密钥应存放在:配置中心 如 application.yml环境变量Vault
❌ 不要硬编码在代码中

如配置在yml中

 jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 admin-ttl: 7200000 # 设置前端传递过来的令牌名称 admin-token-name: token user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication

4. 构造 Claims

将步骤 1 中的字段写入 token 负载:

  • 标准声明
  • 自定义业务字段
  • 时间字段统一使用 Date
Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.USER_ID,user.getId()); String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

5. 生成并签名 JWT

Header + Payload 使用指定算法和密钥进行签名,得到最终的 JWT 字符串

xxxxx.yyyyy.zzzzz 

6. 返回 Token 给客户端

常见返回方式:

JSON 响应体

{ "access_token": "...", "expires_in": 7200 } 

HTTP Header

Authorization: Bearer <jwt> 

三、Spring Boot 中JWT 生成的标准代码结构

代码来自于苍穹外卖,它自己定义了一个JwtUtil类专门处理JWT相关,其中定义了一个createJWT方法,用于创建JWT。

这个例子只是生成JWT的一种,不一定按照这样做

0. 定义createJWT方法,标准化JWT创建流程

苍穹外卖里面既有商家端的员工登录,又有客户端的微信用户登录,都要用到JWT,因此它写了一个通用类来专门处理JWT相关,它的流程是这样的:

1. 传入secretKey(密钥)、ttl(生存时间)、claims(一个HashMap,存储payload部分的信息)

2. 指定加密算法

3. 根据ttl(生存时间) 计算exp(过期时间)

4. 构建JWT,调用官方的Jwts传入 claims、签名(指定的算法、密钥等)、过期时间

package com.sky.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 设置的信息 * @return */ 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); return builder.compact(); } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } } 

1. 在application.yml中配置jwt

上面说到secret key 最好放在配置中,不要放在代码里,因此苍穹外卖在yml中配置

sky: jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 admin-ttl: 7200000 # 设置前端传递过来的令牌名称 admin-token-name: token user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication

2. 配置JwtProperties,读取application.yml中jwt的相关配置

上面的yml配置的jwt属性,需要通过properties读取到

package com.sky.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "sky.jwt") @Data public class JwtProperties { /** * 管理端员工生成jwt令牌相关配置 */ private String adminSecretKey; private long adminTtl; private String adminTokenName; /** * 用户端微信用户生成jwt令牌相关配置 */ private String userSecretKey; private long userTtl; private String userTokenName; } 

3. 在登录的Controller中生成JWT,返回给用户

基本就是两个步骤

1. 创建claims,是一个hashMap,存入“用户id:xxxx" 的键值对。这个东西用来唯一标识是哪个用户。

2. 创建JWT,传入secretKey(密钥)、ttl(生存时间)、claims。createJWT方法内部再进一步的设置加密算法、

员工登录

package com.sky.controller.admin; import com.sky.constant.JwtClaimsConstant; import com.sky.dto.EmployeeDTO; import com.sky.dto.EmployeeLoginDTO; import com.sky.dto.EmployeePageQueryDTO; import com.sky.entity.Employee; import com.sky.properties.JwtProperties; import com.sky.result.PageResult; import com.sky.result.Result; import com.sky.service.EmployeeService; import com.sky.utils.JwtUtil; import com.sky.vo.EmployeeLoginVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * 员工管理 */ @RestController @RequestMapping("/admin/employee") @Slf4j @Api(tags = "员工相关接口") public class EmployeeController { @Autowired private EmployeeService employeeService; @Autowired private JwtProperties jwtProperties; /** * 登录 * @param employeeLoginDTO * @return */ @PostMapping("/login") @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) { log.info("员工登录:{}", employeeLoginDTO); Employee employee = employeeService.login(employeeLoginDTO); //登录成功后,生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims); EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder() .id(employee.getId()) .userName(employee.getUsername()) .name(employee.getName()) .token(token) .build(); return Result.success(employeeLoginVO); }

微信用户登录

package com.sky.controller.user; import com.sky.constant.JwtClaimsConstant; import com.sky.dto.UserLoginDTO; import com.sky.entity.User; import com.sky.properties.JwtProperties; import com.sky.result.Result; import com.sky.service.UserService; import com.sky.utils.JwtUtil; import com.sky.vo.UserLoginVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/user/user") @Api(tags = "C端用户相关接口") @Slf4j public class UserController { @Autowired private UserService userService; @Autowired private JwtProperties jwtProperties; /** * 微信登录 * @param userLoginDTO * @return */ @PostMapping("/login") @ApiOperation("微信登录") public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){ log.info("微信用户登录:{}",userLoginDTO.getCode()); //微信登录 User user = userService.wxLogin(userLoginDTO); //为微信用户生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.USER_ID,user.getId()); String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims); UserLoginVO userLoginVO = UserLoginVO.builder() .id(user.getId()) .openid(user.getOpenid()) .token(token) .build(); return Result.success(userLoginVO); } } 

四、JWT 与 Spring Security 的关系

  • JWT 本身只负责“凭证”
  • Spring Security 负责:
    • 校验 JWT
    • 解析 Claims
    • 构造 Authentication

JWT ≠ 登录框架,而是 无状态认证载体

Could not load content