☰
Vue3+Spring Boot前后端分离项目实战:通信链路、权限与部署闭环
2026/10/14 1:02:49 网站建设 项目流程

简介:这是一套完整的Vue+SpringBoot前后端分离项目源码,面向Java与前端初学者及中级开发者,用于学习企业级Web应用的架构设计与技术整合。项目融合SpringBoot后端框架、Vue.js前端框架,并集成Dubbo微服务通信、Redis缓存、Druid数据库连接池等核心中间件,覆盖用户管理、接口鉴权、异步消息处理等典型业务场景,可作为课程设计、毕业项目或技术栈进阶实践范本。资源共900个文件,包含55个Java后端模块、19个Vue组件、31个JS交互逻辑、181个XML配置及4个YML启动参数文件,前端侧重路由与状态管理,后端聚焦分层架构与数据源优化,压缩包仅495KB,轻量易导入。已有6864人学习下载,源码结构清晰、注释完整,含Redis缓存策略实现、Druid监控配置、RabbitMQ消息消费示例等可直接复用的技术点,适合动手调试与二次开发。

1. Vue + Spring Boot 前后端分离项目源码:不是模板拼凑,而是通信链路、权限边界与部署闭环的真实切片

你拿到一个标着“Vue + Spring Boot 前后端分离项目源码”的压缩包,解压后发现:前端src目录里有api/和store/,后端controller下全是@RestController,pom.xml里spring-boot-starter-web和mybatis-spring-boot-starter都在,但application.yml里数据库密码是明文,vue.config.js代理配置写死了localhost:8080,登录接口返回的 token 没做HttpOnly处理——这根本不是“能跑起来就行”的教学 Demo,而是一份暴露真实工程断点的快照:它卡在「开发联调能过」和「上线前被安全扫描拦下」之间,卡在「路由守卫写了」和「后端 RBAC 权限校验没联动」之间,卡在「跨域用 proxy 解决了」和「生产环境 Nginx 反向代理漏配/api前缀」之间。这份源码的价值,不在于教你npm run serve或mvn spring-boot:run,而在于让你看清:当 Vue 的axios发出第 17 个请求时,Spring Boot 的FilterChain正在第 3 层过滤器里决定放行还是拦截;当用户点击「导出 Excel」按钮,前端传参是exportType=pdf却调用了/api/v1/export/excel接口——这种错位,才是真实项目里最常翻车的现场。适合刚从单体 MVC 转型、正在搭建第一个独立部署项目的开发者,也适合需要快速验证某类集成方案(如 JWT 续签、文件上传进度反馈、WebSocket 实时通知)的熟手。别把它当玩具,要当黑匣子来拆。

2. 从零还原:用这套源码跑通本地开发闭环的最小可行路径

2.1 环境准备与目录结构对齐:先确认你拿到的是“可执行切片”,不是“半成品草稿”

拿到源码包后,第一件事不是npm install,而是打开根目录下的README.md(如果存在)和package.json+pom.xml对比版本号。常见陷阱是:前端用 Vue 3 + Composition API,但main.js里还写着new Vue({});后端用 Spring Boot 3.x,pom.xml却依赖spring-boot-starter-thymeleaf(说明作者可能混入了服务端渲染逻辑)。我们以典型结构为例(这也是该源码包最可能采用的):

project-root/ ├── backend/ # Spring Boot 工程(Maven module) │ ├── pom.xml │ └── src/main/java/com/example/demo/ │ ├── DemoApplication.java │ ├── controller/ # @RestController,返回 JSON │ ├── service/ │ ├── mapper/ # MyBatis XML 或注解 │ └── config/ # WebMvcConfigurer、SecurityConfig、CorsConfig ├── frontend/ # Vue 3 + Vite 工程(注意:不是 Vue CLI!) │ ├── package.json │ ├── vite.config.ts # 关键!代理配置在此,非 vue.config.js │ └── src/ │ ├── api/ # 封装 axios 实例 + 请求拦截器 │ ├── router/ # 路由守卫(判断登录态、权限码) │ └── store/ # Pinia,管理用户信息、菜单树 └── README.md

