JWT的使用方法

实现 JWT (JSON Web Token) 的配置和开发主要包含以下 5 个核心步骤。在 Spring Boot 项目中,通常配合拦截器(Interceptor)来实现登录验证。

后端实现流程

1. 引入依赖 (pom.xml)

首先需要在项目中引入 JWT 的工具包,最常用的是 jjwt

XML 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

2. 编写工具类 (JwtUtils)

我们需要一个工具类来负责 生成 Token解析 Token

java 复制代码
package com.qcby.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtils {
    // 1. 定义秘钥 (只有服务端知道,不能泄露)
    private static final String SECRET_KEY = "MySecretKey123";
    // 2. 过期时间 (例如 24 小时)
    private static final long TTL = 24 * 60 * 60 * 1000L;

    /**
     * 生成 Token
     * @param claims 需要存入 Token 的业务数据 (如用户ID, 用户名)
     * @return Token 字符串
     */
    public static String createToken(Map<String, Object> claims) {
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)                   // 设置载荷
                .setIssuedAt(new Date())             // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + TTL)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY); // 签名算法 + 秘钥
        return builder.compact();
    }

    /**
     * 解析 Token
     * @param token 客户端传来的 Token
     * @return 载荷数据 (Claims)
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

3. 编写拦截器 (JwtInterceptor)

拦截器用于拦截请求,检查请求头中是否携带了合法的 Token。

java 复制代码
package com.qcby.interceptor;

import com.qcby.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 如果是 OPTIONS 请求(预检请求),直接放行
        if("OPTIONS".equals(request.getMethod().toUpperCase())) {
            return true;
        }

        // 2. 从请求头获取 token (通常约定 key 为 "token" 或 "Authorization")
        String token = request.getHeader("token");

        // 3. 校验 token 是否存在
        if (token == null || token.isEmpty()) {
            response.setStatus(401); // 未授权
            response.getWriter().write("Token is missing");
            return false;
        }

        try {
            // 4. 解析 token (如果过期或被篡改,这里会抛出异常)
            Claims claims = JwtUtils.parseToken(token);
            
            // 5. 将解析出的用户信息放入 request,方便 Controller 使用
            request.setAttribute("currUser", claims);
            return true; // 放行

        } catch (Exception e) {
            response.setStatus(401);
            response.getWriter().write("Token is invalid or expired");
            return false; // 拦截
        }
    }
}

4. 注册拦截器 (WebConfig)

配置拦截器,告诉 Spring Boot 哪些路径需要拦截(如 /user/**),哪些不需要(如 /login)。

java 复制代码
package com.qcby.config;

import com.qcby.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/**")        // 拦截所有路径
                .excludePathPatterns(          // 排除以下路径
                        "/login",              // 登录接口不能拦截
                        "/register",           // 注册接口
                        "/static/**"           // 静态资源
                );
    }
}

5. 业务实现 (LoginController)

在登录成功后,调用 JwtUtils 生成 Token 并返回给前端。

java 复制代码
package com.qcby.controller;

import com.qcby.entity.User;
import com.qcby.utils.JwtUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

@RestController
public class LoginController {

    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody User user) {
        Map<String, Object> result = new HashMap<>();

        // 1. 模拟数据库校验 (实际开发请调用 Service)
        if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
            
            // 2. 登录成功,准备存入 Token 的数据
            Map<String, Object> claims = new HashMap<>();
            claims.put("username", user.getUsername());
            claims.put("role", "admin");

            // 3. 生成 Token
            String token = JwtUtils.createToken(claims);

            // 4. 返回 Token 给前端
            result.put("code", 200);
            result.put("msg", "登录成功");
            result.put("token", token); // 核心:把 token 给前端
        } else {
            result.put("code", 400);
            result.put("msg", "账号或密码错误");
        }
        return result;
    }
    
    // 测试需要验证接口
    @GetMapping("/api/test")
    public String testApi(HttpServletRequest request) {
        // 从拦截器存入的 request 中获取用户信息
        Map<String, Object> userInfo = (Map<String, Object>) request.getAttribute("currUser");
        return "当前访问用户是:" + userInfo.get("username");
    }
}

总结

  1. 引包jjwt

  2. 工具createToken (生成) 和 parseToken (验证)。

  3. 拦截 :在 preHandle 中检查 Header 里的 token,验证通过则放行。

  4. 配置addInterceptors 绑定拦截路径,务必排除 /login

  5. 使用:登录成功发 Token,后续请求带 Token。

前端实现流程

前端 JWT 的实现流程主要包含 存储 Token请求携带 Token响应处理 以及 路由守卫 四个核心环节。通常配合 Axios 请求库和前端路由(如 Vue Router)来实现。

以下是基于 Vue + Axios 的标准实现流程(React/原生 JS 逻辑完全一致):

1. 登录与存储 (Login & Storage)

用户填写账号密码后,前端发送请求。后端验证通过返回 Token,前端需要把它存起来(通常存在 localStoragesessionStorage)。

代码示例 (Login.vue):

复制代码
// 登录方法
async function login() {
  try {
    // 1. 发送登录请求
    const res = await axios.post('/api/login', {
      username: 'admin',
      password: '123'
    });

    if (res.data.code === 200) {
      // 2. 核心步骤:将后端返回的 Token 存入本地缓存
      const token = res.data.token;
      localStorage.setItem('token', token); 
      
      // 3. 跳转到首页
      router.push('/home');
    } else {
      alert(res.data.msg);
    }
  } catch (err) {
    console.error(err);
  }
}

2. 请求拦截器 (Request Interceptor)

这是最关键的一步。为了避免每次请求都手动写 headers: { token: ... },我们配置 Axios 的请求拦截器 。它会在每个请求发出之前自动执行,把 Token 塞进请求头。

代码示例 (request.js / main.js):

复制代码
import axios from 'axios';

// 创建 axios 实例
const service = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 5000
});

// 🟢 请求拦截器
service.interceptors.request.use(
  config => {
    // 1. 从缓存中获取 Token
    const token = localStorage.getItem('token');
    
    // 2. 如果 Token 存在,就把它添加到请求头中
    if (token) {
      // header 的 key 要和后端拦截器里取的一致 (比如 'token' 或 'Authorization')
      config.headers['token'] = token; 
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

export default service;

3. 响应拦截器 (Response Interceptor)

用于全局处理 Token 失效的情况。如果后端返回 401 状态码(代表 Token 过期或无效),前端应该自动清空缓存并跳转回登录页。

代码示例 (request.js):

复制代码
// 🔴 响应拦截器
service.interceptors.response.use(
  response => {
    // 请求成功,直接返回数据
    return response;
  },
  error => {
    // 处理错误响应
    if (error.response && error.response.status === 401) {
      // 1. 401 说明 Token 过期或无效
      alert("登录已过期,请重新登录");
      
      // 2. 清除本地过期的 Token
      localStorage.removeItem('token');
      
      // 3. 强制跳转回登录页
      location.href = '/login';
    }
    return Promise.reject(error);
  }
);

4. 路由守卫 (Router Guard)

防止用户在没有登录的情况下,直接在浏览器地址栏输入 /home 强行访问后台页面。

代码示例 (router/index.js):

复制代码
import router from './router'; // 你的路由实例

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 1. 判断该页面是否需要登录权限 (比如在路由配置里加了 meta: { requireAuth: true })
  // 或者简单粗暴:只要不是去登录页,都检查
  if (to.path === '/login') {
    next(); // 如果是去登录页,直接放行
  } else {
    // 2. 检查是否有 Token
    const token = localStorage.getItem('token');
    
    if (token) {
      next(); // 有 Token,放行
    } else {
      // 3. 没有 Token,强制跳转登录页
      next('/login');
    }
  }
});

5. 注销 (Logout)

用户点击退出登录时,清理工作很简单。

复制代码
function logout() {
  // 1. 清除本地 Token
  localStorage.removeItem('token');
  
  // 2. 跳转回登录页
  router.push('/login');
}

总结前端 3 步走:

  1. :登录成功后,把 Token 扔进 localStorage

  2. :用 Axios 拦截器,每次发请求自动把 Token 从 localStorage 拿出来放到 Header 里。

  3. :遇到 401 错误或用户点退出,把 Token 删掉并踢回登录页。

简单来说,Session 是"服务端记账" ,而 JWT 是"客户端自带证明"

为了让你更直观地理解,我用一个生活中的例子来打比方,然后进行详细的技术对比。


session和jwt(token)形式的区别

1. 通俗比喻:健身房会员卡

  • Session (服务端存储)

    • 你办了一张只有卡号的会员卡。

    • 每次你去健身房,前台刷一下卡号,然后去电脑系统里查:"这个卡号是谁?有没有过期?是不是VIP?"

    • 关键点:数据都在健身房的电脑里(服务端),卡上只有个ID。

  • JWT (Token 存储)

    • 你办了一张印着所有信息的工牌(并且有防伪印章)。

    • 卡面上直接写着:"姓名:张三,过期时间:2026年,级别:VIP"。

    • 前台看一眼卡面,验证一下防伪印章(签名)是对的,就直接放行,完全不需要查电脑

    • 关键点:数据都在你的卡上(客户端),健身房只要确认卡是真的就行。


2. 核心区别对比表

维度 Session 实现 JWT (Token) 实现
存储位置 (信息本体) 服务端 (内存、数据库或 Redis) 客户端 (存储在 Token 字符串内部)
客户端持有 仅持有一个 SessionId (通常在 Cookie 中) 持有包含完整数据的 Token 字符串
有状态/无状态 有状态 (Stateful) 服务端必须记住你登录过 无状态 (Stateless) 服务端不存数据,只负责解密验证
扩展性 (集群) 多台服务器需要做 Session 共享 (如存入 Redis) 任意一台服务器只要有"秘钥"就能验证,互不依赖
安全性 (注销/封号) 服务端删掉 Session,用户立马下线 Token 一旦签发,在过期前一直有效,无法强制失效 (除非加黑名单)
数据载荷 想存多少存多少,不影响传输 存多了 Token 会变得很长,导致网络传输变慢
跨域支持 麻烦 (Cookie 有跨域限制) 简单 (放在 HTTP Header 中,无视跨域)

3. 详细深度解析

(1) 存储方式与服务器压力
  • Session : 用户登录后,服务端会在内存(或 Redis)里开辟一块空间存用户信息 User={name: "admin", role: "vip"},生成一个 ID 给前端。

    • 缺点:如果 100 万人在线,服务端内存压力巨大。
  • JWT : 服务端把用户信息 JSON 对象加密签名,生成一串字符扔给前端,服务端自己不留底

    • 优点:服务端省内存,来一亿个用户也不怕,反正数据都在用户手机/电脑里。
(2) 集群扩展性 (最痛的点)

假设你有两台服务器 A 和 B:

  • Session : 你在 A 登录了,Session 存在 A 的内存里。下次请求被负载均衡分发到了 B,B 发现内存里没你的 Session,会让你重新登录

    • 解决办法:必须引入 Redis 做 Session 共享,增加了架构复杂度。
  • JWT : 你在 A 登录拿到 Token。下次请求发给 B,B 只需要用同样的"秘钥"解密 Token,发现签名正确,直接放行。天生支持集群

(3) 强制下线 (JWT 的死穴)
  • Session: 管理员想封号,直接在服务端把这个 SessionId 删掉,用户下一秒就无法访问了。

  • JWT: 最大的问题是**"覆水难收"**。一旦 Token 发出去,只要没过期,它就是有效的。哪怕你把用户在数据库里删了,在 Token 过期前的这几分钟/几小时内,他依然能凭借手里的 Token 访问接口。

    • 解决办法:必须引入 Redis 做"黑名单"机制(但这又变回类似 Session 的逻辑了)。

4. 你的前端项目该选哪个?

结合你之前的 Spring Boot + Vue 代码:

推荐使用 JWT,原因如下:

  1. 前后端分离标准:Vue 和 Spring Boot 通常不在同一个域下(跨域),Session+Cookie 处理跨域很麻烦,而 JWT 放在 Header 里非常方便。

  2. 移动端兼容:如果以后要开发 App 或小程序,它们不支持 Cookie,但支持 Header 传 Token。

  3. 学习成本:Spring Boot 整合 JWT (jjwt) 非常成熟,正如我之前给你的代码那样,一个拦截器就搞定了。

总结

  • 用 Session:如果是传统的单体项目(JSP/Thymeleaf 直接在服务端渲染),或者对安全性要求极高(如银行系统,需要秒级踢人下线)。

  • 用 JWT :如果是前后端分离 (Vue/React)、需要开发移动端、或者需要扛高并发集群。这也是目前互联网公司的主流选择。

面试题:当token过期了 该怎么进行补发

当 JWT(Access Token)过期时,我们不能修改 它(因为它是不可变的),而是必须签发一个新的 Token 给前端。

为了不让用户频繁重新登录(比如 Token 30分钟过期,用户正在填表单突然跳回登录页体验很差),业界通用的标准做法是使用 双 Token 机制 (Access Token + Refresh Token)


1. 核心原理:双 Token 机制

我们需要两个 Token:

  1. Access Token (短命)

    • 有效期:短 (例如 30 分钟)。

    • 作用:用于请求业务接口(如获取用户列表)。

    • 存放:前端请求头。

  2. Refresh Token (长命)

    • 有效期:长 (例如 7 天)。

    • 作用仅用于 当 Access Token 过期时,向后端换取一个新的 Access Token。

    • 存放:安全存储 (Local Storage 或 HttpOnly Cookie)。


2. 后端实现 (Spring Boot)

第一步:修改登录接口,返回两个 Token

JwtUtils 中,你需要支持生成不同过期时间的 Token。或者简单调用两次生成方法,参数不同。

修改 LoginController

java 复制代码
@PostMapping("/login")
public Map<String, Object> login(@RequestBody User user) {
    // ... 账号密码校验通过 ...

    Map<String, Object> claims = new HashMap<>();
    claims.put("username", user.getUsername());

    // 1. 生成 Access Token (30分钟过期)
    String accessToken = JwtUtils.createToken(claims, 30 * 60 * 1000L);

    // 2. 生成 Refresh Token (7天过期) - 这里载荷可以只存用户ID,越少越好
    String refreshToken = JwtUtils.createToken(claims, 7 * 24 * 60 * 60 * 1000L);

    // 3. (可选但推荐) 将 refreshToken 存入 Redis,key为 username,以便服务端能强制注销
    // redisTemplate.opsForValue().set("refresh:" + user.getUsername(), refreshToken, 7, TimeUnit.DAYS);

    Map<String, Object> result = new HashMap<>();
    result.put("token", accessToken);       // 短
    result.put("refreshToken", refreshToken); // 长
    return result;
}
第二步:编写"刷新 Token"的接口

前端发现 401 过期后,会带着 refreshToken 来访问这个接口。

java 复制代码
@PostMapping("/refresh")
public Map<String, Object> refreshToken(@RequestBody Map<String, String> request) {
    String refreshToken = request.get("refreshToken");
    Map<String, Object> result = new HashMap<>();

    try {
        // 1. 校验 Refresh Token 是否合法/过期
        Claims claims = JwtUtils.parseToken(refreshToken);
        
        // 2. (如果有Redis) 检查 Redis 里该用户的 refresh token 是否还存在
        // String storedToken = redisTemplate.opsForValue().get("refresh:" + claims.getSubject());
        // if (storedToken == null) throw new RuntimeException("已注销");

        // 3. 校验通过,生成一个新的 Access Token
        String newAccessToken = JwtUtils.createToken(claims, 30 * 60 * 1000L);

        result.put("code", 200);
        result.put("token", newAccessToken); // 只返回新的 Access Token
        return result;

    } catch (Exception e) {
        // Refresh Token 也过期了,没救了,强制重新登录
        result.put("code", 401);
        result.put("msg", "登录凭证已失效,请重新登录");
        return result;
    }
}

3. 前端实现 (Vue + Axios)

前端最关键的是利用 Axios 响应拦截器 实现"无感刷新"。

流程 : 请求业务接口 -> 报 401 -> 拦截器暂停请求 -> 用 RefreshToken 换新 Token -> 重试原请求

修改 request.js (Axios 配置):

javascript 复制代码
import axios from 'axios'
import router from '@/router'

const service = axios.create({ baseURL: 'http://localhost:8080' })

// 防止并发刷新(如果有多个请求同时 401,只刷新一次)
let isRefreshing = false;
// 存储因为等待刷新而挂起的请求
let requests = [];

service.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // 如果后端返回 401 (未授权/过期) 且不是请求刷新接口本身报错
    if (error.response?.status === 401 && !originalRequest._retry) {
      
      // 1. 如果已经在刷新了,就把当前请求挂起,放入队列
      if (isRefreshing) {
        return new Promise((resolve) => {
          requests.push((newToken) => {
            originalRequest.headers['token'] = newToken;
            resolve(service(originalRequest));
          });
        });
      }

      originalRequest._retry = true; // 标记:这个请求已经重试过一次了,防止死循环
      isRefreshing = true;

      try {
        // 2. 发起刷新 Token 请求
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
           throw new Error("没有 Refresh Token");
        }

        const res = await axios.post('http://localhost:8080/refresh', {
            refreshToken: refreshToken
        });

        if (res.data.code === 200) {
          // 3. 刷新成功:保存新 Token
          const newToken = res.data.token;
          localStorage.setItem('token', newToken);
          
          // 4. 修改默认 Header,供后续使用
          service.defaults.headers.common['token'] = newToken;
          originalRequest.headers['token'] = newToken;

          // 5. 执行队列中挂起的请求
          requests.forEach(cb => cb(newToken));
          requests = [];
          
          // 6. 重试当前请求
          return service(originalRequest);
        } else {
            // 后端说 Refresh Token 也失效了
            throw new Error("刷新失败");
        }
      } catch (refreshErr) {
        // 7. 最坏情况:彻底凉凉,清除数据,跳转登录页
        localStorage.clear();
        router.push('/login');
        return Promise.reject(refreshErr);
      } finally {
        isRefreshing = false;
      }
    }
    return Promise.reject(error);
  }
);

4. 总结

  1. 登录时 :后端给 Token (30分) 和 RefreshToken (7天)。

  2. 平时请求 :前端 Header 带 Token

  3. Token 过期

    • 请求失败 (401)。

    • 前端拦截器捕获 401。

    • 前端自动调用 /refresh 接口,发送 RefreshToken

    • 后端验证通过,返回新 Token

    • 前端拿到新 Token,重发刚才失败的请求。

    • 用户完全感觉不到发生了一次 401,这就叫无感刷新

  4. RefreshToken 过期 :如果是 7 天后,/refresh 接口也会报错,此时前端拦截器会跳转到 /login 让用户重新登录。

相关推荐
xzl042 小时前
小智服务端chat入口工具调用流程
java·服务器·前端
CTO Plus技术服务中2 小时前
2026版Java web高并发面试题和参考答案
java·jvm·spring·spring cloud·面试·tomcat·java-consul
2301_803554522 小时前
Qt中connect()实现信号与槽连接这一核心机制
java·数据库·qt
Just right2 小时前
python安装包问题
开发语言·python
峥嵘life2 小时前
Android16 EDLA【CTS】CtsNetTestCases存在fail项
android·java·linux·学习·elasticsearch
dxz_tust2 小时前
flow match简单直观理解
开发语言·python·深度学习·扩散模型·流匹配·flow match
写代码的【黑咖啡】2 小时前
Python 中的时间序列特征自动提取工具:tsfresh
开发语言·python
Frank学习路上2 小时前
【Qt】问题记录ld: framework ‘AGL‘ not found on MacOS 26
开发语言·qt·macos
陳10302 小时前
C++:二叉搜索树
开发语言·数据结构·c++