From 7e2e9bb0bcea8df4111d0dbf972cf651cf0a0500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Sun, 15 Feb 2026 16:43:27 +0800 Subject: [PATCH] fix(security): enforce mini token identity and score authorization --- .../controller/MartialMiniController.java | 237 +++++++++++++++--- .../service/impl/MartialScoreServiceImpl.java | 15 +- .../service/impl/MiniAuthServiceImpl.java | 12 +- .../service/impl/MiniScoringServiceImpl.java | 51 ++-- 4 files changed, 257 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java index 9d88e76..9e46c03 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.tool.api.R; import org.springblade.core.secure.utils.AuthUtil; @@ -57,6 +58,7 @@ import java.util.stream.Collectors; * * @author BladeX */ +@Slf4j @RestController @AllArgsConstructor @RequestMapping("/mini") @@ -96,7 +98,24 @@ public class MartialMiniController extends BladeController { } @PostMapping("/score/submit") @Operation(summary = "提交评分", description = "裁判员提交评分") - public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) { + public R submitScore( + @RequestHeader(value = "Authorization", required = false) String authorization, + @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken, + @RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto + ) { + if (dto == null) { + return R.fail("请求体不能为空"); + } + JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); + if (authContext == null || authContext.getJudgeId() == null) { + return R.fail("登录状态无效或已过期"); + } + Long payloadJudgeId = parseLong(dto.getJudgeId()); + if (payloadJudgeId != null && !payloadJudgeId.equals(authContext.getJudgeId())) { + log.warn("评分提交身份字段不匹配,已忽略请求体judgeId,payloadJudgeId={}, authJudgeId={}", payloadJudgeId, authContext.getJudgeId()); + } + // 向后兼容:保留 judgeId 字段,但强制覆盖为 token 身份 + dto.setJudgeId(String.valueOf(authContext.getJudgeId())); return miniScoringService.submitScore(dto); } @@ -231,6 +250,64 @@ public class MartialMiniController extends BladeController { } } + private JudgeAuthContext resolveJudgeAuthContext(String authorization, String bladeAuthToken) { + String accessToken = resolveAccessToken(authorization, bladeAuthToken); + if (accessToken != null) { + MartialJudgeInvite invite = findValidInviteByAccessToken(accessToken); + if (invite != null && invite.getJudgeId() != null) { + return JudgeAuthContext.fromInvite(invite); + } + } + + BladeUser bladeUser = AuthUtil.getUser(); + if (bladeUser == null || bladeUser.getUserId() == null) { + return null; + } + + MartialJudge judge = judgeService.getById(bladeUser.getUserId()); + if (judge == null) { + log.warn("Blade-Auth用户未绑定评委信息,userId={}", bladeUser.getUserId()); + return null; + } + return JudgeAuthContext.fromJudge(judge); + } + + private String resolveAccessToken(String authorization, String bladeAuthToken) { + String token = extractToken(authorization); + if (token != null) { + return token; + } + return extractToken(bladeAuthToken); + } + + private String extractToken(String tokenHeader) { + if (Func.isEmpty(tokenHeader)) { + return null; + } + String token = tokenHeader.trim(); + if (token.regionMatches(true, 0, "Bearer ", 0, 7)) { + token = token.substring(7).trim(); + } + return Func.isEmpty(token) ? null : token; + } + + private MartialJudgeInvite findValidInviteByAccessToken(String accessToken) { + LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); + inviteQuery.eq(MartialJudgeInvite::getAccessToken, accessToken); + inviteQuery.eq(MartialJudgeInvite::getStatus, 1); + inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery, false); + if (invite == null) { + return null; + } + LocalDateTime now = LocalDateTime.now(); + if (invite.getTokenExpireTime() == null || !invite.getTokenExpireTime().isAfter(now)) { + log.warn("accessToken已过期或无效,token={}, tokenExpireTime={}", accessToken, invite.getTokenExpireTime()); + return null; + } + return invite; + } + /** * 获取选手列表(支持分页) * - 裁判员:获取所有选手,标记是否已评分 @@ -443,10 +520,14 @@ public class MartialMiniController extends BladeController { */ @PostMapping("/logout") @Operation(summary = "退出登录", description = "清除登录状态") - public R logout(@RequestHeader(value = "Authorization", required = false) String token) { + public R logout( + @RequestHeader(value = "Authorization", required = false) String token, + @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken + ) { // 从Redis删除登录缓存 - if (token != null && !token.isEmpty()) { - String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + String accessToken = resolveAccessToken(token, bladeAuthToken); + if (accessToken != null) { + String cacheKey = MINI_LOGIN_CACHE_PREFIX + accessToken; bladeRedis.del(cacheKey); } return R.success("退出成功"); @@ -457,13 +538,23 @@ public class MartialMiniController extends BladeController { */ @GetMapping("/verify") @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态") - public R verify(@RequestHeader(value = "Authorization", required = false) String token) { - if (token == null || token.isEmpty()) { + public R verify( + @RequestHeader(value = "Authorization", required = false) String token, + @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken + ) { + String accessToken = resolveAccessToken(token, bladeAuthToken); + if (accessToken == null) { return R.fail("Token不能为空"); } + MartialJudgeInvite invite = findValidInviteByAccessToken(accessToken); + if (invite == null) { + bladeRedis.del(MINI_LOGIN_CACHE_PREFIX + accessToken); + return R.fail("Token无效或已失效"); + } + // 从Redis获取登录信息 - String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + String cacheKey = MINI_LOGIN_CACHE_PREFIX + accessToken; MiniLoginVO loginInfo = bladeRedis.get(cacheKey); if (loginInfo != null) { @@ -472,23 +563,14 @@ public class MartialMiniController extends BladeController { return R.data(loginInfo); } - // Redis中没有,尝试从数据库恢复 - LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); - inviteQuery.eq(MartialJudgeInvite::getAccessToken, token); - inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); - MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery); - - if (invite == null) { - return R.fail("Token无效"); - } - - if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) { - return R.fail("Token已过期"); - } - // 重建登录信息 MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); MartialJudge judge = judgeService.getById(invite.getJudgeId()); + if (competition == null || judge == null) { + log.warn("Token验证失败:关联比赛或评委不存在,token={}, competitionId={}, judgeId={}", + accessToken, invite.getCompetitionId(), invite.getJudgeId()); + return R.fail("登录信息不存在"); + } MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null; // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目 List projects = new ArrayList<>(); @@ -509,7 +591,7 @@ public class MartialMiniController extends BladeController { // 如果没有场地,projects保持为空列表 MiniLoginVO vo = new MiniLoginVO(); - vo.setToken(token); + vo.setToken(accessToken); String role = invite.getRole(); Integer refereeType = invite.getRefereeType(); if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) { @@ -718,13 +800,33 @@ public class MartialMiniController extends BladeController { */ @PostMapping("/chief/confirm") @Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数") - public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) { - Long resultId = parseLong(dto.getResultId()); - Long chiefJudgeId = parseLong(dto.getChiefJudgeId()); - if (resultId == null || chiefJudgeId == null) { + public R confirmByChiefJudge( + @RequestHeader(value = "Authorization", required = false) String authorization, + @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken, + @RequestBody ChiefJudgeConfirmDTO dto + ) { + if (dto == null) { return R.fail("参数错误"); } - boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote()); + JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); + if (authContext == null || authContext.getJudgeId() == null) { + return R.fail("登录状态无效或已过期"); + } + if (!authContext.isChiefJudge()) { + log.warn("主裁判确认拒绝:无主裁判权限,judgeId={}, role={}, refereeType={}", + authContext.getJudgeId(), authContext.getRole(), authContext.getRefereeType()); + return R.fail("无主裁判权限"); + } + Long resultId = parseLong(dto.getResultId()); + if (resultId == null) { + return R.fail("参数错误"); + } + Long payloadChiefJudgeId = parseLong(dto.getChiefJudgeId()); + if (payloadChiefJudgeId != null && !payloadChiefJudgeId.equals(authContext.getJudgeId())) { + log.warn("主裁判确认身份字段不匹配,已忽略请求体chiefJudgeId,payloadChiefJudgeId={}, authJudgeId={}", + payloadChiefJudgeId, authContext.getJudgeId()); + } + boolean success = resultService.confirmByChiefJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote()); return success ? R.success("确认成功") : R.fail("确认失败"); } @@ -733,13 +835,33 @@ public class MartialMiniController extends BladeController { */ @PostMapping("/general/confirm") @Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数") - public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) { - Long resultId = parseLong(dto.getResultId()); - Long generalJudgeId = parseLong(dto.getGeneralJudgeId()); - if (resultId == null || generalJudgeId == null) { + public R confirmByGeneralJudge( + @RequestHeader(value = "Authorization", required = false) String authorization, + @RequestHeader(value = "Blade-Auth", required = false) String bladeAuthToken, + @RequestBody GeneralJudgeConfirmDTO dto + ) { + if (dto == null) { return R.fail("参数错误"); } - boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote()); + JudgeAuthContext authContext = resolveJudgeAuthContext(authorization, bladeAuthToken); + if (authContext == null || authContext.getJudgeId() == null) { + return R.fail("登录状态无效或已过期"); + } + if (!authContext.isGeneralJudge()) { + log.warn("总裁确认拒绝:无总裁权限,judgeId={}, role={}, refereeType={}", + authContext.getJudgeId(), authContext.getRole(), authContext.getRefereeType()); + return R.fail("无总裁权限"); + } + Long resultId = parseLong(dto.getResultId()); + if (resultId == null) { + return R.fail("参数错误"); + } + Long payloadGeneralJudgeId = parseLong(dto.getGeneralJudgeId()); + if (payloadGeneralJudgeId != null && !payloadGeneralJudgeId.equals(authContext.getJudgeId())) { + log.warn("总裁确认身份字段不匹配,已忽略请求体generalJudgeId,payloadGeneralJudgeId={}, authJudgeId={}", + payloadGeneralJudgeId, authContext.getJudgeId()); + } + boolean success = resultService.confirmByGeneralJudge(resultId, authContext.getJudgeId(), dto.getScore(), dto.getNote()); return success ? R.success("确认成功") : R.fail("确认失败"); } @@ -976,6 +1098,57 @@ public class MartialMiniController extends BladeController { return R.data(result); } + private static class JudgeAuthContext { + private final Long judgeId; + private final String role; + private final Integer refereeType; + + private JudgeAuthContext(Long judgeId, String role, Integer refereeType) { + this.judgeId = judgeId; + this.role = role; + this.refereeType = refereeType; + } + + static JudgeAuthContext fromInvite(MartialJudgeInvite invite) { + return new JudgeAuthContext(invite.getJudgeId(), invite.getRole(), invite.getRefereeType()); + } + + static JudgeAuthContext fromJudge(MartialJudge judge) { + Integer refereeType = judge.getRefereeType(); + String role; + if (refereeType != null && refereeType == 3) { + role = "general_judge"; + } else if (refereeType != null && refereeType == 1) { + role = "chief_judge"; + } else { + role = "judge"; + } + return new JudgeAuthContext(judge.getId(), role, refereeType); + } + + Long getJudgeId() { + return judgeId; + } + + String getRole() { + return role; + } + + Integer getRefereeType() { + return refereeType; + } + + boolean isChiefJudge() { + return "chief_judge".equals(role) || (refereeType != null && refereeType == 1); + } + + boolean isGeneralJudge() { + return "general_judge".equals(role) + || "general".equals(role) + || (refereeType != null && refereeType == 3); + } + } + private String generateTableNo(ScheduleGroupDetailVO detail) { // 场地编号(简单取第一个数字或默认1) int venueNo = 1; diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScoreServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScoreServiceImpl.java index 5501b77..3a84cdb 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScoreServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScoreServiceImpl.java @@ -79,15 +79,18 @@ public class MartialScoreServiceImpl extends ServiceImpl login(MiniLoginDTO dto) { + LocalDateTime now = LocalDateTime.now(); LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode()); inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + inviteQuery.eq(MartialJudgeInvite::getStatus, 1); MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery); if (invite == null) { - return R.fail("邀请码不存在"); + log.warn("小程序登录失败:邀请码不存在或已禁用,inviteCode={}", dto.getInviteCode()); + return R.fail("邀请码不存在或已失效"); } - if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) { + if (invite.getExpireTime() == null || !invite.getExpireTime().isAfter(now)) { + log.warn("小程序登录失败:邀请码已过期,inviteCode={}, expireTime={}", dto.getInviteCode(), invite.getExpireTime()); return R.fail("邀请码已过期"); } @@ -64,9 +68,9 @@ public class MiniAuthServiceImpl implements IMiniAuthService { String token = UUID.randomUUID().toString().replace("-", ""); invite.setAccessToken(token); - invite.setTokenExpireTime(LocalDateTime.now().plusDays(7)); + invite.setTokenExpireTime(now.plusDays(7)); invite.setIsUsed(1); - invite.setUseTime(LocalDateTime.now()); + invite.setUseTime(now); invite.setLoginIp(dto.getLoginIp()); invite.setDeviceInfo(dto.getDeviceInfo()); judgeInviteService.updateById(invite); diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MiniScoringServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MiniScoringServiceImpl.java index 70197d2..1d9aaa7 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MiniScoringServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MiniScoringServiceImpl.java @@ -36,15 +36,43 @@ public class MiniScoringServiceImpl implements IMiniScoringService { @Override public R submitScore(MiniScoreSubmitDTO dto) { + if (dto == null) { + return R.fail("评分参数不能为空"); + } + + Long athleteId = parseLong(dto.getAthleteId()); + Long judgeId = parseLong(dto.getJudgeId()); + Long projectId = parseLong(dto.getProjectId()); + Long competitionId = parseLong(dto.getCompetitionId()); + Long venueId = parseLong(dto.getVenueId()); + Long scheduleId = parseLong(dto.getScheduleId()); + if (athleteId == null || judgeId == null || projectId == null || competitionId == null || dto.getScore() == null) { + log.warn("评分提交参数缺失,athleteId={}, judgeId={}, projectId={}, competitionId={}", + dto.getAthleteId(), dto.getJudgeId(), dto.getProjectId(), dto.getCompetitionId()); + return R.fail("评分参数不完整"); + } + + MartialJudge judge = judgeService.getById(judgeId); + if (judge == null) { + log.warn("评分提交失败:评委不存在,judgeId={}", judgeId); + return R.fail("评委信息不存在"); + } + + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete == null) { + log.warn("评分提交失败:选手不存在,athleteId={}", athleteId); + return R.fail("选手信息不存在"); + } + MartialScore score = new MartialScore(); - score.setAthleteId(parseLong(dto.getAthleteId())); - score.setJudgeId(parseLong(dto.getJudgeId())); + score.setAthleteId(athleteId); + score.setJudgeId(judgeId); score.setScore(dto.getScore()); - score.setProjectId(parseLong(dto.getProjectId())); - score.setCompetitionId(parseLong(dto.getCompetitionId())); - score.setVenueId(parseLong(dto.getVenueId())); - score.setScheduleId(parseLong(dto.getScheduleId())); + score.setProjectId(projectId); + score.setCompetitionId(competitionId); + score.setVenueId(venueId); + score.setScheduleId(scheduleId); score.setNote(dto.getNote()); score.setScoreTime(LocalDateTime.now()); @@ -56,20 +84,11 @@ public class MiniScoringServiceImpl implements IMiniScoringService { score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds)); } - Long judgeId = parseLong(dto.getJudgeId()); - if (judgeId != null) { - MartialJudge judge = judgeService.getById(judgeId); - if (judge != null) { - score.setJudgeName(judge.getName()); - } - } + score.setJudgeName(judge.getName()); boolean success = scoreService.save(score); if (success) { - Long athleteId = parseLong(dto.getAthleteId()); - Long projectId = parseLong(dto.getProjectId()); - Long venueId = parseLong(dto.getVenueId()); if (athleteId != null && projectId != null) { updateAthleteTotalScore(athleteId, projectId, venueId); }