提示:若源码包里frontend下是vue.config.js,说明它是 Vue CLI 项目(Vue 2 或旧版 Vue 3),需确认backend是否兼容;若为vite.config.ts,则默认是 Vue 3 + Vite,构建更快,但proxy配置语法不同——这是第一个必须对齐的点。

验证方式:进入backend目录,执行:

mvn clean compile -q && echo "✅ 后端编译通过" || echo "❌ 编译失败,请检查 JDK 版本(建议 17+)和 Maven 仓库"

进入frontend目录,执行:

npm install && npm run build --silent && echo "✅ 前端构建成功" || echo "❌ 构建失败,请检查 Node.js 版本(建议 18+)和依赖冲突"

2.2 后端启动:绕过数据库初始化陷阱,用 H2 内存库快速验证接口连通性

源码包里的application.yml往往预设了 MySQL 配置,但你本地未必装了 MySQL,强行启动会卡在DataSourceHealthIndicator。不要急着配 Docker,先用 H2 内存数据库把接口链路跑通——这是验证“前后端通信是否真通”的黄金 5 分钟。

修改backend/src/main/resources/application.yml:

# 替换原 datasource 配置段 spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver username: sa password: password h2: console: enabled: true # 开启 H2 控制台,访问 http://localhost:8080/h2-console jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop # 启动时建表,退出时删表(仅开发用!) show-sql: true properties: hibernate: format_sql: true

同时,在pom.xml中确保 H2 依赖已启用(注释掉 MySQL 依赖或用 profile 隔离):

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- 临时注释掉 MySQL 依赖 --> <!-- <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> -->

启动后端:

cd backend mvn spring-boot:run -Dspring.profiles.active=dev

看到控制台输出HikariPool-1 - Starting...和Started DemoApplication in X.XXX seconds即成功。此时访问http://localhost:8080/h2-console,填入 JDBC URLjdbc:h2:mem:testdb、用户名sa、密码password,可查看自动生成的表结构。

2.3 前端代理配置:Vite 下的proxy不是转发,而是请求重写规则

Vue CLI 的vue.config.js里devServer.proxy是对象式配置,而Vite 的vite.config.ts中server.proxy是函数式重写,稍有不慎就导致 404。源码包若用 Vite,其vite.config.ts中常见错误写法:

// ❌ 错误:直接转发,未重写路径 server: { proxy: { '/api': 'http://localhost:8080' } }

这会导致前端请求/api/v1/user,后端收到/api/v1/user——但你的 Spring Boot Controller 映射是@RequestMapping("/v1/user"),所以 404。正确做法是去掉/api前缀再转发:

// ✅ 正确:用 rewrite 去除 /api 前缀 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // 关键!把 /api/v1/user → /v1/user } } } })

验证:启动前端npm run dev,打开浏览器开发者工具 Network 标签页,触发一个登录请求(如POST /api/v1/auth/login),观察:

  • Request URL 应显示http://localhost:3000/api/v1/auth/login(前端地址)
  • 被代理后,实际发往http://localhost:8080/v1/auth/login(后端地址)
  • Response Status 为200 OK,且返回 JSON 数据(非 HTML 页面)

若仍 404,请检查后端 Controller 是否真的映射了/v1/auth/login(而非/api/v1/auth/login)——这是前后端路径约定不一致的典型表现。

3. 权限体系落地:JWT Token 的生成、校验与刷新不是“写个拦截器”就完事

3.1 后端 JWT 核心三件套:生成、解析、续签的代码级实现

源码包中config/SecurityConfig.java往往只做了基础配置,但真正决定权限是否生效的是JwtAuthenticationFilter和JwtTokenProvider。我们以主流实现为例(基于jjwt-api0.11+):

