Mall 商城项目 — 白名单访问控制 & Token 携带与持久化实现详解

Mall 商城项目 — 白名单访问控制 & Token 携带与持久化实现详解

一、整体架构概览

┌─────────────────────────────────────────────────────────────┐
│                    前端 (Vue 3 + Pinia)                      │
│  ┌──────────┐  ┌──────────────┐  ┌─────────────────────┐   │
│  │ 登录页面  │  │ Axios 拦截器  │  │ Router 导航守卫     │   │
│  │          │  │ (请求/响应)   │  │ (token 存在性判断)   │   │
│  └────┬─────┘  └──────┬───────┘  └──────────┬──────────┘   │
│       │               │                     │               │
│       │     ┌─────────▼──────────┐           │               │
│       │     │  Pinia Store       │           │               │
│       │     │  (token/user)      │           │               │
│       │     │  persist:          │           │               │
│       │     │  sessionStorage    │           │               │
│       │     └────────────────────┘           │               │
└───────┼───────────────┼─────────────────────┼───────────────┘
        │               │                     │
        ▼               ▼                     │
┌──────────────────────────────────────────────────────────────┐
│                  后端 (Spring Boot 微服务)                     │
│                                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │              JwtInterceptor (统一拦截器)                │    │
│  │  1. 白名单检查 → 放行                                  │    │
│  │  2. OPTIONS 请求 → 放行 (跨域预检)                      │    │
│  │  3. Token 验证 → 解析 JWT                              │    │
│  │  4. Token 刷新 → 每次请求生成新 Token,放入响应头         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │ goods    │  │ admin    │  │ order    │  │ user     │    │
│  │ WebConfig│  │ WebConfig│  │ WebConfig│  │ (无拦截器)│    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
└──────────────────────────────────────────────────────────────┘

二、白名单(White List)机制详解

白名单机制是本项目访问控制的第一道防线,它决定了哪些请求不需要携带 Token 就能直接访问。

2.1 白名单配置的数据模型

文件:common/src/main/java/org/example/mall/common/bean/Rule.java

@Data
@ConfigurationProperties
@NoArgsConstructor
public class Rule {
    private String regpath;   // 匹配 URL 的正则表达式
    private String method;    // HTTP 请求方法(GET / POST / PUT / DELETE)
}

设计要点:每条白名单规则是一个 (正则路径, HTTP方法) 的二元组。这意味着白名单不仅匹配 URL,还精确到请求方法——同一个路径,GET 可能放行,POST 则可能不放行。这比简单的路径排除更精细。

2.2 白名单配置的加载

文件:common/src/main/java/org/example/mall/common/config/WhiteListConfig.java

@Data
@Configuration
@ConfigurationProperties(prefix = "white-list")   // ← 绑定 application.yml 中 white-list 开头的配置
public class WhiteListConfig {
    private List<Rule> rules;   // 自动注入 rules 列表
}

工作原理:Spring Boot 启动时,@ConfigurationProperties(prefix = "white-list") 会自动读取 application.yml 中的 white-list.rules 数组,将每一条序列化为 Rule 对象,最终注入到 WhiteListConfig 的 rules 字段中。

2.3 白名单的具体配置(以 goods_service 为例)

文件:goods_service/src/main/resources/application.yml

white-list:
  rules:
    - regpath: ^/goods.*$        # 正则:匹配所有 /goods 开头的路径
      method: GET                # 方法:仅限 GET 请求
    - regpath: ^/category/[0-9]+$ # 正则:匹配 /category/数字 的路径
      method: GET                # 方法:仅限 GET 请求

业务含义:
– ^/goods.*$ + GET:查看商品不需要登录——任何人都可以浏览商品
– ^/category/[0-9]+$ + GET:查看分类不需要登录——比如 /category/123

但修改商品(POST/PUT)和删除商品(DELETE)不在白名单中,必须携带有效 Token。

2.4 白名单的匹配逻辑(核心)

