Spring Boot + Vue 项目中 JWT 鉴权失败的完整排查与改造实录
本文记录了我在毕业设计《校园社团活动管理系统》中,使用 Spring Boot + Vue + JWT 进行用户权限控制时,遇到 403 权限拒绝、SecurityContext 为空、Vue 请求失败等问题的全过程,并总结出一套标准、通用、无坑的配置方案。
背景简介
本系统采用以下技术栈:
- 后端:Spring Boot 3 + Spring Security 6 + MyBatis-Plus
- 前端:Vue 3 + Element Plus + Vite
- 权限控制方式:基于 JWT(Token)
遇到的问题
在配置好登录、鉴权、角色控制之后,我访问 /api/auth/me 等受保护接口时,依然出现了:
403 Forbidden权限不足SecurityContextHolder.getContext().getAuthentication() == null- 控制器日志未打印,未进入方法体
- Token 明明存在,但认证失败
- 登录接口
POST /api/login直接 403 - Vue 报错
No static resource dashboard/stat或No route match
最终排查出的核心问题(坑点)
| 问题点 | 描述 | 修复方式 |
|---|---|---|
| Token 成功解析,但权限未注入 | JwtAuthFilter 中设置了 authorities,但没有放入 userDetails 对象中 | userDetails.setAuthorities(...)显式注入 |
| SecurityContext 为空 | JwtAuthFilter 中抛出异常后提前 response.sendError(...) |
❌ 禁止在 Filter 中打断请求,使用 try/catch包裹,最后无条件 chain.doFilter() |
| userDetails 无权限 | 从数据库加载的 User 实例未设置 authorities字段 |
使用 @TableField(exist = false)+ 手动注入 |
| isEnabled() 返回 false | User 状态未设置,默认 null ≠ ACTIVE | JWT 注入时强制设置为 ACTIVE |
| POST /api/login 被拦截 403 | 后端接口是 /login,前端发的是 /api/login |
后端 Controller 加上 /api前缀,统一路径风格 |
| 前端访问 404/静态资源找不到 | /api请求未通过 Vite 转发 |
vite.config.js中配置 proxy,确保 /api正确转发到后端 |
Vue Router 报 No match found |
页面跳转路由未定义 | 确保 router/index.js注册了所有 /admin/xxx页面路径 |
JwtAuthFilter 最终版本
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final CustomUserDetailsService userDetailsService;
public JwtAuthFilter(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
try {
String token = getToken(request);
if (token != null && token.startsWith("\"") && token.endsWith("\"")) {
token = token.substring(1, token.length() - 1);
}
if (StringUtils.hasText(token)) {
Long userId = TokenUtil.getUserId(token);
List<String> roles = TokenUtil.getRoles(token);
List<GrantedAuthority> authorities = roles.stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.collect(Collectors.toList());
User userDetails = (User) userDetailsService.loadUserByUserId(userId);
userDetails.setAuthorities(authorities);
userDetails.setStatus(UserStatus.ACTIVE); // 避免 isEnabled 为 false
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
System.out.println("JWT 认证失败:" + e.getMessage());
// 不要中断 filter 链
}
chain.doFilter(request, response);
}
private String getToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
return (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) ? bearer.substring(7) : null;
}
}
User 实体类注意事项
@TableField(exist = false)
private List<GrantedAuthority> authorities;
并实现 UserDetails 接口中的 getAuthorities() 方法。
CustomUserDetailsService 增加支持 userId 的方法
public UserDetails loadUserByUserId(Long userId) throws UsernameNotFoundException {
User user = userMapper.selectById(userId);
if (user == null) throw new UsernameNotFoundException("用户不存在");
return user;
}
前端 Axios 实例配置(统一 baseURL)
const service = axios.create({
baseURL: '/api',
timeout: 5000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token && config.url !== '/login') {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
})
vite.config.js 代理配置
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
// ⚠️ 不要加 rewrite,保留 /api
}
}
}
})
Vue Router 配置补充
{
path: '/admin/approval',
component: () => import('@/views/admin/Approval.vue')
}
最终效果
- 携带 Token 后访问任意接口,如
/api/dashboard/stat,自动注入角色 - 登录接口:POST
/api/login SecurityContextHolder.getContext().getAuthentication()成功拿到用户对象与权限- 支持
@PreAuthorize("hasRole('COLLEGE_ADMIN')")注解进行权限判断
经验总结
- 不要在 Filter 中提前 return 或 response;
- 认证状态必须手动注入
SecurityContextHolder; - 所有接口路径统一
/api/xxx,更好做前后端联调与权限管理; - Vue 项目开发期间务必配置 Vite Proxy,否则请求打不到后端!
Spring Boot + Vue 项目中 JWT 鉴权失败的完整排查与改造实录
https://blog.waynews.top/archives/2IN4Ajtt