ARTICLE DETAIL

资讯详情

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

SpringBoot+Spring Security实现竞赛系统认证与权限控制

SpringBoot+Spring Security实现竞赛系统认证与权限控制 简介一份面向高校毕业设计及课程设计场景的“大学生竞赛管理系统”完整项目源码基于 SpringBoot Spring Security Jwt 构建后端接口配合 Vue.js Element UI axios MyBatis Plus 实现了清晰的前后端分离。项目聚焦大学生竞赛的报名、管理与信息展示是一套可运行的完整系统适合作为毕业设计选题或课程设计参考。资源包共 53 个文件以 31 个 Vue 组件、11 个 JS 脚本、3 个 CSS 样式及若干 JSON 配置为主另有 README 等说明文档整体仅 230KB结构精简适合快速理解项目骨架。目前已有 85 人浏览学习。内容包含前端页面组件、路由与状态管理、接口调用封装以及后端基于 Spring Security 与 JWT 的权限认证和业务逻辑示例可帮助读者掌握登录鉴权、赛事报名、信息管理等典型模块源码目录层次分明便于在毕业设计答辩或课程作业中直接复用、扩展或二次开发。1. 大学生竞赛管理系统SpringBoot Spring Security 的选型从哪里开始高校里的竞赛报名往往在截止前半小时达到峰值几十支队伍同时提交材料管理员还要在几百条记录里逐个核对成员资格。比功能缺失更麻烦的是权限边界模糊报过名的学生能改掉已经审核过的名单吗评审老师能不能看到其他老师的打分记录很多人会用if (user.getRole().equals(ADMIN))这种散落的脏代码来应付。这个标题要解决两件事用SpringBoot快速把竞赛管理系统的CRUD跑通用Spring Security把登录、授权、接口保护做成完整链路。适合正在做课设、毕设或准备接手校内竞赛项目的开发者你需要的不只是能跑的demo还要知道版本怎么选、过滤器链怎么配、403为什么查不出原因。2. 用SpringBoot搭竞赛管理系统的项目骨架与数据模型2.1 创建项目时怎么选版本避免SpringBoot版本太高的坑很多人拿到一个“.zip”项目后第一件事是解压后直接运行结果遇到javax.servlet包不存在或者Security配置类找不到。常见原因就是SpringBoot版本和本地JDK不匹配。目前最常见的选择是JDK8环境用SpringBoot 2.7.xJDK17及以上用SpringBoot 3.x。SpringBoot 3.x把javax.都换成了jakarta.从旧版复制过来的代码很多人就在这里卡住。Spring Security的配置方式也随之变化5.7以前常用的WebSecurityConfigurerAdapter在5.7里标了deprecated6.0以后直接移除。所以在创建项目阶段先去 start.spring.io 或者IDEA的Spring Initializr里核对依赖版本比后面排错省时间。我一般在pom.xml里固定依赖如下!-- SpringBoot父工程统一管理依赖版本 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web、Security、JPA、MySQL驱动 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies这段配置的核心是spring-boot-starter-parent它统一管理了SpringBoot、Spring Framework、Spring Security的版本号不需要自己再去对齐各个jar的版本。spring-boot-starter-security会把Security自动装配进Web应用也就是说只要引入依赖所有接口默认都会被拦截控制台会给你一串随机的登录密码。很多人第一次看到这个现象会以为项目坏了其实是Security的默认保护机制生效了。JPA在这里用来管理用户、队伍、竞赛表的关联关系如果你更熟悉MyBatis-Plus可以换成mybatis-plus-boot-starter只是后面基于SQL的权限控制要自己写一些XML。为了减少后续返工我建议把版本和ORM选型先确定下来。下面是我常对比的几项选型点SpringBoot 2.7.x JPASpringBoot 3.x MyBatis-PlusJDK要求JDK8/11/17JDK17及以上Security配置SecurityFilterChain Bean同样是Bean但命名空间是jakarta表关系处理ManyToMany/OneToMany直接映射自己写SQL关联适合场景表结构清晰、关系多团队习惯SQL复杂的动态查询多常见坑mysql-connector-java的坐标带版本驱动类名变成了com.mysql.cj.jdbc.Driver注意不要把版本锁得太死比如Spring Security OAuth2.0服务搭建依赖的SDK版本和普通Servlet应用的Security版本是不同分支等做到单点登录时再单独引入。2.2 竞赛管理系统的核心表设计与表关系竞赛管理系统最典型的三张核心表是用户表、竞赛表、队伍表。用户表存放学生、教师、管理员三类账号竞赛表存放报名起止时间和当前状态队伍表把多个学生组织起来并记录审核状态。设计时不要为了图省事把这些字段全塞进一张表因为Spring Security的授权判断会频繁读取角色和状态拆开后可以用索引覆盖。下面是一份可以直接执行的DDL我写的时候刻意控制了字段数量-- 用户表角色用大写英文方便和Security的ROLE_前缀配合 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) NOT NULL DEFAULT STUDENT, enabled TINYINT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 竞赛表报名起止时间由后端判定不信任前端传参 CREATE TABLE competition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, register_start DATETIME NOT NULL, register_end DATETIME NOT NULL, status VARCHAR(20) NOT NULL DEFAULT DRAFT ); -- 队伍表status用于审核流程 CREATE TABLE team ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL, leader_id BIGINT NOT NULL, team_name VARCHAR(100) NOT NULL, status VARCHAR(20) DEFAULT PENDING ); -- 队伍成员关联表 CREATE TABLE team_member ( team_id BIGINT NOT NULL, user_id BIGINT NOT NULL, role_in_team VARCHAR(20), PRIMARY KEY (team_id, user_id) );这份SQL里sys_user.role的值建议直接存大写英文ADMIN、TEACHER、STUDENT。这样和Spring Security默认的ROLE_前缀配合时只需在代码里写hasRole(ADMIN)即可。team.status用于报名审核推荐取值PENDING、APPROVED、REJECTED它是后面授权时的一个重要资源状态底下会细讲。team_member这张关联表决定了谁属于哪个队伍审查资料时通过队伍ID反查成员就不用在用户表里存放冗长的JSON。参数说明如果希望一个学生同时加入多个队伍主键用(team_id, user_id)就够了如果要求一个学生只能参加一项竞赛可以在team_member上加competition_id字段并用联合唯一索引限制同一个人在同一场竞赛只能出现在一个队伍中。我这里没有加competition_id因为队伍表已经通过competition_id确定了竞赛遇到更严格的场景再补。2.3 配置数据源并验证Security初始行为application.yml 在SpringBoot项目里承担自动装配的配置入口。数据源配置错误时项目会启动失败或报无法打开连接Security相关的自动装配也会一起失效。下面是我在本地开发会用的最小配置spring: datasource: url: jdbc:mysql://localhost:3306/contest?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jpa: hibernate: ddl-auto: update # 开发期自动建表上线前改成validate show-sql: true # 联调时打印SQL压测时关闭这里的ddl-auto: update在开发阶段帮我在建表时自动补字段但上线前要改成validate或直接关闭否则生产环境一旦有人改了数据库Hibernate可能自动改结构像竞赛这种要留痕的场景表结构变动是没有审计的。show-sql: true会打印JPA生成的SQL报名高峰期日志量很大本地联调时开着压测时关掉。配置好数据源后启动项目观察控制台日志。你会看到类似Using generated security password: a8c6...的内容说明Security已经接管了所有接口。这时不要急着写登录页面先把第3章的UserDetailsService落地Spring Security才能从数据库里取账号信息而不是每次启动都随机生成密码。这里补充一句SpringBoot自动装配原理的实际应用Spring Boot会读取spring.factories或AutoConfiguration.imports里的配置类Security自动配置类在没有自定义SecurityFilterChainBean时生效。一旦你注册了自己的过滤器链这些默认行为就会被覆盖这也是我们下一步要做的。3. Spring Security 认证从表单登录到JWT无状态接口3.1 SecurityFilterChainSpring Security 6之后唯一的配置入口Spring Security的配置核心不再是继承WebSecurityConfigurerAdapter而是一个SecurityFilterChainBean。这种方法把「哪些路径放行、哪些路径需要认证、用不用Session、是否开启CSRF」集中在一个方法里比之前分散的配置更容易审查。一个适合前后端分离竞赛系统的配置如下Configuration EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter jwtAuthenticationFilter; } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 前后端分离使用JWT关闭CSRF保留Session时不要关 .csrf(csrf - csrf.disable()) // 不创建会话每个请求都通过JWT认证 .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth // 登录和注册接口放行 .requestMatchers(/api/auth/login, /api/register).permitAll() // 管理端接口只允许ADMIN角色 .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) // JWT过滤器放在用户名密码认证过滤器之前 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }逻辑说明csrf(csrf - csrf.disable())表示关闭跨站请求伪造防护因为纯API场景使用了JWT服务端不依赖Cookie会话如果你保留了Session这里一定不能关否则登录后的接口会成为CSRF攻击目标。sessionCreationPolicy(STATELESS)告诉Spring Security不要创建HttpSession每次请求都从Authorization头解析用户信息。requestMatchers(/api/auth/login, /api/register).permitAll()放行登录和注册接口/api/admin/**要求管理员角色其余所有请求都必须登录。参数说明UsernamePasswordAuthenticationFilter.class是一个位置标记Spring Security的过滤器链按顺序执行JWT过滤器放在它前面可以让JWT解析结果先于用户名密码认证生效。注意addFilterBefore的第二个参数是类对象不是实例写错会直接在启动时报ClassCastException。很多老牌教程里会看到configure(AuthenticationManagerBuilder)这种写法那是Spring Security 5.6以前的风格。现在的框架版本里WebSecurityConfigurerAdapter类已经不存在了复制过来的代码需要改成上述Bean形式。这个区别几乎占了SpringBoot/Spring Security面试题中“旧项目怎么升级Security”的一半内容。3.2 从数据库加载用户UserDetailsService与BCrypt密码在学生、教师、管理员共存的系统里UserDetailsService是认证的数据来源。它的返回值是UserDetails而我们的sys_user表里还包含了姓名、创建时间等字段不建议把实体直接返回给Security而是把Security需要的用户名、密码、角色组装进去。下面是一个实现Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; public UserDetailsServiceImpl(UserRepository userRepository) { this.userRepository userRepository; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDO user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(用户不存在)); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) // 密码保持数据库里的BCrypt密文由PasswordEncoder负责比对 .password(user.getPassword()) // 自动加ROLE_前缀 .roles(user.getRole()) // enabled字段控制是否禁用 .disabled(!user.getEnabled()) .build(); } }逻辑说明查询数据库后先判断用户是否存在这是第一道检查随后把密码原样交给Security去比对比对规则由PasswordEncoder决定。roles(user.getRole())会为角色自动加上ROLE_前缀比如数据库中存的是ADMIN最终生成的权限就是ROLE_ADMIN。这样在PreAuthorize里写hasRole(ADMIN)时才能对上。这里有个必须配套完成的BeanBean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }这件事不做的话注册用户时即使密码是明文认证时Security也会用BCrypt去比导致永远登录失败。注册接口里每次保存密码前都要调用passwordEncoder.encode(rawPassword)。参数说明BCryptPasswordEncoder的强度字段默认是10可以传入更高的值比如12但每次哈希计算耗时也会增加。竞赛系统的登录频率不高用默认值就够了。同一个明文密码每次加密结果不同这是因为BCrypt会自动生成随机盐所以不要用等值查询来比密码。3.3 JWT认证过滤器把token解析成Authentication如果采用前后端分离前端登录后拿到一个JWT之后每次请求在Authorization头带上Bearer前缀。服务端不需要Session只需要在过滤器中解析并校验这个token是否由我们的密钥签发。核心过滤器代码如下Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserDetailsServiceImpl userDetailsService; public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsServiceImpl userDetailsService) { this.jwtTokenProvider jwtTokenProvider; this.userDetailsService userDetailsService; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); if (jwtTokenProvider.validateToken(token)) { String username jwtTokenProvider.getUsernameFromToken(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); // 把认证结果放进SecurityContext后续授权判断才能拿到用户信息 SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } }逻辑说明这个过滤器只处理带了Authorization头的请求。token被解析出用户名后我们会重新调用loadUserByUsername从数据库拿最新的用户状态。这样做的代价是多一次查询但好处是用户被封禁或删除时JWT还没过期也能立刻失效如果要追求极致性能可以把角色直接写进token省去这次数据库访问。UsernamePasswordAuthenticationToken构造函数的三个参数分别是principal、credentials和authorities业务代码里通常通过principal拿到当前登录用户。参数说明header.substring(7)是因为Bearer正好7个字符如果前端没按约定加这个前缀解析出来的就是空token会被validateToken拒绝。OncePerRequestFilter保证一个请求只执行一次过滤器避免在转发场景下重复认证。JwtTokenProvider被反复调用它的内部一般包含生成token和验证token两个方法。生成时可以用io.jsonwebtoken.Jwts示例钥匙长度在教程中经常被忽略用HS256算法时密钥长度不能少于256位否则一部分JDK会抛WeakKeyException。这就是为什么很多照着教程敲的学生在看到“密码学弱密钥”报错时一脸茫然。JWT相关参数推荐值说明过期时间7200秒竞赛报名接口操作较多但不要设24小时泄风险会变大密钥至少32字节随机字符串写入application.yml不要硬编码在代码里RefreshToken需要如果支持移动端建议用OAuth2.0的refresh_token方案claim中的角色可选为了实时权限建议不放入角色每次从UserDetails中获取4. Spring Security权限模型落地守住竞赛项目中的报名、审核与评分4.1 RBAC在Spring Security中的两种授权写法竞赛管理系统通常不只有“登录才能访问”还要细化到“学生只能报名和自己有关的队伍评委只能看被分配给自己的作品”。Spring Security提供了两层控制URL级和方法级。URL级在SecurityFilterChain里配适合粗粒度管理比如所有/api/admin/**接口都只有管理员能进方法级通过注解写在业务接口上适合处理“同一资源不同角色不同行为”的场景。在讲解注解之前先明确两个概念的差别。hasRole(ADMIN)会在判断时自动补全ROLE_前缀要求当前用户的权限集合里存在ROLE_ADMIN。hasAuthority(competition:register)则不做任何前缀处理需要权限记录里直接存储competition:register这样的权限代码。两者没有绝对优劣角色适合稳定不常变的功能模块权限码适合需要给不同角色灵活分配操作的复杂系统。表达式写法适合场景hasRolehasRole(ADMIN)管理员后台、教师评审端等固定角色hasAnyRolehasAnyRole(TEACHER,ADMIN)评分接口允许教师和管理员共同访问hasAuthorityhasAuthority(team:register)需要给普通用户单独开放权限码的RBAC实现hasPermissionhasPermission(#teamId,Team)数据级权限需要实现PermissionEvaluator如果只是做课设用hasRole和hasAnyRole就够了能把接口权限说清楚。如果要做到细粒度数据隔离那就要实现PermissionEvaluator工作量会明显上升。4.2 用EnableMethodSecurity和PreAuthorize守护报名接口我建议在Controller方法上写PreAuthorize而不是在Service里用if手工判断角色。因为注解的声明式写法能让权限规则和业务逻辑一起被审查别人接手时看到注解就知道这个接口是给谁用的。SpringBoot 2.7需要在配置类上加EnableGlobalMethodSecurity(prePostEnabled true)而SpringBoot 3.x引入了EnableMethodSecurity它默认开启PreAuthorize和PostAuthorize。下面是一个竞赛报名接口的写法RestController RequestMapping(/api/competition) public class CompetitionController { private final CompetitionService competitionService; public CompetitionController(CompetitionService competitionService) { this.competitionService competitionService; } // 只有学生角色才能报名 PreAuthorize(hasRole(STUDENT)) PostMapping(/{competitionId}/register) public Result register(PathVariable Long competitionId) { LoginUser current (LoginUser) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); return competitionService.register(current.getId(), competitionId); } }逻辑说明PreAuthorize(hasRole(STUDENT))在方法执行前校验当前用户是否拥有学生角色如果不满足Spring Security会直接抛出AccessDeniedException整个方法体不会进入。SecurityContextHolder.getContext().getAuthentication().getPrincipal()是取出当前登录用户的标准写法但要注意这里拿到的对象必须是你之前放进UsernamePasswordAuthenticationToken的UserDetails实例所以强转前要确认类型。参数说明LoginUser建议是自定义的UserDetails实现类里面包含用户ID。不要试图从username去反查用户ID那样会多一次无谓的查询。曾经有同事把Principal理解成前端传进来的用户名结果在RequestParam里定义了一个同名的principal参数导致Spring Security解析混乱这是命名冲突造成的一个很隐性错误。4.3 审核/评分接口的权限边界与403排错竞赛系统的审核流程一般是学生提交队伍材料管理员或教师审核通过评审阶段由教师打分。这三类操作需要的权限完全不同。URL级已经在SecurityConfig里把/api/admin/**限定为管理员那么评委打分的接口可以放在/api/review/**下通过方法级注解控制为教师或管理员。RestController RequestMapping(/api/review) public class ReviewController { private final ReviewService reviewService; public ReviewController(ReviewService reviewService) { this.reviewService reviewService; } // 教师和管理员都可评分 PreAuthorize(hasAnyRole(TEACHER,ADMIN)) PostMapping(/{teamId}/score) public Result score(PathVariable Long teamId, RequestBody ScoreRequest request) { return reviewService.score(teamId, request.getScore()); } }这里的hasAnyRole(TEACHER,ADMIN)表示两种角色都能调用评委本人是不是被分配到这个队伍还需在reviewService.score里再做一次数据级校验。没有数据级权限控制的系统容易出现一个教师看到所有队伍打分结果的越权问题。如果你在排错时遇到403先按下表排查大多数情况不是代码逻辑问题而是配置细节问题症状最常见原因处理方式所有登录请求都403SecurityConfig没放行登录接口检查permitAll是否覆盖了正确路径只有学生403角色前缀对不上数据库role里存了ROLE_吗如果存了用hasAuthority(ROLE_ADMIN)带token也403JWT过滤器没被加入链路检查addFilterBefore的过滤器顺序普通请求偶尔403CSRF未关闭且前端没带tokenAPI环境csrf.disable()方法上PreAuthorize失效没开EnableGlobalMethodSecurity或EnableMethodSecurity在配置类或启动类上补注解排错时不要只盯着接口的返回值先把过滤器的日志级别调到DEBUG在application.yml里设logging.level.org.springframework.securityDEBUG。这样能看到每次请求命中了哪条授权规则。竞赛系统上线前建议把所有角色都跑一遍用不同账号访问同一个接口确认预期中的401/403都是一致的。5. 大学生竞赛管理系统验收技巧用测试和参数校验守住截止时间5.1 用Spring Security Test模拟三种角色接口自测时手动登录再复制token很慢。Spring Security Test提供了WithMockUser可以跳过完整登录流程直接让请求带上指定角色。这在测试PreAuthorize规则时非常高效。在pom.xml中加入spring-boot-starter-test和spring-security-test依赖然后写一个MockMvc测试SpringBootTest AutoConfigureMockMvc public class RegisterApiTest { Autowired private MockMvc mockMvc; Test WithMockUser(username stu01, roles STUDENT) void student_can_register_own_team() throws Exception { mockMvc.perform(post(/api/competition/1/register)) .andExpect(status().isOk()); } Test WithMockUser(username teacher01, roles TEACHER) void teacher_cannot_register_team() throws Exception { mockMvc.perform(post(/api/competition/1/register)) .andExpect(status().isForbidden()); } }WithMockUser里的roles值不需要加ROLE_前缀测试框架会自动补全。这个测试验证的是授权规则不是认证流程如果要测试JWT解析需要自己构造带Authorization头的请求。实测时我还会加一条用管理员账号访问学生报名接口预期403防止未来有人改SecurityConfig时把角色放开。5.2 报名接口的高危参数校验清单竞赛报名接口涉及上传材料与截止时间最大的风险是前端把截止时间改掉后直接提交。常见的做法是在Service里用LocalDateTime.now()对比competition.getRegisterEnd()而不是让页面传入注册时间。团队人数用Min和Max限制上传文件名用UUID重命名后再存盘可以同时避开路径穿越和重名覆盖。还有一个容易被忽略的检查项SpringBoot Actuator的heapdump端点。如果项目引入了spring-boot-starter-actuator生产环境一定要用management.endpoints.web.exposure.includehealth,info限制暴露否则/actuator/heapdump会把堆转储文件泄露出去里面有敏感信息。把这几条写进验收用例截止时间不信任前端、队伍人数边界、文件名校验、Actuator端口关闭。跑完这些用例后再回到浏览器里用学生、教师、管理员三个账号分别走一遍报名和审核流程检查日志里是否出现非预期的匿名访问记录。本文还有配套的精品资源点击获取
返回列表