文件:common/src/main/java/org/example/mall/common/intercepter/JwtInterceptor.java(第 30-33 行)

// 判断请求是否在白名单中 - 放行
if (whiteListConfig.getRules() != null && whiteListConfig.getRules()
        .stream()
        .anyMatch(rule -> rule.getMethod().equalsIgnoreCase(menthod)   // ① 方法名匹配
                        && path.matches(rule.getRegpath()))) {          // ② 正则路径匹配
    return true;   // ③ 直接放行,不检查 Token
}

执行逻辑:
1. 遍历所有白名单规则
2. anyMatch:只要任意一条规则同时满足「方法一致」+「路径匹配正则」,即为命中
3. 命中白名单 → 返回 true,直接放行,后续 Token 校验全部跳过
4. 未命中 → 继续执行 Token 校验逻辑

2.5 OPTIONS 请求的特殊处理

// 对 OPTIONS 请求放行,不然会出现跨域问题
if ("OPTIONS".equals(request.getMethod().toUpperCase())) {
    return true;
}

原因:浏览器的跨域预检请求(CORS Preflight)使用 OPTIONS 方法,它通常不携带自定义 Header(如 token)。如果不放行 OPTIONS,浏览器在正式请求前就会因预检失败而报跨域错误。


三、JWT Token 的生成与解析

3.1 JwtUtil 工具类

文件:common/src/main/java/org/example/mall/common/utils/JwtUtil.java

public class JwtUtil {
    private static String secret = "q#@#!(=90123";  // ① 签名密钥(硬编码)

    // ========== 生成 JWT ==========
    public static String generateJwt(Map<String, Object> map) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 30 * 60);  // ② 过期时间:30 分钟

        String token = JWT.create()
                .withClaim("payload", map)                 // ③ payload 中存储用户信息
                .withExpiresAt(calendar.getTime())          // ④ 设置过期时间
                .sign(Algorithm.HMAC256(secret));           // ⑤ HMAC256 签名
        return token;
    }

    // ========== 解析 JWT ==========
    public static Map<String, Object> parseJwtToMap(String token)
            throws SignatureVerificationException, TokenExpiredException,
                   AlgorithmMismatchException {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
        DecodedJWT verify = jwtVerifier.verify(token);
        return verify.getClaim("payload").asMap();          // ⑥ 取出 payload 中的 Map
    }
}

关键设计决策:
| 决策 | 取值 | 影响 |
|——|——|——|
| 签名算法 | HMAC256 | 对称加密,服务端只需一个密钥即可签发和验证 |
| 密钥存储 | 硬编码 "q#@#!(=90123" | 简单但不安全,生产环境应使用环境变量或配置中心 |
| 过期时间 | 30 分钟 | 安全性与用户体验的折中 |
| Payload 结构 | Map<String, Object> → 嵌套在 "payload" claim 中 | 灵活性高,可存储任意用户属性 |

3.2 JWT Payload 的内容(登录时写入)

文件:admin_service/.../controller/AdminController.java(第 62-67 行) 和 user_service/.../controller/UserController.java(第 64-68 行)

// 管理员登录
HashMap<String, Object> map = new HashMap<>();
map.put("id", admin.getId());           // 用户 ID
map.put("username", admin.getUsername()); // 用户名
map.put("role", "admin");               // 角色:admin
String jwtStr = JwtUtil.generateJwt(map);
// 用户登录
HashMap<String, Object> map = new HashMap<>();
map.put("id", user.getId());
map.put("username", user.getUsername());
map.put("role", "admin");   // 注:这里角色固定为 admin,实际应为 "user"
String jwtStr = JwtUtil.generateJwt(map);

Token 内部结构示意(解码后):

> {
>   "payload": {
>     "id": 1,
>     "username": "dxk",
>     "role": "admin"
>   },
>   "exp": 1690001234
> }
> 

四、拦截器(JwtInterceptor)—— 白名单 + Token 校验 + 自动续期

