最近又有人拿着“springboot大学生社团管理系统”这个题目来找我看代码,这几乎是计算机毕业设计里每年必出现的方向,但每年都有同学在同样几个地方栽跟头。有的卡在项目创建时版本选错,JDK和SpringBoot对不上,一启动就报错;有的卡在Mapper扫描不到、页面白屏;还有的做完基础增删改查后发现流程漏洞太多,答辩被老师一问就露馅。这篇文章我就把这个项目从需求拆解到落地部署完完整整讲一遍,覆盖技术选型、数据库设计、登录认证、拦截器校验、分页查询、审核流程还有部署上线,过程中会把我在实际调试里踩过的坑一并列出来。准备拿这个题目做毕设、或者想借一个真实项目吃透SpringBoot开发套路的朋友,这篇文章可以直接当操作手册来用。
1. 先看清这个毕设的本质
1.1 一个标题三副面孔:系统到底要做什么
“计算机毕业设计springboot大学生社团管理系统”“基于SpringBoot的高校学生社团信息化管理平台”“基于Java Web的校园社团活动与服务系统”,这三个标题听起来不一样,本质上说的是同一个东西:一个面向高校学生、学生社团和系统管理员角色的校园社团信息管理平台。
核心业务可以归纳成几条线。学生端要做的事情是注册账号、浏览社团列表、查看社团详情、申请加入社团、报名活动、查看自己的活动记录。社团管理员端要做的事情是创建和管理本社团的基本信息、发布活动、审核成员加入申请、审核活动报名、发布社团公告。系统管理员端要做的事情是审核社团创建申请、管理全部用户、统计社团和活动数据、处理违规内容。
很多同学一拿到题目就急着写代码,这是最大的误区。毕设项目首先是个“管理系统”,管理两个字意味着数据从录入、流转、审核到归档,每一步都要有逻辑。比如学生申请加入社团,这个申请从提交到通过需要走一个状态变化;活动发布后有人报名,报名人数不能超过活动名额。这些流程想清楚了,数据库字段表结构自然就出来了,代码反而不是最难的部分。
1.2 为什么这个题目年年有人选
这个题目的存活率高是有道理的,它不是特别难,但也不是无脑的纯增删改查,难度刚好卡在能锻炼人又不至于做不出来的区间。
第一,业务场景清晰。高校社团管理是所有人都有生活经验的事,需求不用凭空编。学生、社团、活动、报名、公告、审核,这些概念在现实里都能对上号,做设计的时候不容易跑偏。第二,技术覆盖得全面。一个完整的社团管理系统会用到用户认证、权限拦截、CRUD、分页、一对多多对多关联查询、异常处理、数据校验,这些都是Java Web开发的核心知识点,面试和答辩都能聊。第三,扩展空间大。基础版做完之后,随便加一个功能都能成为亮点,比如活动签到、数据可视化大屏、Excel导出、消息通知,这些都是同一个题目下的加分项。
所以如果你还没定题,这个方向是可以选的;如果已经选定了,就踏踏实实把流程做完整,别只顾着堆页面。
1.3 为什么大家最后都选了SpringBoot
很多教材还在讲SSH(Struts2+Spring+Hibernate)或者SSM(Spring+SpringMVC+MyBatis),但到了真正动手做项目,几乎没人再手动搭SSM框架了,原因很现实:配置太多,效率太低。
SpringBoot最核心的价值是“自动配置”和“起步依赖”。你引入一个spring-boot-starter-web,它就帮你把SpringMVC、内嵌Tomcat、JSON转换这些基础配置全部准备好;引一个mybatis-plus-boot-starter,数据访问那一层的配置也就齐了大半。你不需要再写一堆XML配置文件去定义Bean,只需要在application.yml里写上必要的信息,比如端口、数据库连接,项目就能跑起来。
很多人担心用SpringBoot会不会丢了底层知识,其实不会。SpringBoot底层还是Spring那一套,依赖注入、AOP、事务管理全都在,只是把重复劳动简化了。答辩的时候老师问“SpringBoot和SpringMVC什么关系”“自动配置原理是什么”,你能答清楚,说明你不仅会用,还理解原理。这也是我在后面几节反复强调版本、目录、注解这些细节的原因,因为细节才是拉开分差的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统整体设计:先画清楚再动手
2.1 三种角色和权限边界
社团管理系统至少要有三种角色:学生、社团管理员、超级管理员(系统管理员)。
这里要先明确权限边界,否则后面写拦截器的时候会乱。我见过很多代码,登录是做了,但进入后台管理页面的判断只有“是否登录”,没有“角色是否匹配”,结果普通学生也能进管理页,这就是明显的设计缺陷。
我的建议是这样划分。
学生能做的事情:修改自己的个人资料、浏览所有已审核通过的社团、查看社团详情、申请加入社团、退出社团、浏览活动列表、报名活动、查看自己的报名状态。
社团管理员能做的事情:管理隶属自己的一个或多个社团的基本信息、发布和编辑社团活动、审核加入本社团的申请、审核报名本社团活动的申请、发布社团公告。需要强调“隶属自己”,也就是说一个社团管理员不能去删别人的社团,这是很常见的数据越权问题。
超级管理员能做的事情:审核社团创建申请、禁用或删除违规用户、冻结违规社团、给所有社团发系统公告、查看全站统计报表。
这道权限边界要在数据库设计阶段就体现出来,用户表里要有role字段,社团表和用户表之间要有创建者关联,成员关系表里要有状态字段。权限控制要在后端拦截器和服务层做双重校验,前端隐藏按钮只是体验优化,不能作为安全手段。
2.2 功能模块怎么拆
我通常把系统拆成六个模块:用户模块、社团模块、活动模块、报名审核模块、公告模块、统计模块。
用户模块负责注册、登录、个人信息修改和密码重置。注册环节要注意密码加密,一定不要明文存数据库,Spring Security的BCryptPasswordEncoder或者Hutool里的BCrypt都可以,哪怕不做那么复杂,至少加个盐做SHA-256,这是答辩老师特别喜欢问的点。
社团模块包含社团分类、社团创建申请、社团信息编辑、社团成员列表、成员退出和移除。社团创建不能直接生效,要有一个待审核的状态,由超级管理员审核通过之后,这个社团才会出现在前台列表里。这个流程符合现实,也能体现你的逻辑设计能力。
活动模块包含活动发布、活动编辑、活动列表、活动详情、报名截止时间、活动名额和已报名人数。发布活动之后要能防止重复提交,也要判断活动开始时间和当前时间的关系。
报名审核模块是系统的关键,学生报名活动后,状态应该是“待审核”或者“报名成功”,如果社团管理员设置了需要审核,那么状态流转就是待审核到通过/驳回,如果不需要审核,则直接占用名额。
公告模块就是简单的发布和展示,可以按角色区分:系统公告所有人可见,社团公告只有加入该社团的人可见。
统计模块可以作为加分项,展示社团数量、成员数量、活动数量、活动参与人次,按月份统计趋势,这里用ECharts画几个图表,效果非常好。
2.3 数据库设计:核心表和字段关系
数据库设计决定整个项目的上限,表字段建不好,后面写关联查询的时候想哭都哭不出来。
一个常见的核心表设计如下。用户表叫sys_user,字段包括id、username、password、real_name、student_no、phone、email、role、avatar、create_time、update_time、deleted。这里deleted是逻辑删除位,0表示正常,1表示删除,逻辑删除在毕设项目里比物理删除更好,数据可回溯,老师也认可这种设计。
社团表叫club,字段包括id、name、introduction、category_id、logo、leader_id、status、create_time、update_time、deleted。leader_id外键关联社团管理员的用户ID。status可以定义成0待审核、1已通过、2已禁用、3已驳回。
社团成员关系表叫club_member,字段包括id、club_id、user_id、role_in_club、status、join_time、create_time。这里status表示申请状态,0待审核、1已加入、2已拒绝、3已退出或者被移出。
活动表叫activity,字段包括id、club_id、title、content、location、start_time、end_time、max_participants、current_participants、status、create_time、update_time。活动状态可以设计成0未开始、1进行中、2已结束、3已取消,但这个状态最好通过时间计算,保证业务逻辑一致性。
活动报名表叫activity_register,字段包括id、activity_id、user_id、status、apply_time、audit_time、audit_remark。status是0待审核、1报名成功、2已拒绝、3已取消。报名表要加联合唯一约束,保证同一个用户对同一个活动不能重复报名。
公告表叫notice,字段包括id、title、content、type、target_club_id、create_user_id、create_time。type可以区分系统公告和社团公告。
核心的关系是:用户和社团是多对多,通过club_member表关联;一个社团有一个负责人,通过club.leader_id关联;用户和活动是多对多,通过activity_register表关联。建议在表设计文档里把字段注释都写清楚,答辩的时候直接展示ER图,很加分。
2.4 技术栈清单与版本选择
技术栈选型直接决定了你后面要踩多少坑,这里给出一套我实测下来比较稳的组合。
后端用SpringBoot 2.7.18,JDK用1.8。很多人问为什么不用最新的SpringBoot 3.x,原因很简单:3.x基于Jakarta EE,很多老版本的依赖不兼容,而且JDK要求17起步,很多学校机房和老师的电脑未必装了JDK17。毕设求稳,2.7.x是SpringBoot 2.x最后一个维护版本,功能完善,资料最多,遇到问题搜一下就有一堆答案。
数据访问层用MyBatis-Plus,版本用3.5.x。MyBatis-Plus在MyBatis之上封装了通用Mapper和通用Service,单表CRUD不需要写SQL,分页插件也好用,能省下大量时间。要注意mybatis-plus和SpringBoot的版本兼容,后面我会专门讲。
数据库用MySQL 8.x,驱动在SpringBoot 2.7.x里会自动匹配mysql-connector-java 8.0.x,连接字符串要注意使用com.mysql.cj.jdbc.Driver。
前端如果做简单模式,用Thymeleaf服务端渲染加Bootstrap就行,一个项目里不用再开Node环境;如果做前后端分离,前端用Vue3加Element Plus加Vite,后端预留统一返回结果和跨域配置。我这里两种方案都涉及,但核心代码以服务端渲染为主来演示,逻辑是一样的。
认证方案用JWT,生成一个token字符串,登录成功之后返回给前端,前端存到header里,后端用拦截器校验token。这个方案在答辩里容易讲清楚,也是现在企业开发的主流做法。
其他工具类尽量用Hutool,里面封装了日期、字符串、加密等常用工具,能减少很多造轮子的时间。
3. 核心功能实操:从搭建到跑通
3.1 创建项目和标准目录结构
打开IDEA,新建项目选择Spring Initializr,注意右侧Spring Boot版本选择2.7.18,Java版本选8,Group填com.example,Artifact填club-system。
创建完项目之后,我习惯把包结构规划成以下形式,这个结构也是很多企业项目的标准做法。
text复制com.example.clubsystem
├── config // 配置类:拦截器、跨域、MyBatis-Plus分页
├── controller // 控制层:接收请求、返回结果
├── service // 业务层:写核心业务逻辑
│ └── impl // 业务实现类
├── mapper // 数据访问层:Mapper接口
├── entity // 数据库实体类
├── dto // 前端入参封装对象
├── vo // 返回给前端的视图对象
├── common // 公共类:统一返回结果、状态枚举、异常处理
├── utils // 工具类:JWT、加密、日期处理
└── ClubSystemApplication.java // 启动类
controller放接口,service放业务逻辑,mapper放数据库操作,entity和数据库表一一对应,dto和vo负责前后端数据格式转换。这里有个细节:不要让entity直接暴露给前端,尤其是用户表里的password字段,一定要用vo把密码过滤掉再返回。
启动类最好放在包的根目录下,这样SpringBoot的组件扫描默认就能覆盖到所有子包。如果你把启动类放到com.example下面,然后controller放到了com.example.club.xxx里,启动类在com.example.clubsystem里,扫描包路径依然能覆盖,但为了直观,建议启动类就在根包。
3.2 配置文件怎么写才不出幺蛾子
SpringBoot的配置文件看似简单,但不少人在这里忽略几个关键项。下面是我在项目里使用的application.yml,摘取核心部分说明。
yaml复制server:
port: 8080
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
context-path: /
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/club_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.clubsystem.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
配置里的每个关键项我都说一下用途。driver-class-name使用com.mysql.cj.jdbc.Driver,这是MySQL 8的驱动类,以前的老驱动com.mysql.jdbc.Driver在8.x里已经废弃了,如果用了会启动报错。
url里加了useUnicode和characterEncoding=utf8,防止中文乱码;serverTimezone=Asia/Shanghai,防止日期时间比实际少了8个小时,这是很多同学容易忽略的问题。
Jackson的日期格式配置成yyyy-MM-dd HH:mm:ss,这样返回JSON里的LocalDateTime和Date字段不会变成一串数字。
multipart配置是给文件上传用的,如果社团logo、活动封面需要上传图片,没有这个配置会报文件大小超限。
mybatis-plus的map-underscore-to-camel-case打开之后,数据库的create_time能自动映射到Java的createTime,不用写一大串resultMap。log-impl打印SQL日志,调试时候非常方便,正式部署可以注释掉。
logic-delete配置是MyBatis-Plus的全局逻辑删除,实体里有个deleted字段后,删除操作执行的是update语句,查询语句会自动追加deleted=0条件。这一点务必要在配置里加,不然MyBatis-Plus默认不启用逻辑删除,逻辑删除字段形同虚设。
3.3 登录认证与Token校验:拦截器到底是干什么的
登录流程是这样的:用户提交用户名和密码,后端根据用户名查出用户,用BCrypt工具比对密码。登录成功之后,生成一个JWT token,token里可以放userId和role,然后用拦截器对所有需要登录的接口做校验。
JWT工具类核心代码大致如下。
java复制public class JwtUtil {
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("your-secret-key-please-change-123456".getBytes(StandardCharsets.UTF_8));
public static String generateToken(Long userId, String role) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24))
.signWith(SECRET_KEY)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
}
这里有一个很关键的点:密钥不要太短,太短启动的时候会报签名错误;不要硬编码到代码里,至少放到application.yml里用@Value注入,做个样子也是好的。token过期时间设置24小时,如果要做“记住我”就设置7天。
拦截器类实现HandlerInterceptor接口,在preHandle方法里从request的header中取出token,解析成功就放行,失败就返回401。
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}");
return false;
}
try {
Claims claims = JwtUtil.parseToken(token);
request.setAttribute("userId", Long.valueOf(claims.getSubject()));
request.setAttribute("role", claims.get("role"));
return true;
} catch (Exception e) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"token无效或已过期\"}");
return false;
}
}
}
拦截器要注册到WebConfig里,同时指定放行路径。
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login", "/user/register", "/club/list");
}
}
接口设计上我习惯把需要认证的接口统一放在/api下面,比如/api/user/profile、/api/club/manage,这样拦截器的路径配置就很清晰。登录注册接口放行,其他接口都要过token校验。如果社团管理员的接口要额外校验角色,可以在拦截器里根据路径前缀判断,也可以在Controller方法上写自定义注解实现,毕设规模用拦截器判断角色就足够了。
3.4 社团与活动的CRUD:MyBatis-Plus让代码量少一半
如果用原生MyBatis,写一个社团列表要自己写XML、resultMap、动态SQL,代码量很大。MyBatis-Plus把单表CRUD都封装好了,你的Mapper接口只需要继承BaseMapper,Service实现类继承ServiceImpl,基础的增删改查就全有了。
社团管理员的添加社团和编辑社团业务,核心代码可以这样写。
java复制@Service
public class ClubServiceImpl extends ServiceImpl<ClubMapper, Club> implements ClubService {
@Override
public Club createClub(Long userId, ClubDTO dto) {
Club club = new Club();
BeanUtils.copyProperties(dto, club);
club.setLeaderId(userId);
club.setStatus(0);
club.setDeleted(0);
this.save(club);
return club;
}
@Override
public Page<Club> pageClubs(int pageNum, int pageSize, Long categoryId) {
LambdaQueryWrapper<Club> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Club::getDeleted, 0)
.eq(categoryId != null, Club::getCategoryId, categoryId)
.orderByDesc(Club::getCreateTime);
return this.page(new Page<>(pageNum, pageSize), wrapper);
}
}
LambdaQueryWrapper的好处是写起来简单,而且属性名是编译期检查,数据库表字段改了,代码这里会直接编译报错,比字符串写法安全很多。分页这一块,要记得配置MyBatis-Plus的分页插件,否则分页查询拿到的是全表数据。
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
这个分页插件一定要在配置类里创建,并且指定数据库类型。没有它会怎样?分页方法会执行查询所有记录然后内存里手动分页,数据量一大就卡死,而且发出的SQL没有limit,属于经典坑点。
活动模块的代码结构和社团类似,但要注意几个业务校验。发布活动的时候判断clubId是否存在、当前用户是不是该社团的管理员,开始时间不能晚于结束时间,最大人数必须大于0。更新活动的时候要判断已经有报名的用户数量,如果currentParticipants大于新的maxParticipants,这个修改就要拒绝,否则会出现报名人数超过上限的数据错误。
3.5 报名审核与名额控制:状态流转要提前设计
活动报名是整个系统里最容易出问题的点,重复报名、超额报名、活动结束还在报名,这些如果没处理,答辩的时候大概率会被问到。
我的做法是先在数据库层面加唯一约束,activity_register表里activity_id和user_id建立联合唯一索引,从底层杜绝同一个人对同一个活动报名两次。这样即使代码里漏判了,数据库也会报错兜底。
然后在代码里加三个校验。第一,校验活动状态,活动必须处于报名时间内。第二,校验名额,活动表里已经报名的人数要小于maxParticipants。第三,校验审批流程,如果活动需要审核,状态设为待审核;如果不需要审核,直接报名成功并增加当前人数。
这里的名额扣减有并发问题。两个用户同时报名最后一个名额,代码里先查再用,可能两个请求都查到还有名额,然后都插入成功,让报名人数超过上限。对于毕设项目,最直接的解决办法是给活动表加一个乐观锁版本号字段version,更新当前人数时使用update语句带上version条件,更新成功后version加1。如果影响行数为0,说明名额已被占满,提示用户报名失败。这个方案能讲明白,老师会觉得你考虑到了并发场景,是加分项。
审核状态流转建议用一张表格提前定清楚,开发的时候照着写就不会乱。
| 场景 | 原状态 | 操作(角色) | 新状态 |
|---|---|---|---|
| 学生提交入社申请 | 无 | 学生 | 待审核 |
| 管理员通过入社申请 | 待审核 | 社团管理员 | 已加入 |
| 管理员拒绝入社申请 | 待审核 | 社团管理员 | 已拒绝 |
| 学生报名活动 | 无 | 学生 | 待审核或报名成功 |
| 管理员通过报名 | 待审核 | 社团管理员 | 报名成功 |
| 管理员拒绝报名 | 待审核 | 社团管理员 | 已拒绝 |
| 学生取消报名 | 报名成功 | 学生 | 已取消 |
| 学生退出社团 | 已加入 | 学生 | 已退出 |
状态字段用int类型,在Java侧定义一个枚举类,比如ActivityRegisterStatusEnum,里面把每个状态值和描述写清楚,代码里不要到处写魔法数字。这样团队合作也好,答辩展示也好,都能看出你写代码是有工程意识的。
4. 常见问题与排查技巧实录
4.1 版本不匹配:SpringBoot不是越高越好
每年都有人因为版本踩坑,其中最典型的问题是IDEA里新建SpringBoot项目时默认选了3.x版本,结果本机是JDK1.8,启动直接报错。SpringBoot 3.x要求JDK17及以上,这是硬门槛,换不了。
我的建议很直接:毕设就老老实实用SpringBoot 2.7.18配JDK1.8,这个搭配最稳。如果要学新特性,等毕设做完再折腾。
还有一个常见问题是mybatis-plus的版本和spring-boot 2.x对应不上。mybatis-plus 3.5.9之后的一些版本已经要求Spring Boot 3环境,所以如果你用SpringBoot 2.7,就把mybatis-plus版本固定在3.5.3.x左右。以下是我常用的一组稳定依赖版本,可以直接照抄:
- SpringBoot 2.7.18
- mybatis-plus-boot-starter 3.5.3.2
- mysql-connector-java 8.0.33(由SpringBoot管理)
- hutool-all 5.8.25
- jjwt-api / jjwt-impl / jjwt-jackson 0.11.5
依赖冲突看Maven的依赖树,IDEA里可以直接右键项目选择Maven-Show Dependencies,或者在Terminal执行mvn dependency:tree,定位到exclusions排除掉多余依赖就行。
4.2 Mapper扫描不到与XML路径问题
启动项目时报Invalid bound statement not found,或者提示找不到Mapper,绝大多数是三个原因。
第一个原因是启动类没有加@MapperScan注解,或者Mapper接口没有加@Mapper注解。解决办法是在启动类上加@MapperScan("com.example.clubsystem.mapper"),或者每个Mapper接口上标@Mapper。二选一即可,不要重复,重复也无伤大雅,但养成好习惯更重要。
第二个原因是Mapper的XML文件没被编译到target目录,或者路径配置不对。如果你把XML放到了src/main/java/com/.../mapper下面,Maven默认只编译.java文件,XML会被忽略。解决办法是把XML放到src/main/resources/mapper目录下,然后在application.yml里配置mybatis-plus.mapper-locations: classpath*:mapper/**/*.xml。如果你的XML在java目录下,也可以在pom.xml里配置resources标签把XML也打进去,但这不是常规做法,不推荐。
第三个原因是XML里的namespace和Mapper接口全路径不匹配,或者方法id和Mapper接口方法名不一致。这类问题通常看控制台报错,会直接告诉你哪个语句找不到,按照报错信息去检查即可。
4.3 端口占用、上下文路径与404
SpringBoot项目启动时报Port 8080 was already in use,说明8080被其他进程占用了。Windows下可以在命令行执行netstat -ano | findstr 8080,找到占用进程的PID,然后taskkill /PID 你的PID /F,或者直接改application.yml里的server.port为8081、8082都行。
还有一类404问题很隐蔽,就是配置了server.servlet.context-path: /api之后,Controller里的请求路径也要带上/api前缀,模板和静态资源的引用路径也会被影响。我建议初学者不要随便配置context-path,保持默认根路径,让所有路径都在Controller的RequestMapping上管理,这样排查问题简单很多。
如果访问Controller接口返回404,先看控制台有没有类似“Map 请求路径”的日志,再看请求路径和注解路径是否一致,特别是路径里的变量名和@PathVariable参数名是否对得上。有时候是跨服务调用时端口写错,这些都要逐一排查。
4.4 前后端跨域与Token失效
做前后端分离的时候,前端跑在localhost:5173,后端跑在localhost:8080,浏览器会拦截跨域请求。解决办法是后端加一个全局跨域配置。
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注意allowedOriginPatterns和allowCredentials不能同时使用通配符*,所以才用allowedOriginPatterns("*"),这是SpringBoot比较新版本的标准写法。跨域配置添加后,前端发起OPTIONS预检请求时,后端拦截器要放行OPTIONS请求,否则预检请求被拦截器挡住,一样会报跨域错误。
token失效这个问题常见于登录后无法获取用户信息。排查思路是:前端是否在请求头里带了token,后端拦截器里是否用request.getHeader("token")取得和前端一致的key。如果前端把token放在localStorage,请求拦截器里一定要手动设置header,很多同学忘记这一步,导致token一直为空。
4.5 中文乱码、XSS过滤与文件上传
中文乱码问题在SpringBoot里一般有三处。第一处是数据库连接的URL没有加characterEncoding=utf8,第二处是服务端响应没有设置UTF-8编码,第三处是前端页面本身的charset不是UTF-8。SpringBoot 2.x里只要配置了server.servlet.encoding.force=true,再在数据库URL里加上characterEncoding=utf8,大部分乱码都能解决。前提是数据库表本身的字符集也要是utf8mb4,建库的时候写清楚,比如CREATE DATABASE club_system DEFAULT CHARACTER SET utf8mb4。
XSS过滤是很多毕设忽略的点。热搜里有人问“上传PDF时XSS处理”,说明老师现在也会关注安全问题。我的建议是写一个全局过滤器,把所有请求参数里的尖括号等危险字符转义,或者引入一个开源的XSS过滤器工具类,对请求体里的脚本标签做清洗。至少要把