JwtTokenProvider.java(关键参数说明):

@Component public class JwtTokenProvider { // 🔑 秘钥必须是 Base64 编码的 256-bit 字符串!不能直接写明文 private static final String SECRET = "your-32-byte-secret-key-here-123456789012"; // 生产环境务必从环境变量读取 // ⏱️ 过期时间:30 分钟太短(用户操作中断),24 小时太长(安全风险),建议 2 小时 private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000; // 2小时毫秒数 // 📦 生成 Token:将用户ID、用户名、角色列表塞进 payload public String generateToken(Authentication authentication) { String username = authentication.getName(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRE_TIME); return Jwts.builder() .setSubject(username) // 存用户名(非密码!) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, SECRET) // HS512 比 HS256 更安全 .compact(); } // 🔍 解析 Token:捕获 ExpiredJwtException 和 SignatureException public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); return true; } catch (ExpiredJwtException ex) { log.error("JWT expired", ex); } catch (SignatureException ex) { log.error("JWT signature validation failed", ex); } catch (Exception ex) { log.error("JWT parsing error", ex); } return false; } // 🆕 提取用户名:用于加载 UserDetails public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } }

JwtAuthenticationFilter.java(重点看doFilterInternal):

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); // 从 Authorization Header 提取 Bearer Token if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromToken(jwt); // ⚠️ 关键:加载用户权限,不是只查用户名! UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); // 去掉 "Bearer " 前缀 } return null; } }

参数说明:EXPIRE_TIME设为 2 小时是平衡体验与安全的血泪经验——测试发现,用户填写一个复杂表单平均耗时 8~12 分钟,30 分钟 Token 容易在提交时过期;而 24 小时 Token 若被盗,攻击者可长期冒充。SECRET必须用Base64.getEncoder().encodeToString(key)生成 32 字节密钥,硬编码在代码里是严重漏洞。

3.2 前端 Token 管理:Pinia Store 里存 Token,Axios 拦截器里自动携带

源码包的src/store/modules/user.ts通常用defineStore管理用户状态,但新手常犯的错是:把 Token 存在localStorage却没做持久化同步,导致页面刷新后 Token 丢失。

正确做法(src/store/modules/user.ts):