这是整个认证体系的核心枢纽,代码集中在 JwtInterceptor.java 的 preHandle 方法中。

4.1 完整流程图

请求到达
    │
    ▼
┌─────────────────┐
│ 1. 获取 path +   │
│    method        │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    命中白名单
│ 2. 白名单匹配?  │──────────────► return true (放行)
└────────┬────────┘
         │ 未命中
         ▼
┌─────────────────┐    是 OPTIONS
│ 3. OPTIONS 请求?│──────────────► return true (放行)
└────────┬────────┘
         │ 不是
         ▼
┌─────────────────┐
│ 4. 从 Header 取   │
│    token 字段     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    解析成功
│ 5. parseJwtToMap │──────────────► 生成新 Token → 放入响应头
│    验证签名+过期  │                  return true (放行)
└────────┬────────┘
         │ 解析失败
         ▼
┌─────────────────┐
│ 6. 捕获异常类型   │
│  Signature       │──► "无效签名"
│  TokenExpired    │──► "令牌超时"
│  Algorithm       │──► "算法不匹配"
│  其他            │──► "令牌无效"
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 7. 返回 403      │
│    JSON 错误信息  │
│    return false  │
└─────────────────┘

4.2 代码逐段解析

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                         Object handler) throws Exception {
    // ===== 第一步:获取请求路径和方法 =====
    String path = request.getRequestURI();
    String menthod = request.getMethod();  // 注意:变量名拼写为 menthod(应为 method)

    // ===== 第二步:白名单判断 =====
    if (whiteListConfig.getRules() != null && whiteListConfig.getRules()
            .stream()
            .anyMatch(rule -> rule.getMethod().equalsIgnoreCase(menthod)
                    && path.matches(rule.getRegpath()))) {
        return true;  // 白名单命中 → 放行
    }

    // ===== 第三步:OPTIONS 跨域预检放行 =====
    if ("OPTIONS".equals(request.getMethod().toUpperCase())) {
        return true;
    }

    // ===== 第四步:获取 Token =====
    String token = request.getHeader("token");  // 从请求头 "token" 字段获取
    RespBean respBean = null;

    try {
        // ===== 第五步:解析 JWT =====
        Map<String, Object> map = JwtUtil.parseJwtToMap(token);

        // ===== 第六步:生成新 JWT(自动续期)=====
        String jwt = JwtUtil.generateJwt(map);           // 用旧 payload 生成新 Token

        // ===== 第七步:将新 Token 放入响应头 =====
        response.setHeader("token", jwt);
        response.setHeader("Access-Control-Expose-Headers", "token");
        // ↑ 这行关键:让浏览器能读到自定义响应头 "token"

        return true;  // 放行

    } catch (SignatureVerificationException e) {
        respBean = RespBean.error("无效签名");
    } catch (TokenExpiredException e) {
        respBean = RespBean.error("令牌超时");
    } catch (AlgorithmMismatchException e) {
        respBean = RespBean.error("算法不匹配");
    } catch (Exception e) {
        respBean = RespBean.error("令牌无效");
    }

    // ===== 第八步:校验失败 → 返回 403 JSON 响应 =====
    ObjectMapper objectMapper = new ObjectMapper();
    response.setStatus(HttpServletResponse.SC_FORBIDDEN);        // HTTP 403
    response.setContentType("application/json;charset=utf-8");
    objectMapper.writeValue(response.getOutputStream(), respBean); // 写入 JSON 错误体

    return false;  // 拦截请求
}

4.3 核心设计:Token 自动续期(滑动过期)

// 每次校验成功,都生成一个新 Token
String jwt = JwtUtil.generateJwt(map);
response.setHeader("token", jwt);

这是本项目最关键的设计之一:
– 不是被动等待 Token 过期让用户重新登录
– 而是每次请求只要 Token 有效,就签发一个新的 30 分钟 Token
– 效果等价于「只要用户持续操作,Token 永不过期」(滑动过期策略)
– 用户连续 30 分钟无操作后,Token 才真正过期

