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/statNo 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
作者
Bruce
发布于
2025年03月26日
许可协议