import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { login, logout } from '@/api/auth' export const useUserStore = defineStore('user', () => { // ✅ 用 ref 响应式存储,且初始化时从 localStorage 读取 const token = ref<string>(localStorage.getItem('token') || '') const userInfo = ref<any>(null) // ✅ 登录成功后,同时更新 ref 和 localStorage const loginAction = async (data: any) => { const res = await login(data) token.value = res.data.token localStorage.setItem('token', res.data.token) // 持久化 userInfo.value = res.data.user return res } // ✅ 退出时清空 const logoutAction = async () => { await logout() token.value = '' userInfo.value = null localStorage.removeItem('token') } // ✅ 计算属性:判断是否已登录 const isLogin = computed(() => !!token.value) return { token, userInfo, loginAction, logoutAction, isLogin } })

src/api/request.ts中 Axios 拦截器(关键在请求头注入):

import axios from 'axios' import { useUserStore } from '@/store/modules/user' const request = axios.create({ baseURL: '/api', // 与 vite.config.ts 中 proxy 的 key 一致 timeout: 10000 }) // ✅ 请求拦截器:自动添加 Authorization 头 request.interceptors.request.use( (config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` // 注意 Bearer 后有空格! } return config }, (error) => Promise.reject(error) ) // ✅ 响应拦截器:统一处理 401(Token 过期) request.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.logoutAction() // 清空状态,跳转登录页 window.location.href = '/login' } return Promise.reject(error) } ) export default request

4. 避坑指南:这 4 个高频翻车点,90% 的“能跑通”源码都栽在这儿

4.1 现象:前端登录成功,但后续所有接口返回 403 Forbidden

原因:后端SecurityConfig中antMatchers("/api/**").authenticated()配置了路径保护,但JwtAuthenticationFilter未被addFilterBefore注入到 Spring Security 过滤链中,导致请求根本没经过 JWT 校验,直接被AnonymousAuthenticationFilter拦截。
解决:检查SecurityConfig.java,确保addFilterBefore调用在http.authorizeRequests()之前:

@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 关键:禁用 session .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() // 登录、注册等免鉴权 .antMatchers("/api/**").authenticated() // 其他接口需认证 .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // ✅ 必须加这一行! }

4.2 现象:文件上传接口(@RequestParam("file") MultipartFile file)始终报Required request part 'file' is not present

原因:前端axios默认Content-Type是application/json,但文件上传必须是multipart/form-data,且axios不会自动设置 boundary,需显式配置headers: {}并删除Content-Type让浏览器自动生成。
解决:前端上传代码改为:

// ✅ 正确:不设 headers,让 axios 自动处理 multipart export const uploadFile = (file: File) => { const formData = new FormData() formData.append('file', file) // key 必须与后端 @RequestParam("file") 一致 return request.post('/v1/file/upload', formData) // 注意:URL 是 /v1/...,非 /api/v1/... }

后端 Controller 保持:

@PostMapping("/v1/file/upload") public Result<String> uploadFile(@RequestParam("file") MultipartFile file) { ... }

4.3 现象:生产环境 Nginx 部署后,前端静态资源(JS/CSS)404,但 API 接口正常

原因:源码包vite.config.ts中base: '/'是开发默认值,但生产打包后,Vite 生成的index.html中 JS/CSS 引用路径为/assets/xxx.js,而 Nginx 配置未将/assets/指向dist/assets/目录。
解决:Nginx 配置增加静态资源 location:

location /assets/ { alias /var/www/dist/assets/; # 指向 dist/assets 目录 expires 1y; add_header Cache-Control "public, immutable"; } # 同时确保根路径指向 index.html location / { root /var/www/dist; try_files $uri $uri/ /index.html; }

4.4 现象:WebSocket 连接ws://localhost:3000/ws失败,报Error during WebSocket handshake: Unexpected response code: 404

原因:前端 WebSocket 地址写死localhost:3000,但生产环境域名是https://your-domain.com,且 WebSocket 协议必须与页面协议一致(HTTPS 页面只能用wss://)。
解决:前端动态构造 WebSocket 地址:

// ✅ 根据当前页面协议自动适配 const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; const socket = new WebSocket(wsUrl);

后端WebSocketConfig.java确保允许跨域:

@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler(), "/ws") .setAllowedOrigins("*"); // 生产环境请替换为具体域名 } }

5. 生产就绪检查:用这 7 个命令,3 分钟内完成上线前最后一遍扫描

5.1 后端安全加固:5 个必须执行的curl检查项

别信application.yml里写的配置,用真实请求验证。在后端启动状态下,执行以下命令(替换http://localhost:8080为你的真实地址):

检查项命令期望响应说明
1. H2 控制台是否已关闭curl -I http://localhost:8080/h2-consoleHTTP/1.1 404生产环境必须禁用 H2 Console,否则数据库裸奔
2. Actuator 端点是否暴露敏感信息curl http://localhost:8080/actuator/envHTTP/1.1 401 Unauthorizedmanagement.endpoints.web.exposure.include=*是高危配置,至少设为health,info
3. 静态资源是否禁止目录遍历curl http://localhost:8080/..%2F..%2F..%2Fetc%2FpasswdHTTP/1.1 400或404若返回200和系统文件内容,存在路径遍历漏洞
4. HTTP 方法是否严格限制curl -X TRACE http://localhost:8080/HTTP/1.1 405 Method Not AllowedTRACE、OPTIONS等非必要方法应禁用
5. Cookie 是否启用 HttpOnly 和 Securecurl -I http://localhost:8080/loginSet-Cookie: ...; HttpOnly; Secure; SameSite=StrictSecure表示仅 HTTPS 传输,HttpOnly防 XSS 窃取

执行方式:将以上命令保存为check-backend.sh,一键运行。若任一检查失败,立即修改application.yml或 Security 配置。

5.2 前端构建产物审计:用grep扫描敏感信息残留

前端dist/目录下可能残留开发配置。进入frontend/dist目录,执行:

# 检查是否有明文 API 地址(开发时写死的 localhost) grep -r "http://localhost" . --include="*.js" --include="*.html" # 检查是否有未移除的 console.log(影响性能且暴露逻辑) grep -r "console\.log" . --include="*.js" --include="*.ts" # 检查是否有硬编码的 Token 或密钥(极危险!) grep -r -i "token\|secret\|key\|password" . --include="*.js" --include="*.json" --include="*.ts"

若发现结果,说明构建前未清除开发环境变量。正确做法是在vite.config.ts中用import.meta.env.VUE_APP_API_BASE_URL,并通过.env.production文件注入:

# .env.production VUE_APP_API_BASE_URL = https://api.your-domain.com

5.3 跨域配置最终验证:用curl模拟浏览器预检请求

CORS 预检(OPTIONS)失败是生产环境最隐蔽的 403 来源。模拟浏览器发送预检:

curl -X OPTIONS \ -H "Origin: https://your-domain.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: authorization,content-type" \ -I http://localhost:8080/api/v1/user

期望响应头包含:

Access-Control-Allow-Origin: https://your-domain.com Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS Access-Control-Allow-Headers: authorization,content-type Access-Control-Allow-Credentials: true

若缺失Access-Control-Allow-Origin或Access-Control-Allow-Credentials,检查后端CorsConfiguration是否设置了allowCredentials(true)且allowedOrigins不为["*"](二者互斥)。

6. 我的上线 checklist 习惯:一份带时间戳的deploy.log比任何文档都可靠

每次部署新版本,我都会在项目根目录下生成一个deploy.log,内容不是流水账,而是可回溯、可验证、可归责的操作记录。格式如下(用date命令自动生成时间戳):

# deploy.log - 2024-06-15 14:22:31 ## 部署人:A同学 ## 版本:v2.3.1 (commit: a1b2c3d) ## 环境:prod (k8s namespace: prod-app) ### ✅ 前端构建 - 命令:cd frontend && npm ci && npm run build -- --mode production - 输出 hash:dist/assets/index.bf8a2e3e.js - 静态检查:grep -q "localhost" dist/index.html || echo "✅ 无 localhost 痕迹" ### ✅ 后端构建 - 命令:cd backend && mvn clean package -DskipTests -Pprod - Jar 名:demo-2.3.1.jar - 安全扫描:java -jar demo-2.3.1.jar --spring.profiles.active=test 2>&1 | grep -q "Started DemoApplication" || echo "❌ 启动失败" ### ✅ 部署验证 - curl -s http://prod-api.your-domain.com/actuator/health | jq -r '.status' # 期望 UP - curl -sI https://your-domain.com | grep -i "strict-transport-security" # 期望存在 ### ⚠️ 本次变更重点 - 新增 /api/v1/report/export 接口(需 DB 权限:SELECT on report_table) - 修改 JWT 过期时间:2h → 4h(见 backend/src/main/resources/application-prod.yml) - Nginx 配置更新:新增 /report/ 路径反向代理至 report-service ### 📌 回滚指令(粘贴即用) kubectl rollout undo deployment/demo-backend --to-revision=12 kubectl rollout undo deployment/demo-frontend --to-revision=8

这个deploy.log会随每次发布提交到 Git(放在ops/目录下),它比 Confluence 文档更准——因为它是命令执行后的实时输出,不是人脑记忆。当凌晨 2 点告警响起,运维同事第一眼看到的就是这个文件里清晰的回滚指令,而不是翻找三天前的会议纪要。我坚持了 17 个项目,没一次因部署问题定位超 10 分钟。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询