4.4 Access-Control-Expose-Headers 的作用

response.setHeader("Access-Control-Expose-Headers", "token");

为什么需要这行?

在跨域请求中,浏览器默认只允许 JavaScript 读取以下响应头:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma

token 是自定义响应头,必须显式暴露,否则前端 resp.headers.token 将始终为 undefined,无法获取后端下发的新 Token。


五、各微服务的拦截器配置(WebConfig)

每个微服务通过 WebMvcConfigurer.addInterceptors() 决定拦截哪些路径以及排除哪些路径。

5.1 Admin 服务

文件:admin_service/.../config/WebConfig.java

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(jwtInterceptor)
            .addPathPatterns("/admin/**")          // 拦截所有 /admin 开头的请求
            .excludePathPatterns(
                "/admin/captcha",                   // 排除:获取验证码(登录前需要)
                "/admin/login"                      // 排除:登录接口(登录前需要)
            );
}

业务逻辑:管理员后台中,只有验证码和登录两个接口不需要登录,其余所有操作(查看用户、管理商品、修改密码等)都需要 Token。

5.2 Goods 服务

文件:goods_service/.../config/WebConfig.java

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(jwtInterceptor)
            .addPathPatterns("/goods/**", "/category/**")  // 拦截商品和分类的所有请求
            .excludePathPatterns(
                "/category/allParent",    // 排除:获取所有父分类(首页展示用)
                "/category/pic/**",       // 排除:分类图片(静态资源)
                "/category/search",       // 排除:分类搜索(前台浏览用)
                "/goods/pic/**",          // 排除:商品图片(静态资源)
                "/goods/search"           // 排除:商品搜索(前台浏览用)
            );
}

注意:这里的 excludePathPatterns 是固定路径排除——不依赖 Token 即可访问。而 application.yml 中的 white-list 则是正则匹配 + HTTP 方法维度的白名单——两者是互补关系。

5.3 Order 服务

文件:order_service/.../config/WebConfig.java

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(jwtInterceptor)
            .addPathPatterns("/cart/**", "/order/**", "/addr/**")  // 购物车、订单、地址
            .excludePathPatterns(
                "/order/notify",   // 排除:支付宝异步通知(外部回调,无法带 Token)
                "/order/return"    // 排除:支付宝同步回跳(外部回调,无法带 Token)
            );
}

关键排除:支付宝的支付结果通知是支付宝服务器主动回调,不可能携带我们系统的 Token,必须排除在拦截之外。

5.4 User 服务

user_service 没有 WebConfig,意味着它对所有请求不做拦截(登录、注册、获取信息等接口本身就需要在未登录状态下访问)。但文件上传等需要认证的操作,会在 Controller 方法中通过 @RequestHeader("token") 手动获取并验证。


六、前端 Token 携带机制

6.1 Axios 实例创建

文件:front/src/api/index.js

import axios from "axios";
import { userTokenStore } from "@/stores/token.js";

// 创建统一的 Axios 实例
const service = axios.create({
    baseURL: import.meta.env.VITE_SERVER_ADDR  // 从环境变量读取后端地址
})

6.2 请求拦截器 —— Token 携带

// ========== 请求拦截器 ==========
service.interceptors.request.use(req => {
    // 只要有 token 就携带
    const tokenStore = userTokenStore();
    if (tokenStore.tokenStr) {
        req.headers.token = tokenStore.tokenStr;  // 将 Token 放入请求头 "token" 字段
    }
    return req;
}, error => {})

执行逻辑:
1. 每次发出 HTTP 请求前,自动执行此函数
2. 从 Pinia Store 中读取当前 Token
3. 如果 Token 存在,写入请求头 token 字段
4. Token 不存在(如未登录),请求头中就没有 token 字段

这意味着前端不需要手动在每个 API 调用中加 Token,一切由拦截器自动完成。

