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皆由服务器端生成,并且保存生成JWT所使用的密钥)

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 生效时间
jti JWT 唯一标识
(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中

XML 复制代码
  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

java 复制代码
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 给客户端

常见返回方式:

  • HTTP Header

    复制代码
    Authorization: Bearer <jwt>
  • JSON 响应体

    复制代码
    {
      "access_token": "...",
      "expires_in": 7200
    }

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

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

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

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

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

  1. 传入secretKey(密钥)、ttl(生存时间)、claims(一个HashMap,表示用户信息)

  2. 指定加密算法

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

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

java 复制代码
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中配置

XML 复制代码
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读取到

java 复制代码
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方法内部再进一步的设置加密算法、

员工登录

java 复制代码
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);
    }

微信用户登录

java 复制代码
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 ≠ 登录框架,而是 无状态认证载体

相关推荐
码农水水2 小时前
宇树科技Java被问:数据库连接池的工作原理
java·数据库·后端·oracle
Seven972 小时前
回溯算法总结
java
小鸡脚来咯2 小时前
软链接的作用和用途
java·ide·eclipse
这周也會开心2 小时前
双栈实现队列以及双队列实现栈
java·开发语言
小北方城市网2 小时前
解析GEO:定义、价值与忽视的代价
python·ai·geo
Bruce_kaizy2 小时前
c++图论——最短路之Johnson算法
开发语言·数据结构·c++·算法·图论
“抚琴”的人2 小时前
C#上位机观察者模式
开发语言·观察者模式·c#·上位机
乾元2 小时前
AI 在云网络(VPC / VNet)部署的编排与安全对齐——从“手工堆资源”到“意图驱动的网络生成”(含 Terraform 工程化)
运维·网络·人工智能·网络协议·安全·云计算·terraform
思成Codes2 小时前
Go语言的多返回值是如何实现的?
开发语言·后端·golang