ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM+Vue前后端分离登录态实战:Session、跨域与鉴权全链路解析

SSM+Vue前后端分离登录态实战:Session、跨域与鉴权全链路解析 简介本资源是一套完整的Java毕业设计项目——基于SSM框架SpringSpringMVCMyBatis与Vue前后端分离架构的学习交流论坛系统面向计算机专业本科生及Java初学者解决课程设计、毕设选题与全栈开发实践需求。压缩包为ZIP格式大小20.79MB包含可直接运行的源码工程、MySQL5.7数据库脚本、开题报告与毕业论文文档等核心内容涵盖后端Maven工程结构、前端静态资源目录、SQL建表语句及完整业务模块代码。已有63人学习下载适合需要快速掌握B/S系统开发流程、理解管理员与用户双角色权限设计、实践博客管理论坛交互公告轮播等典型功能的学生开发者。资源提供开箱即用的完整解决方案含登录鉴权、文章分类、留言反馈、收藏评论、个人中心等模块源码以及清晰的分层结构与注释便于二次开发与技术复现。1. 这不是又一个“SSM毕设模板”它真能跑通前后台分离的论坛交互闭环且 Vue SSM 的请求链路不丢 session、不跨域报错、不卡在登录态校验——适合想交差但更想搞懂「为什么毕业设计总卡在登录跳转」的同学你肯定见过太多标着“SSM论坛系统”的压缩包解压后pom.xml里 Spring 版本写的是 4.3.20web.xml还在配DispatcherServlet映射路径前端index.jsp里混着c:forEach和jQuery.ajax一启动就报No mapping found for HTTP request with URI [/login]。这不是代码问题是架构断层——后端用 MVC 模式前端却硬塞 Vue 实例路由、状态、鉴权全靠 JS 拼凑结果登录成功后跳首页Vue 路由守卫拿不到 token后台接口返回 401学生只能截图发群里问“老师为啥我点登录没反应”这个项目不一样。它把 B/S 架构真正拆成两层SpringMVC 只做 RESTful 接口/api/user/login,/api/article/listVue CLI 项目独立运行npm run serve通过axios.defaults.baseURL http://localhost:8080统一代理关键是在LoginController里用HttpSession存用户 ID 角色再配合CrossOrigin注解和WebMvcConfigurer配置CorsRegistry让前端跨域请求带credentials: true时后端能正确读取JSESSIONIDCookie 并校验登录态。数据库用 MySQL 5.7所有表字段加了NOT NULL和COMMENT连article_collect收藏表都建了联合唯一索引(user_id, article_id)防止重复收藏。它不是教你“怎么写增删改查”而是告诉你毕业设计里最常翻车的不是功能缺而是请求链路断在哪一环——是 Cookie 传不过去是 Session ID 没同步还是 Vue 路由守卫没等接口返回就跳了如果你正卡在“登录成功但进不了首页”“管理员后台点不了按钮”“收藏文章后刷新就没了”这篇笔记就是为你写的。2. 把 SSM 后端从“能编译”变成“能扛住并发请求”SpringMVC 接口设计、MyBatis 动态 SQL 与事务边界实操2.1 SpringMVC 接口分层设计为什么/api/admin/和/api/user/必须物理隔离项目里所有接口路径强制以/api/开头这是为后续可能的 Nginx 反向代理或微服务拆分留的余地。更关键的是管理员接口和用户接口做了物理路径隔离// AdminController.java RestController RequestMapping(/api/admin) public class AdminController { Autowired private ArticleService articleService; GetMapping(/articles) public ResultListArticle listArticles(RequestParam Integer page, RequestParam Integer limit) { PageHelper.startPage(page, limit); ListArticle articles articleService.selectAll(); return Result.success(articles); } }// UserController.java RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/register) public ResultString register(RequestBody User user) { // 密码加密逻辑在此 userService.insert(user); return Result.success(注册成功); } }提示RequestMapping(/api/admin)不是装饰是契约。它让前端 Axios 实例能按角色创建不同 base URL// api/admin.js const adminApi axios.create({ baseURL: /api/admin }); // api/user.js const userApi axios.create({ baseURL: /api/user });这样当管理员点击“文章管理”时调adminApi.get(/articles)用户点击“注册”时调userApi.post(/register)路径天然隔离避免权限混淆。如果全写成/api/articles后期加 RBAC 权限控制时你得在每个 Controller 方法上加PreAuthorize(hasRole(ADMIN))而路径隔离后Nginx 层就能直接拦截/api/admin/**的非管理员请求。2.2 MyBatis 动态 SQL 实战论坛帖子搜索、分页、多条件组合查询怎么写才不慢论坛交流模块的帖子列表支持按标题关键词、发布时间范围、分类 ID 三重筛选。如果用WHERE 11 AND title LIKE %xxx% AND category_id ?硬拼SQL 会丢失索引。项目用 MyBatis 的if标签动态拼接!-- ArticleMapper.xml -- select idselectByCondition resultTypeArticle SELECT * FROM forum_post where if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if if teststartTime ! null AND create_time #{startTime} /if if testendTime ! null AND create_time #{endTime} /if /where ORDER BY create_time DESC /select关键点有三个where标签自动处理AND开头问题它会智能去掉第一个AND避免WHERE AND title LIKE ...的语法错误CONCAT(%, #{title}, %)防止 SQL 注入#{}是预编译参数比${}安全ORDER BY create_time DESC必须加索引MySQL 5.7 中对create_time字段建INDEX idx_create_time (create_time)否则LIMIT 10,20分页会全表扫描。参数说明startTime和endTime是String类型如2024-01-01MyBatis 会自动转换为DatecategoryId为Integer0表示“全部分类”所以判断! 0而非! null。2.3 Service 层事务边界为什么“发布帖子更新分类统计数”必须在一个Transactional里用户发布一篇新帖子时系统要同时做两件事插入forum_post表一条记录更新forum_category表中对应分类的post_count字段post_count post_count 1。如果这两步分开写中间出错比如插入成功但更新失败就会导致帖子存在但分类计数不准。项目在PostService中用Transactional包裹Service public class PostService { Autowired private ForumPostMapper postMapper; Autowired private ForumCategoryMapper categoryMapper; Transactional(rollbackFor Exception.class) public void publishPost(ForumPost post) { postMapper.insert(post); // 步骤1插入帖子 categoryMapper.updatePostCount(post.getCategoryId()); // 步骤2更新分类计数 } }rollbackFor Exception.class是重点默认Transactional只对RuntimeException回滚而updatePostCount()可能抛SQLException检查异常不加这句SQL 异常发生时事务不会回滚造成数据不一致。血泪经验很多同学把Transactional加在 Controller 层这是大忌。Controller 层可能调用多个 Service事务边界过宽会导致锁表时间过长。必须精确到“一个业务原子操作”比如“发帖”是一个原子“收藏文章”是另一个原子各自独立事务。3. Vue 前端不是“套个模板”路由守卫、Token 管理、Axios 请求拦截器的真实落地3.1 Vue Router 路由守卫如何让“未登录用户点个人中心自动跳登录页”不丢参数项目用 Vue Router 4Composition API路由配置里明确区分publicRoutes无需登录和privateRoutes需登录// router/index.js const publicRoutes [ { path: /, component: () import(/views/Home.vue) }, { path: /login, component: () import(/views/Login.vue) }, { path: /register, component: () import(/views/Register.vue) } ] const privateRoutes [ { path: /user/profile, component: () import(/views/UserProfile.vue) }, { path: /admin/articles, component: () import(/views/AdminArticles.vue) } ] const router createRouter({ history: createWebHistory(), routes: [...publicRoutes, ...privateRoutes] })全局前置守卫判断登录态router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) const isPublic publicRoutes.some(route route.path to.path) if (isPublic) { next() // 公共页面直接放行 } else if (!token) { next({ path: /login, query: { redirect: to.fullPath } }) // 记录原目标路径 } else { try { // 验证 token 是否有效调用 /api/user/info const res await axios.get(/api/user/info) if (res.data.code 200) { next() } else { localStorage.removeItem(token) next(/login) } } catch (e) { localStorage.removeItem(token) next(/login) } } })注意next({ path: /login, query: { redirect: to.fullPath } })是关键。用户从/user/profile被重定向到/login后登录成功时应自动跳回/user/profile而不是首页。to.fullPath包含完整路径如/user/profile?tabpostsquery参数确保 URL 参数不丢失。3.2 Axios 请求拦截器为什么所有请求必须带X-Requested-With: XMLHttpRequest头SpringMVC 默认开启X-Requested-With头校验用于区分 AJAX 请求和普通表单提交。如果前端不带这个头ResponseBody返回的 JSON 会被ViewResolver当作视图名处理返回 404。项目在utils/request.js中统一设置const service axios.create({ baseURL: http://localhost:8080, timeout: 10000, headers: { X-Requested-With: XMLHttpRequest // 必须否则后端返回 404 } }) // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) )玄学点X-Requested-With头不是可选的“兼容性补丁”而是 SpringMVC 的底层机制。如果你删掉这行/api/user/login接口会返回{code:200,msg:登录成功}的 HTML 页面源码因为ViewResolver找不到login视图而不是纯 JSON。这是毕业设计里最隐蔽的坑之一。3.3 用户角色动态菜单如何让管理员看到“用户管理”普通用户看不到前端菜单不是写死的而是根据登录后返回的role字段动态渲染。登录成功后后端返回{ code: 200, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., user: { id: 1001, username: admin, role: ADMIN } } }Vuex store 中存user.role侧边栏组件用v-for渲染!-- components/Sidebar.vue -- template el-menu :default-activeactiveMenu menu-item v-foritem in filteredMenus :keyitem.path :indexitem.path {{ item.title }} /menu-item /el-menu /template script setup import { computed } from vue import { useStore } from /store import { menus } from /config/menus const store useStore() const role computed(() store.state.user.role) const filteredMenus computed(() { return menus.filter(menu { if (menu.role ALL) return true return menu.role.includes(role.value) }) }) /scriptmenus.js配置如下// config/menus.js export const menus [ { title: 首页, path: /, role: [ALL] }, { title: 个人中心, path: /user/profile, role: [USER, ADMIN] }, { title: 文章管理, path: /admin/articles, role: [ADMIN] }, { title: 用户管理, path: /admin/users, role: [ADMIN] } ]避坑不要在v-if里写v-ifrole ADMIN。这样会导致 DOM 节点被销毁重建Vue 组件状态丢失。用filter动态生成菜单数组既保持响应式又避免重渲染。4. 数据库设计不是“建完表就完事”MySQL 5.7 的字符集、索引、外键约束与备份脚本实操4.1 字符集与排序规则为什么utf8mb4是唯一选择utf8会丢 emoji项目数据库建库语句明确指定CREATE DATABASE learning_forum DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4是 MySQL 对 UTF-8 的完整实现支持 4 字节字符如 、‍、❤️而 MySQL 的utf8实际是utf8mb3最多只支持 3 字节遇到 emoji 会存成?或报错Incorrect string value。参数说明COLLATE utf8mb4_unicode_ci表示按 Unicode 标准排序和比较比utf8mb4_general_ci更准确后者已废弃。例如café和cafe在unicode_ci下不相等在general_ci下可能被误判为相等。4.2 关键字段索引策略为什么forum_post表要建联合索引(category_id, create_time)forum_post表有 3 个高频查询场景按分类查最新帖子WHERE category_id ? ORDER BY create_time DESC LIMIT 10按用户查自己发的帖子WHERE user_id ? ORDER BY create_time DESC全站最新帖子ORDER BY create_time DESC。单列索引INDEX idx_category_id (category_id)无法优化ORDER BY create_time因为 MySQL 只能用索引的最左前缀。项目建联合索引-- 优化“按分类查最新帖子” ALTER TABLE forum_post ADD INDEX idx_category_time (category_id, create_time); -- 优化“按用户查自己发的帖子” ALTER TABLE forum_post ADD INDEX idx_user_time (user_id, create_time);验证方法用EXPLAIN查看执行计划。执行EXPLAIN SELECT * FROM forum_post WHERE category_id 5 ORDER BY create_time DESC LIMIT 10;如果type是refkey是idx_category_timeExtra里有Using index condition; Using filesort说明索引生效但排序仍需临时文件——这是正常现象因为ORDER BY方向与索引方向一致都是 DESCMySQL 5.7 会利用索引有序性避免额外排序。4.3 外键约束与级联删除为什么forum_comment表的post_id必须加外键forum_comment表的post_id字段关联forum_post.id项目建外键ALTER TABLE forum_comment ADD CONSTRAINT fk_comment_post FOREIGN KEY (post_id) REFERENCES forum_post(id) ON DELETE CASCADE;ON DELETE CASCADE表示当删除一篇帖子时该帖子下的所有评论自动删除。如果不加外键靠应用层手动删评论容易漏删或事务失败导致数据不一致。注意MySQL 5.7 中外键字段必须有索引post_id已建索引且两张表引擎必须是 InnoDBMyISAM 不支持外键。建表时务必写ENGINEInnoDB DEFAULT CHARSETutf8mb4。4.4 数据库备份脚本一行命令导出结构数据且排除日志表项目提供backup_db.sh脚本一键备份#!/bin/bash # backup_db.sh DB_NAMElearning_forum BACKUP_DIR/home/backup DATE$(date %Y%m%d_%H%M%S) mysqldump -u root -pyour_password \ --databases $DB_NAME \ --single-transaction \ --routines \ --triggers \ --events \ --ignore-table$DB_NAME.forum_log \ $BACKUP_DIR/$DB_NAME_$DATE.sql--single-transaction保证备份时数据一致性InnoDB 表--ignore-table排除日志表如forum_log避免备份文件过大--routines导出存储过程和函数项目虽未用但留扩展空间。血泪经验别用mysqldump -A备份所有库生产环境可能有其他测试库备份文件里混着敏感数据。必须明确指定--databases $DB_NAME。5. 避坑SSM Vue 项目部署上线前必须过的五道坎5.1 现象前端npm run serve能登录但npm run build后部署到 Tomcat登录接口 404原因Vue CLI 打包后静态资源放在dist/目录需通过 Nginx 或 Tomcat 的webapps/ROOT目录访问。但项目后端是 SpringMVCweb.xml中DispatcherServlet映射路径为/会拦截所有请求包括/static/js/app.js导致静态资源 404。解决在web.xml中配置静态资源不拦截servlet-mapping servlet-namedefault/servlet-name url-pattern/static/*/url-pattern /servlet-mapping servlet-mapping servlet-namedefault/servlet-name url-pattern/favicon.ico/url-pattern /servlet-mapping同时Vue 的vue.config.js中设置publicPath: /static/确保打包后 JS/CSS 路径正确。5.2 现象管理员登录后点“用户管理”页面空白浏览器控制台报GET http://localhost:8080/api/admin/users 403原因Spring Security 未配置/api/admin/**的权限放行或PreAuthorize注解写错。项目实际未用 Spring Security而是手写拦截器但拦截器中excludeUrls没包含/api/admin/**。解决检查LoginInterceptor.javaOverride public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); // 必须放行所有 /api/** 接口否则拦截器会拦掉 if (uri.startsWith(/api/)) { return true; } // ... 其他逻辑 }5.3 现象MySQL 5.7 启动时报ERROR 1045 (28000): Access denied for user rootlocalhost原因项目application.yml中数据库密码写的是123456但本地 MySQL root 密码不是这个或者 MySQL 5.7 默认认证插件是caching_sha2_password而老版 JDBC 驱动不支持。解决登录 MySQLmysql -u root -p执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;刷新权限FLUSH PRIVILEGES;pom.xml中 JDBC 驱动版本升级到8.0.28dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency5.4 现象Vue 页面中v-model绑定的输入框输入中文后失去焦点内容消失原因Vue 2.x 中v-model在某些输入法下触发时机异常项目用 Vue 2.6需加.lazy修饰符或改用input。解决将v-modelform.title改为input v-model.lazyform.title / !-- 或 -- input inputform.title $event.target.value /5.5 现象IDEA 启动项目报Error creating bean with name sqlSessionFactory提示Cannot load driver class com.mysql.cj.jdbc.Driver原因JDK 1.8 与 MySQL 8.0 驱动不兼容但项目要求 MySQL 5.7驱动应为mysql-connector-java:5.1.47。解决检查pom.xml确保dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency并删除 Maven 仓库中mysql-connector-java-8.x的 jar 包避免冲突。6. 从“能跑起来”到“能讲清楚”用 Postman 验证接口、用 Chrome DevTools 查看 Cookie、用 MySQL Workbench 分析慢查询6.1 用 Postman 验证登录流程三步确认 Session 传递是否可靠毕业答辩时老师最爱问“你怎么证明登录态是有效的” 光说“我用了 HttpSession”不够得拿出证据。我习惯用 Postman 做三步验证第一步POST/api/user/loginBody 选x-www-form-urlencoded填usernameadminpassword123456成功返回{code:200,data:{token:xxx}}查看 Postman 的Cookies标签页确认JSESSIONID已自动保存值如ABC123XYZ第二步GET/api/user/infoHeaders 加Cookie: JSESSIONIDABC123XYZ返回{code:200,data:{id:1,username:admin,role:ADMIN}}证明 Session 有效第三步GET/api/user/infoHeaders 删除Cookie行返回{code:401,msg:未登录}证明无 Session 时被拦截。技巧Postman 的Cookies标签页里右键JSESSIONID→Edit可以手动修改值模拟 Session 失效场景验证后端拦截逻辑是否健壮。6.2 用 Chrome DevTools 查看 Cookie为什么SameSiteLax会导致跨域请求丢失 CookieVue 项目运行在http://localhost:8080后端接口在http://localhost:8081或同端口但不同上下文这就是跨域。Chrome 80 默认给 Cookie 加SameSiteLax意味着跨站 POST 请求如登录会带上 Cookie但跨站 GET 请求如/api/user/info不会带。项目解决方案是后端WebMvcConfigurer中配置CorsConfigurationBean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) .allowCredentials(true) // 关键允许携带 Cookie .maxAge(3600); } }; }前端 Axios 请求必须加withCredentials: trueaxios.defaults.withCredentials true;然后在 Chrome DevTools → Application → Cookies 中查看localhost下的JSESSIONID其SameSite字段应为None因allowCredentialstrueSpring 会自动设为None否则请求发不出 Cookie。6.3 用 MySQL Workbench 分析慢查询定位“论坛首页加载慢”的真实瓶颈假设首页加载超过 3 秒怀疑是SELECT * FROM forum_post ORDER BY create_time DESC LIMIT 20慢。在 MySQL Workbench 中打开Performance Dashboard→Query Analyzer执行SELECT * FROM forum_post ORDER BY create_time DESC LIMIT 20查看Execution Plan如果type是ALL说明全表扫描执行SHOW CREATE TABLE forum_post确认create_time有索引如果仍有问题加FORCE INDEX (idx_create_time)SELECT * FROM forum_post FORCE INDEX (idx_create_time) ORDER BY create_time DESC LIMIT 20;表格常见慢查询优化对照表| 场景 | 原 SQL | 优化后 SQL | 索引建议 | |------|--------|------------|----------| | 按分类查最新帖子 |WHERE category_id5 ORDER BY create_time DESC|WHERE category_id5 ORDER BY create_time DESC|INDEX idx_category_time (category_id, create_time)| | 模糊搜索标题 |WHERE title LIKE %java%|WHERE title LIKE java%左匹配 |INDEX idx_title (title)| | 统计某用户发帖数 |SELECT COUNT(*) FROM forum_post WHERE user_id1001|SELECT COUNT(*) FROM forum_post WHERE user_id1001|INDEX idx_user_id (user_id)|从那以后我每次交接毕业设计代码都会先跑一遍 Postman 三步验证再打开 Chrome DevTools 看一眼 Cookie 的SameSite值最后用 MySQL Workbench 抓一个慢查询。不是为了炫技是怕学生第二天早上哭着找我说“老师我昨天还能登录今天突然 401 了……” —— 其实就是JSESSIONIDCookie 被浏览器清掉了而他没意识到localStorage里的 token 和Cookie是两套体系。希望帮到你。本文还有配套的精品资源点击获取
返回列表