6.3 响应拦截器 —— Token 自动更新

// ========== 响应拦截器 ==========
service.interceptors.response.use(resp => {
    // 从响应头中获取新 Token
    const token = resp.headers.token;
    if (token) {
        const tokenStore = userTokenStore();
        tokenStore.updateToken(token);     // 更新 Pinia 中的 Token
    }
    return resp.data;                      // 只返回 data,简化调用方代码
}, error => {
    // ===== 403 错误处理 =====
    if (error.status === 403) {
        ElMessage.error({
            message: "令牌错误,重新登录",
            duration: 1200,
            onClose: () => {
                const tokenStore = userTokenStore();
                tokenStore.$reset();       // 清空 Pinia 中的 Token

                // 根据当前路径判断跳转到哪个登录页
                let currentPath = router.currentRoute.value.path;
                if (currentPath.startsWith("/admin")) {
                    router.push("/admin/login");   // 后台登录页
                } else {
                    router.push("/user/login");    // 前台登录页
                }
            }
        })
    }
})

核心工作流程:

> 后端返回响应
>     │
>     ├── 响应头中有 token?
>     │   ├── 有 → 更新 Pinia Store 中的 Token(自动续期)
>     │   └── 没有 → 不做处理
>     │
>     ├── HTTP 状态码是 403?
>     │   ├── 是 → 清空 Token → 跳转登录页(强制重新登录)
>     │   └── 不是 → 不做处理
>     │
>     └── 返回 resp.data(业务层只关心数据)
> 

七、前端 Token 持久化(Pinia + sessionStorage)

7.1 Token Store

文件:front/src/stores/token.js

import { defineStore } from 'pinia'

export const userTokenStore = defineStore('token', () => {
    const token = ref(null)                          // ① 响应式状态:Token
    const tokenStr = computed(() => token.value)     // ② 计算属性:对外暴露 Token 值

    function updateToken(newToken) {                 // ③ 更新 Token
        token.value = newToken;
    }

    function $reset() {                              // ④ 重置(清空)Token
        token.value = null;
    }

    return { token, tokenStr, updateToken, $reset }
}, {
    persist: {
        key: 'token',              // ⑤ 持久化 key 名
        storage: sessionStorage    // ⑥ 存储介质:sessionStorage
    }
})

7.2 User Store(用户信息持久化)

文件:front/src/stores/user.js

export const userToken = defineStore('user', () => {
    const user = ref(null)
    const userInfo = computed(() => user.value)

    function updateUser(u) {
        user.value = u;
    }

    function $reset() {
        user.value = null;
    }

    return { user, userInfo, updateUser, $reset }
}, {
    persist: {
        key: 'user',
        storage: sessionStorage
    }
})

7.3 为什么用 sessionStorage 而不是 localStorage?

存储方式 生命周期 安全性 本项目选择
sessionStorage 关闭标签页即清除 较高(不会持久化到磁盘) ✅ 选用
localStorage 永久存储,除非手动清除 较低(持久化,易被 XSS 读取) ❌ 未选用
Cookie 可设置过期时间 中等(可设 HttpOnly) ❌ 未选用

选择理由:sessionStorage 在用户关闭浏览器标签页后自动清除,降低了 Token 被长期窃取的风险。同时 Pinia 的 persist 插件实现了自动同步——Store 中的值变化时自动写入 sessionStorage,页面刷新后自动从 sessionStorage 恢复。


八、前端路由守卫 —— 基于 Token 的页面访问控制

文件:front/src/router/index.js(第 136-170 行)

