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 |
用户登录页面 |