router.beforeEach((to, from) => {
    // ===== 第一步:白名单页面 → 直接放行 =====
    if (to.path === '/admin/login'       // 后台登录页
        || to.path == '/'                // 首页
        || to.path === '/user/login'     // 前台登录页
        || to.path === '/user/index'     // 前台首页
        || to.path.startsWith('/user/search')  // 搜索页(支持 /user/search/:categoryId?)
        || to.path === '/user/reg'       // 注册页
        || to.path === '/user/goods'     // 商品详情页
        || to.path === '/user/center') { // 用户中心页
        return true;
    }

    // ===== 第二步:非白名单页面 → 检查 Token =====
    const tokenStore = new userTokenStore();
    if (tokenStore.tokenStr) {
        return true;                     // 有 Token → 放行
    } else {
        // 无 Token → 根据目标路径判断跳转到哪个登录页
        if (to.path.startsWith("/admin")) {
            return "/admin/login";       // 跳转后台登录页
        } else {
            return "/user/login";        // 跳转前台登录页
        }
    }
})

路由守卫 vs 后端拦截器:

| 层面 | 作用 | 能防止什么 |
|——|——|———–|
| 前端路由守卫 | 页面跳转前检查 Token 是否存在 | 未登录用户看到需要登录的页面(UX 防护) |
| 后端拦截器 | API 请求时验证 Token 有效性 | 伪造的 API 请求(安全防护) |

两者是互补关系,不是替代关系。路由守卫提供用户体验保护,后端拦截器提供真正的安全保护。


九、完整认证流程(端到端)

9.1 登录流程

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  浏览器    │     │  前端     │     │  后端     │     │  Redis   │
│           │     │  Vue     │     │  Spring  │     │          │
└─────┬─────┘     └─────┬─────┘     └─────┬─────┘     └─────┬─────┘
      │                 │                 │                   │
      │  1. 访问登录页   │                 │                   │
      │────────────────►│                 │                   │
      │                 │                 │                   │
      │                 │  2. GET /admin/captcha              │
      │                 │────────────────►│                   │
      │                 │                 │  3. 生成验证码     │
      │                 │                 │  4. 存 Redis       │
      │                 │                 │──────────────────►│
      │                 │  5. {key, base64图片}               │
      │                 │◄────────────────│                   │
      │                 │                 │                   │
      │  6. 用户填表提交  │                 │                   │
      │────────────────►│                 │                   │
      │                 │                 │                   │
      │                 │  7. POST /admin/login               │
      │                 │   {username, password,              │
      │                 │    key, captchaInput}               │
      │                 │────────────────►│                   │
      │                 │                 │  8. 从 Redis 取验证码
      │                 │                 │◄──────────────────│
      │                 │                 │  9. 校验验证码      │
      │                 │                 │ 10. 校验用户名密码   │
      │                 │                 │ 11. 生成 JWT       │
      │                 │                 │     (payload:      │
      │                 │                 │      id, username, │
      │                 │                 │      role)         │
      │                 │                 │                   │
      │                 │ 12. RespBean{code:200, data: jwtStr}│
      │                 │◄────────────────│                   │
      │                 │                 │                   │
      │                 │ 13. Pinia Store │                   │
      │                 │     保存 Token  │                   │
      │                 │     ↓           │                   │
      │                 │ 14. sessionStorage.set('token', jwt)│
      │                 │                 │                   │
      │                 │ 15. GET /admin/info (带 token)      │
      │                 │────────────────►│                   │
      │                 │ 16. 返回用户信息  │                   │
      │                 │◄────────────────│                   │
      │                 │                 │                   │
      │                 │ 17. Pinia Store │                   │
      │                 │     保存 User   │                   │
      │                 │     ↓           │                   │
      │                 │ 18. sessionStorage.set('user', ...) │
      │                 │                 │                   │
      │ 19. 跳转首页    │                 │                   │
      │◄────────────────│                 │                   │

9.2 后续请求的 Token 自动流转

┌──────────┐          ┌──────────┐          ┌──────────┐
│  Axios    │          │  后端     │          │  Axios    │
│  请求拦截器│          │  JWT拦截器│          │  响应拦截器│
└─────┬─────┘          └─────┬─────┘          └─────┬─────┘
      │                      │                      │
      │ 1. 从 Pinia 取 Token  │                      │
      │ 2. 放入请求头 token    │                      │
      │                      │                      │
      │ ──── 带 token 的请求 ──►                      │
      │                      │                      │
      │                      │ 3. 白名单检查          │
      │                      │ 4. 验证 JWT 签名       │
      │                      │ 5. 解析 payload        │
      │                      │ 6. 生成新 Token        │
      │                      │    (自动续期 30 分钟)   │
      │                      │                      │
      │ ◄─── 响应头含新 token ────                    │
      │                      │                      │
      │                      │             7. 读取响应头 token
      │                      │             8. 更新 Pinia Store
      │                      │             9. sessionStorage 自动同步
      │                      │                      │

9.3 Token 过期处理流程

┌──────────┐          ┌──────────┐          ┌──────────┐
│  Axios    │          │  后端     │          │  浏览器    │
│  响应拦截器│          │  拦截器   │          │           │
└─────┬─────┘          └─────┬─────┘          └─────┬─────┘
      │                      │                      │
      │                      │ Token 过期/无效       │
      │ ◄─── HTTP 403 ───────│                      │
      │                      │                      │
      │ 1. error.status=403  │                      │
      │ 2. ElMessage:        │                      │
      │    "令牌错误,重新登录"│                      │
      │ 3. tokenStore.$reset()│                     │
      │    (清空 Token)       │                      │
      │ 4. sessionStorage     │                      │
      │    中的 token 被清除   │                      │
      │                      │                      │
      │ 5. router.push()     │                      │
      │─────────────────────────────────────────────►│
      │                      │      跳转到登录页      │

十、关键设计总结

设计点 实现方式 优点 潜在风险
白名单 YAML 正则 + HTTP 方法 灵活精细,可精确控制每个接口的访问权限 正则写错可能导致意外放行
Token 生成 JWT + HMAC256 对称签名 无状态,服务端不需要存储 Token 密钥硬编码在代码中,泄露风险
Token 携带 Axios 请求拦截器自动注入 Header 对业务代码透明,无需手动传 Token 如果拦截器注册失败,所有请求都无 Token
Token 续期 每次请求验证成功后签发新 Token 用户体验好,操作中不会突然过期 增加计算开销,响应头变大
Token 持久化 Pinia persist → sessionStorage 关闭标签页自动清除,相对安全 同一标签页内的 XSS 仍可读取
路由守卫 router.beforeEach 前端层面拦截未登录用户 纯前端校验,不替代后端安全
403 处理 Axios 响应拦截器 → 清 Token → 跳转 用户体验一致,统一错误处理 需确保登录页不在拦截范围内

十一、文件索引

文件 作用
common/.../bean/Rule.java 白名单规则数据模型
common/.../config/WhiteListConfig.java 白名单配置加载类
common/.../utils/JwtUtil.java JWT 生成与解析工具类
common/.../intercepter/JwtInterceptor.java 核心:拦截器(白名单匹配 + Token 校验 + 自动续期)
common/.../bean/RespBean.java 统一响应结构体
common/.../utils/RedisUtil.java Redis 工具类(验证码存储)
admin_service/.../config/WebConfig.java Admin 微服务拦截器注册
admin_service/.../controller/AdminController.java 管理员登录/Token 生成
goods_service/.../config/WebConfig.java Goods 微服务拦截器注册
goods_service/.../application.yml 唯一配置了白名单的服务
order_service/.../config/WebConfig.java Order 微服务拦截器注册
user_service/.../controller/UserController.java 用户登录/Token 生成
front/src/api/index.js 前端核心:Axios 实例 + 请求/响应拦截器
front/src/stores/token.js Pinia Token Store + sessionStorage 持久化
front/src/stores/user.js Pinia User Store + sessionStorage 持久化
front/src/router/index.js 路由导航守卫
front/src/views/admin/LoginView.vue 管理员登录页面
front/src/views/user/LoginView.vue 用户登录页面
上一篇
下一篇