This commit is contained in:
zlwu committed 2026-09-22 19:22:18 +08:00
1 parent 5fd0042068
commit 1e6dcb164c
94 files changed
+796 -312

No files matched your search

+3 -3
View File
@@ -80,7 +80,7 @@
<profiles.active>dev</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
<monitor.password>HzdyjsMon@Dev#9Kx7mQ2pL</monitor.password>
</properties>
<activation>
<!-- 默认环境 -->
@@ -94,7 +94,7 @@
<profiles.active>prod-local</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
<monitor.password>HzdyjsMon@Prod#TclhloNMWuVjq</monitor.password>
</properties>
</profile>
<profile>
@@ -103,7 +103,7 @@
<profiles.active>prod</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
<monitor.password>HzdyjsMon@Prod#TclhloNMWuVjq</monitor.password>
</properties>
</profile>
</profiles>
+3 -1
View File
@@ -80,11 +80,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再打入运行包(含 /demo/mail、/demo/queue 等) -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
@@ -21,6 +21,8 @@ import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.utils.*;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
@@ -41,6 +43,7 @@ import org.dromara.web.domain.vo.TenantListVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.SysRegisterService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -73,6 +76,12 @@ public class AuthController {
private final ISysClientService clientService;
private final ScheduledExecutorService scheduledExecutorService;
/**
* 配置层注册总开关,默认 false(优先于数据库 sys.account.registerUser)
*/
@Value("${user.register.enabled:false}")
private boolean registerEnabled;
/**
* 登录方法
*
@@ -119,6 +128,7 @@ public class AuthController {
@GetMapping("/binding/{source}")
public R<String> authBinding(@PathVariable("source") String source,
@RequestParam("tenantId") String tenantId, @RequestParam("domain") String domain) {
loginService.checkTenant(tenantId);
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
if (ObjectUtil.isNull(obj)) {
return R.fail(source + "平台账号暂不支持");
@@ -178,11 +188,16 @@ public class AuthController {
}
/**
* 用户注册
* 用户注册(免登接口;默认关闭,需同时打开配置与数据库开关)
*/
@ApiEncrypt
@RateLimiter(time = 60, count = 5, limitType = LimitType.IP)
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
// P6:配置层硬关闭,避免仅依赖库开关被后台误开后遭滥用
if (!registerEnabled) {
return R.fail("当前系统没有开启注册功能!");
}
// 先校验租户(含格式白名单),禁止脏 tenantId 进入 selectRegisterEnabled 触发 SQL 注入
loginService.checkTenant(user.getTenantId());
if (!configService.selectRegisterEnabled(user.getTenantId())) {
@@ -68,9 +68,11 @@ public class CasController {
CAS_BASE, CLIENT_ID, CLIENT_SECRET, URLEncoder.encode(code, StandardCharsets.UTF_8.toString()),
REDIRECT_URI_ENCODED);
log.info("请求 CAS accessToken: {}", url);
// 禁止打印 client_secret、完整换票 URL、access_token
log.info("请求 CAS accessToken: base={}, client_id={}, codeLen={}", CAS_BASE, CLIENT_ID,
code != null ? code.length() : 0);
String resp = HttpUtil.get(url);
log.info("CAS accessToken 响应: {}", resp);
log.info("CAS accessToken 响应已收到: respLen={}", resp != null ? resp.length() : 0);
// 尝试解析 access_token(CAS 可能返回 JSON 或 query-string 风格)
String accessToken = null;
@@ -96,9 +98,10 @@ public class CasController {
// 使用 access_token 请求 profile 接口
String profileUrl = String.format("%s/cas/oauth2.0/profile?access_token=%s", CAS_BASE,
URLEncoder.encode(accessToken, StandardCharsets.UTF_8.toString()));
log.info("请求 CAS profile: {}", profileUrl);
// 禁止打印 access_token 与完整 profile
log.info("请求 CAS profile: base={}", CAS_BASE);
String profileResp = HttpUtil.get(profileUrl);
log.info("CAS profile 响应: {}", profileResp);
log.info("CAS profile 响应已收到: respLen={}", profileResp != null ? profileResp.length() : 0);
// 解析 profile,尝试从 attributes 中找到 CODE(用户名) 和 TENANTID
try {
@@ -178,7 +181,7 @@ public class CasController {
return R.fail("处理 CAS profile 失败: " + e.getMessage());
}
} else {
return R.fail("无法解析 CAS access_token,原始响应: " + resp);
return R.fail("无法解析 CAS access_token");
}
} catch (Exception e) {
log.error("请求 CAS accessToken 失败", e);
@@ -225,14 +225,16 @@ public class SysLoginService {
* @param tenantId 租户ID
*/
public void checkTenant(String tenantId) {
// 只要传入了 tenantId,无论租户开关是否开启都先做格式白名单(防注入)
if (StringUtils.isNotBlank(tenantId)) {
TenantHelper.checkTenantIdFormat(tenantId);
}
if (!TenantHelper.isEnable()) {
return;
}
if (StringUtils.isBlank(tenantId)) {
throw new TenantException("tenant.number.not.blank");
}
// 格式白名单,阻断恶意 tenantId 进入后续查询/动态租户
TenantHelper.checkTenantIdFormat(tenantId);
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
return;
}
@@ -22,6 +22,7 @@ import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
@@ -37,11 +38,19 @@ public class SysRegisterService {
private final SysUserMapper userMapper;
private final CaptchaProperties captchaProperties;
@Value("${user.register.enabled:false}")
private boolean registerEnabled;
/**
* 注册
*/
public void register(RegisterBody registerBody) {
if (!registerEnabled) {
throw new UserException("user.register.disabled");
}
String tenantId = registerBody.getTenantId();
// 动态租户查询前校验格式,防止绕过 Controller 时注入
TenantHelper.checkTenantIdFormat(tenantId);
String username = registerBody.getUsername();
String password = registerBody.getPassword();
// 校验用户类型是否存在
@@ -1,7 +1,19 @@
--- # 监控中心配置
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: ""
endpoint:
health:
enabled: false
heapdump:
enabled: false
--- # 监控中心配置(不使用,关闭客户端)
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
@@ -74,8 +86,8 @@ spring:
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
# 增加锁等待超时参数,减少锁冲突
url: jdbc:mysql://47.99.164.66:3306/hzdyjs_xg?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&innodb_lock_wait_timeout=120&autocommit=true
username: root
password: hn02le.34lkdLKD
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
# # 从库数据源
# slave:
# lazy: true
@@ -130,7 +142,7 @@ spring.data:
# 数据库索引
database: 8
# redis 密码必须配置
password: Hzdyjs@123456
password: "Fwd7y-8UDlZ3%3VO8kwn%J1T"
# 连接超时时间
timeout: 30s
# 是否开启ssl
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -18,7 +18,7 @@ logging:
# 监控中心配置 - 端口漂移
spring.boot.admin.client:
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -1,10 +1,22 @@
--- # 临时文件存储位置 避免临时文件被系统清理报错
spring.servlet.multipart.location: /ruoyi/server/temp
--- # 监控中心配置
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: ""
endpoint:
health:
enabled: false
heapdump:
enabled: false
--- # 监控中心配置(不使用,关闭客户端)
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://47.121.125.41:9090/admin
instance:
service-host-type: IP
@@ -53,8 +65,8 @@ spring:
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://47.99.164.66:3306/hzdyjs_xg?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: hn02le.34lkdLKD
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
# # 从库数据源
# slave:
# lazy: true
@@ -107,7 +119,7 @@ spring.data:
# 数据库索引
database: 3
# redis 密码必须配置
password: Hzdyjs@123456
password: "Fwd7y-8UDlZ3%3VO8kwn%J1T"
# 连接超时时间
timeout: 10s
# 是否开启ssl
@@ -1,10 +1,22 @@
--- # 临时文件存储位置 避免临时文件被系统清理报错
spring.servlet.multipart.location: /ruoyi/server/temp
--- # 监控中心配置
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: ""
endpoint:
health:
enabled: false
heapdump:
enabled: false
--- # 监控中心配置(不使用,关闭客户端)
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -53,8 +65,8 @@ spring:
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://172.16.0.67:3306/hzdyjs_xg?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: Hzdyjs@Root@123456
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
# # 从库数据源
# slave:
# lazy: true
@@ -58,6 +58,9 @@ logging:
# 用户配置
user:
# 开放注册总开关(P6:校园场景默认关闭,防止免登 /auth/register 被滥用)
register:
enabled: false
password:
# 密码最大错误次数
maxRetryCount: 5
@@ -125,7 +128,7 @@ sa-token:
# security配置
security:
# 排除路径
# 排除路径(P0:已下线 Swagger/api-docs,不再加入白名单)
excludes:
- /*.html
- /**/*.html
@@ -133,20 +136,10 @@ security:
- /**/*.js
- /favicon.ico
- /error
- /*/api-docs
- /*/api-docs/**
- /api-docs
- /api-docs/**
- /swagger-ui/**
- /swagger-ui.html
- /swagger-ui/index.html
- /v3/api-docs/**
- /webjars/**
- /warm-flow-ui/config
# 招聘门户公开接口 weizhong
- /rs/job/public/**
- /rs/job/view/**
- /rs/application/submit
- /system/dict/data/type/*
# 多租户配置
@@ -216,50 +209,25 @@ api-decrypt:
# 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
# P0:Swagger / api-docs 已下线,禁止对外暴露接口清单
springdoc:
api-docs:
# 是否开启接口文档
enabled: true
# 接口文档路径
enabled: false
path: /api-docs
swagger-ui:
# 启用 Swagger UI
enabled: true
# Swagger UI 路径
enabled: false
path: /swagger-ui.html
info:
# 标题
title: '标题:杭州第一技师学院智慧校园管理系统_接口文档'
# 描述
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
# 版本
description: '描述:接口文档已关闭'
version: '版本号: ${ruoyi.version}'
# 作者信息
contact:
name: Lion Li
email: crazylionli@163.com
url: https://gitee.com/dromara/RuoYi-Vue-Plus
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
#这里定义了两个分组,可定义多个,也可以不定义
group-configs:
- group: 1.演示模块
packages-to-scan: org.dromara.demo
- group: 2.通用模块
packages-to-scan: org.dromara.web
- group: 3.系统模块
packages-to-scan: org.dromara.system
- group: 4.代码生成模块
packages-to-scan: org.dromara.generator
- group: 5.工作流模块
packages-to-scan: org.dromara.workflow
- group: 6.所有业务模块
packages-to-scan: org.dromara
group-configs: []
# 防止XSS攻击
xss:
@@ -286,15 +254,18 @@ lock4j:
# 分布式锁的超时时间,默认为 30 秒
expire: 30000
--- # Actuator 监控端点的配置项
--- # Actuator:不使用监控,关闭全部 Web 端点(含 heapdump)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
include: ""
endpoint:
health:
show-details: ALWAYS
enabled: false
heapdump:
enabled: false
logfile:
external-file: ./logs/sys-console.log
@@ -25,6 +25,7 @@ user.login.success=登录成功
user.register.success=注册成功
user.register.save.error=保存用户 {0} 失败,注册账号已存在
user.register.error=注册失败,请联系系统管理人员
user.register.disabled=当前系统没有开启注册功能!
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
@@ -25,6 +25,7 @@ user.login.success=Login successful
user.register.success=Register successful
user.register.save.error=Failed to save user {0}, The registered account already exists
user.register.error=Register failed, please contact system administrator
user.register.disabled=User registration is disabled
user.notfound=Please login again
user.forcelogout=The administrator is forced to exit,please login again
user.unknown.error=Unknown error, please login again
@@ -25,6 +25,7 @@ user.login.success=登录成功
user.register.success=注册成功
user.register.save.error=保存用户 {0} 失败,注册账号已存在
user.register.error=注册失败,请联系系统管理人员
user.register.disabled=当前系统没有开启注册功能!
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
@@ -3,6 +3,7 @@ package org.dromara.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 程序注解配置
@@ -12,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
@AutoConfiguration
@EnableAspectJAutoProxy
@EnableAsync(proxyTargetClass = true)
@EnableScheduling
public class ApplicationConfig {
}
@@ -1,6 +1,7 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serial;
@@ -31,8 +32,9 @@ public class LoginBody implements Serializable {
private String grantType;
/**
* 租户ID
* 租户ID(仅允许纯数字,防止拼进 SQL 导致注入)
*/
@Pattern(regexp = "^[0-9]{1,20}$", message = "{tenant.id.invalid}")
private String tenantId;
/**
@@ -10,17 +10,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 启动定时任务
* 启动 SnailJob 客户端
* <p>Spring {@code @Scheduled} 已由 {@code ApplicationConfig} 统一开启,不再依赖本配置。</p>
*
* @author opensnail
* @date 2024-05-17
*/
@AutoConfiguration
@ConditionalOnProperty(prefix = "snail-job", name = "enabled", havingValue = "true")
@EnableScheduling
@EnableSnailJob
public class SnailJobConfig {
@@ -160,11 +160,13 @@ public class TenantHelper {
* @param global 是否全局生效
*/
public static void setDynamic(String tenantId, boolean global) {
// 传入非空时一律先校验格式,避免租户开关关闭时仍把脏数据带入后续逻辑
if (StringUtils.isNotBlank(tenantId)) {
checkTenantIdFormat(tenantId);
}
if (!isEnable()) {
return;
}
// 动态租户一律做格式白名单校验,覆盖 login/register/config 等所有 TenantHelper.dynamic 入口
checkTenantIdFormat(tenantId);
if (!LoginHelper.isLogin() || !global) {
TEMP_DYNAMIC_TENANT.set(tenantId);
return;
@@ -21,22 +21,25 @@ spring:
title: RuoYi-Vue-Plus服务监控中心
context-path: /admin
--- # Actuator 监控端点的配置项
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
include: ""
endpoint:
health:
show-details: ALWAYS
enabled: false
heapdump:
enabled: false
logfile:
external-file: ./logs/ruoyi-monitor-admin.log
--- # 监控配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
# 设置 Spring Boot Admin Server 地址
url: http://47.121.125.41:9090/admin
instance:
@@ -23,22 +23,25 @@ spring:
# 配置公网访问地址,确保重定向和静态资源路径包含正确的端口号
# public-url: http://web.hzdyjsxy.cn:18081/admin
--- # Actuator 监控端点的配置项
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
include: ""
endpoint:
health:
show-details: ALWAYS
enabled: false
heapdump:
enabled: false
logfile:
external-file: ./logs/ruoyi-monitor-admin.log
--- # 监控配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
# 设置 Spring Boot Admin Server 地址
url: http://172.16.0.67:9090/admin
instance:
@@ -21,22 +21,25 @@ spring:
title: RuoYi-Vue-Plus服务监控中心
context-path: /admin
--- # Actuator 监控端点的配置项
--- # Actuator 全关(不使用)
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
include: ""
endpoint:
health:
show-details: ALWAYS
enabled: false
heapdump:
enabled: false
logfile:
external-file: ./logs/ruoyi-monitor-admin.log
--- # 监控配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
# 设置 Spring Boot Admin Server 地址
url: http://127.0.0.1:9090/admin
instance:
@@ -3,8 +3,8 @@ spring:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.99.164.66:3306/hzdyjs_xg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: hn02le.34lkdLKD
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
hikari:
connection-timeout: 30000
validation-timeout: 5000
@@ -29,7 +29,7 @@ snail-job:
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
@@ -3,8 +3,8 @@ spring:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.99.164.66:3306/hzdyjs_xg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: hn02le.34lkdLKD
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
hikari:
connection-timeout: 30000
validation-timeout: 5000
@@ -29,7 +29,7 @@ snail-job:
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://47.121.125.41:9090/admin
instance:
service-host-type: IP
@@ -3,8 +3,8 @@ spring:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.16.0.67:3306/hzdyjs_xg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Hzdyjs@Root@123456
username: hzdyjs_app
password: Hzdyjs@App#n4SQi8AfPHldNj49
hikari:
connection-timeout: 30000
validation-timeout: 5000
@@ -29,7 +29,7 @@ snail-job:
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
@@ -29,11 +29,14 @@ logging:
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
include: ""
endpoint:
health:
show-details: ALWAYS
enabled: false
heapdump:
enabled: false
logfile:
external-file: ./logs/ruoyi-snailjob-server/console.log
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
@@ -174,4 +174,11 @@ public interface IHqSatSurveyService {
* @param response HTTP响应
*/
void exportSurveyRawData(Long surveyId, jakarta.servlet.http.HttpServletResponse response);
/**
* 将已过期问卷状态更新为已结束(status=2)
*
* @return 更新条数
*/
int updateExpiredSurveys();
}
@@ -6,7 +6,9 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.dromara.common.tenant.helper.TenantHelper;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -1741,4 +1743,21 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
public java.util.List<java.util.Map<String, Object>> getAnswers() { return answers; }
public void setAnswers(java.util.List<java.util.Map<String, Object>> answers) { this.answers = answers; }
}
/**
* 将 end_time 已过期且未结束的问卷标记为已结束。
* 定时任务无登录上下文,需忽略租户插件,覆盖全部租户。
*/
@Override
public int updateExpiredSurveys() {
Date now = new Date();
return TenantHelper.ignore(() -> {
LambdaUpdateWrapper<HqSatSurvey> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.set(HqSatSurvey::getStatus, "2")
.set(HqSatSurvey::getUpdateTime, now)
.lt(HqSatSurvey::getEndTime, now)
.ne(HqSatSurvey::getStatus, "2");
return baseMapper.update(null, updateWrapper);
});
}
}
@@ -0,0 +1,46 @@
package org.dromara.hq.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.hq.service.IHqSatSurveyService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 满意度调查问卷过期扫描任务
* <p>替代原 SnailJob surveyEndTimeJobExecutor:每小时将 end_time 已过期的问卷 status 更新为 2(已结束)。</p>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HqSatSurveyExpireTask {
private static final String LOCK_KEY = "hq:sat:survey:expire:lock";
private static final Duration LOCK_TTL = Duration.ofMinutes(10);
private final IHqSatSurveyService satSurveyService;
/**
* 每小时整点执行(与原 SnailJob 3600 秒间隔对齐)
*/
@Scheduled(cron = "0 0 * * * ?")
public void updateExpiredSurveys() {
boolean locked = RedisUtils.setObjectIfAbsent(LOCK_KEY, "1", LOCK_TTL);
if (!locked) {
log.debug("满意度调查过期扫描跳过:其他实例正在执行");
return;
}
try {
log.info("开始执行满意度调查过期扫描");
int updatedCount = satSurveyService.updateExpiredSurveys();
log.info("满意度调查过期扫描完成,更新数量: {}", updatedCount);
} catch (Exception e) {
log.error("满意度调查过期扫描失败", e);
} finally {
RedisUtils.deleteObject(LOCK_KEY);
}
}
}
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
-6
View File
@@ -27,12 +27,6 @@
<artifactId>ruoyi-common-job</artifactId>
</dependency>
<!-- 添加spring-jdbc依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,44 +0,0 @@
package org.dromara.job.snailjob;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.core.util.JsonUtil;
import com.aizuda.snailjob.common.log.SnailJobLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 满意度调查问卷结束时间扫描任务
* 扫描hq_sat_survey表,检查end_time是否已小于当前时间
* 若已过期,则将状态(status)更新为2
*/
@Component
@JobExecutor(name = "surveyEndTimeJobExecutor")
public class SurveyEndTimeJobExecutor {
@Autowired
private SurveyJobService surveyJobService;
public ExecuteResult jobExecute(JobArgs jobArgs) {
SnailJobLog.LOCAL.info("surveyEndTimeJobExecutor start. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
SnailJobLog.REMOTE.info("surveyEndTimeJobExecutor start. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
try {
// 执行扫描和状态更新
int updatedCount = surveyJobService.updateExpiredSurveys();
String resultMsg = "满意度调查过期状态更新完成,更新数量: " + updatedCount;
SnailJobLog.LOCAL.info(resultMsg);
SnailJobLog.REMOTE.info(resultMsg);
return ExecuteResult.success(resultMsg);
} catch (Exception e) {
String errorMsg = "满意度调查过期状态更新失败: " + e.getMessage();
SnailJobLog.LOCAL.error(errorMsg, e);
SnailJobLog.REMOTE.error(errorMsg, e);
return ExecuteResult.failure(errorMsg);
}
}
}
@@ -1,56 +0,0 @@
package org.dromara.job.snailjob;
import org.dromara.common.core.enums.FormatsType;
import org.dromara.common.core.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* 满意度调查问卷结束时间扫描服务
* 提供扫描过期调查问卷并更新状态的功能
*/
@Service
public class SurveyJobService {
/**
* 满意度调查问卷表名
*/
private static final String TABLE_NAME = "hq_sat_survey";
/**
* 已结束状态值
*/
private static final int STATUS_ENDED = 2;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 更新过期的满意度调查问卷状态
* 检查end_time小于当前时间的记录,将status更新为2
*
* @return 更新的记录数
*/
@Transactional(rollbackFor = Exception.class)
public int updateExpiredSurveys() {
Date now = DateUtils.getNowDate();
// 构建更新SQL
String updateSql = "UPDATE " + TABLE_NAME +
" SET status = ?, update_time = ? " +
" WHERE end_time < ? AND status != ? AND deleted = 0";
// 执行更新并返回更新的记录数
return jdbcTemplate.update(
updateSql,
STATUS_ENDED,
DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, now),
DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, now),
STATUS_ENDED
);
}
}
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
@@ -24,8 +24,8 @@ import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.exception.ServiceException;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.rs.domain.RsApplication;
import org.dromara.rs.service.IRsApplicationService;
@@ -292,9 +292,8 @@ public class RsApplicationController extends BaseController {
}
/**
* 提交应聘申请(公开接口)
* 提交应聘申请(须登录)
*/
@SaIgnore
@Log(title = "提交应聘申请", businessType = BusinessType.INSERT)
@PostMapping("/submit")
public R<Map<String, Object>> submitApplication(@RequestBody RsApplication application) {
@@ -307,9 +306,8 @@ public class RsApplicationController extends BaseController {
// 设置申请时间
application.setApplyTime(new java.util.Date());
// 对于匿名用户,applicant_id 设置为 null
// 如果将来需要支持已登录用户,可以从 token 中获取用户ID
application.setApplicantId(null);
// 绑定当前登录用户
application.setApplicantId(LoginHelper.getUserId());
// 根据jobId设置职位标题
if (application.getJobId() != null) {
@@ -12,7 +12,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.annotation.SaCheckPermission;
/**
* 人事管理初始化控制器
@@ -116,9 +116,9 @@ public class RsInitController extends BaseController {
}
/**
* 更新菜单数据(不需要认证)
* 更新菜单数据(须登录且具备菜单编辑权限)
*/
@SaIgnore
@SaCheckPermission("system:menu:edit")
@PostMapping("/update-menu")
@Log(title = "更新人事管理菜单", businessType = BusinessType.UPDATE)
public R<Void> updateMenu() {
@@ -83,6 +83,8 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
*/
@Override
public boolean selectRegisterEnabled(String tenantId) {
// 防御纵深:动态租户查询前再次校验,避免绕过 Controller 时注入
TenantHelper.checkTenantIdFormat(tenantId);
SysConfig retConfig = TenantHelper.dynamic(tenantId, () -> {
return baseMapper.selectOne(new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getConfigKey, "sys.account.registerUser"));
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
+3 -1
View File
@@ -92,11 +92,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<!-- P0:demo 模块已下线,不再依赖 -->
<!--
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
-->
<!-- 工作流模块 -->
<dependency>
@@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
@@ -103,9 +102,8 @@ public class ZsjyEnrollmentInfoController extends BaseController {
}
/**
* 根据身份证号和准考证号查询报名信息
* 根据身份证号和准考证号查询报名信息(须登录)
*/
@SaIgnore
@GetMapping("/query")
public R<ZsjyEnrollmentInfoVo> queryByIdCardAndExamNumber(
@RequestParam(value = "idCard", required = false) String idCard,
@@ -115,9 +113,8 @@ public class ZsjyEnrollmentInfoController extends BaseController {
}
/**
* 提交报名信息(供学生端使用)
* 提交报名信息(供学生端使用,须登录)
*/
@SaIgnore
@Log(title = "学生报名", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/submit")
@@ -126,9 +123,8 @@ public class ZsjyEnrollmentInfoController extends BaseController {
}
/**
* 查询我的报名状态(供学生端使用)
* 查询我的报名状态(供学生端使用,须登录)
*/
@SaIgnore
@GetMapping("/myStatus")
public R<ZsjyEnrollmentInfoVo> getMyEnrollmentStatus(
@RequestParam("idCard") String idCard,
@@ -160,9 +156,8 @@ public class ZsjyEnrollmentInfoController extends BaseController {
}
/**
* 获取学院列表(供移动端公开访问)
* 获取学院列表(须登录)
*/
@SaIgnore
@GetMapping("/colleges")
public R<List<ZsjyEnrollmentDeptVo>> getColleges() {
try {
@@ -174,9 +169,8 @@ public class ZsjyEnrollmentInfoController extends BaseController {
}
/**
* 根据学院ID获取专业列表(供移动端公开访问)
* 根据学院ID获取专业列表(须登录)
*/
@SaIgnore
@GetMapping("/majors/{collegeId}")
public R<List<ZsjyEnrollmentDeptVo>> getMajorsByCollegeId(@PathVariable("collegeId") Long collegeId) {
try {
@@ -2,8 +2,8 @@
## 文档信息
- **版本**:1.0.0
- **最后更新**:2025-07-01
- **版本**:1.0.1
- **最后更新**:2026-09-20
- **作者**:Cursor AI
- **分类**:后勤管理模块
@@ -11,6 +11,13 @@
本文档详细说明后勤满意度调查系统的API接口设计,包括问卷管理、题目管理、选项管理、答题记录等功能。系统采用前后端分离架构,接口采用RESTful API风格设计,所有接口均需通过Token进行身份验证。
### 问卷过期处理(2026-09-20)
- 原依赖 SnailJob 任务 `surveyEndTimeJob` / `surveyEndTimeJobExecutor` 每小时扫描过期问卷。
- 已改为 Spring `@Scheduled`:`HqSatSurveyExpireTask`(cron:`0 0 * * * ?`),将 `hq_sat_survey` 中 `end_time` 已过期且未结束的记录 `status` 更新为 `2`。
- 多实例通过 Redis 锁 `hq:sat:survey:expire:lock` 避免重复执行。
- 测试库已关闭对应 SnailJob 任务(`job_status=0`),避免与 `@Scheduled` 双跑。
## 通用响应格式
### 成功响应
+1 -1
View File
@@ -649,4 +649,4 @@
1. 职位申请提交前需要先保存应聘者信息,获取applicantId
2. 职位状态更改会影响前台展示,下线的职位不会在公开列表中展示
3. 所有API请求需要在Header中附带Token
4. 公开的API(如public/list、public/{jobId})可以通过权限配置允许匿名访问
4. 公开的API(如public/list、public/{jobId})可以通过权限配置允许匿名访问;`POST /rs/application/submit` 须登录后调用
@@ -331,8 +331,8 @@ DEFAULT_TENANT_ID = 000000
## 7. 后续优化建议
1. **配置外置化**:将 CAS 地址、client_id、client_secret、redirect_uri 移至 `application.yml`,避免硬编码
2. **密钥安全**:前端不应保留 `CAS_CLIENT_SECRET`(当前未使用但存在代码中,建议删除)
1. **配置外置化**:将 CAS 地址、client_id、client_secret、redirect_uri 移至 `application.yml`,避免硬编码(后端 secret 仍待外置)
2. **密钥安全**:~~前端不应保留 `CAS_CLIENT_SECRET`~~(已删除;换票仅走后端;CAS 相关日志已脱敏,禁止打印 secret/token/profile)
3. **租户映射**:如需支持多租户,可恢复对 CAS `TENANTID` 的解析逻辑
4. **用户自动注册**:CAS 用户首次登录时自动创建本地账号(需评估权限与安全策略)
5. **移动端接入**:mobile 工程可参考相同 OAuth2 流程实现统一认证登录
+1 -1
View File
@@ -2,4 +2,4 @@
url: jdbc:mysql://47.99.164.66:3306/hzdyjs_xg
username: root
password: hn02le.34lkdLKD
password: nsyOfyOyoFCr%Brj-k_5PqbW
@@ -0,0 +1,27 @@
-- 宿舍卫生检查扣分明细表
-- 生产现象:提交宿舍卫生检查时报 Table 'hzdyjs_xg.xg_dorm_hygiene_check_deduction' doesn't exist
-- 原因:代码在保存主表后会先逻辑删除再写入扣分明细,生产库未建该表,整单事务回滚
-- 执行库:hzdyjs_xg
CREATE TABLE IF NOT EXISTS `xg_dorm_hygiene_check_deduction` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`hygiene_check_id` bigint(20) NOT NULL COMMENT '卫生检查ID,关联xg_dorm_hygiene_check.id',
`discipline_type_id` bigint(20) NOT NULL COMMENT '违纪类型ID,关联xskh_discipline_type.id',
`discipline_type_name` varchar(100) DEFAULT NULL COMMENT '违纪类型名称',
`score` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '该项扣分',
`sort_order` int(11) DEFAULT '1' COMMENT '排序序号',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_hygiene_check_id` (`hygiene_check_id`),
KEY `idx_discipline_type_id` (`discipline_type_id`),
KEY `idx_tenant_deleted` (`tenant_id`,`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍卫生检查扣分明细表';
+10 -25
View File
@@ -1,4 +1,4 @@
// 移动端新生报名相关API接口
// 移动端新生报名相关API接口(须登录,需携带 Token)
import { request } from '@/common/request.js'
/**
@@ -6,37 +6,31 @@ import { request } from '@/common/request.js'
*/
export const enrollmentApi = {
/**
* 提交报名信息(供学生端使用)
* 提交报名信息(供学生端使用,须登录)
* @param {Object} data 报名表单数据
*/
submitApplication: (data) => {
return request({
url: '/zsjy/enrollment/info/submit',
method: 'POST',
data: data,
header: {
isToken: false // 不需要登录token,支持对外公开访问
}
data: data
})
},
/**
* 查询我的报名状态(供学生端使用)
* 查询我的报名状态(供学生端使用,须登录)
* @param {Object} params 查询参数 { idCard?, examNumber? }
*/
queryMyStatus: (params) => {
return request({
url: '/zsjy/enrollment/info/myStatus',
method: 'GET',
params: params,
header: {
isToken: false // 不需要登录token,支持对外公开访问
}
params: params
})
},
/**
* 根据身份证号和准考证号查询报名信息
* 根据身份证号和准考证号查询报名信息(须登录)
* @param {string} idCard 身份证号
* @param {string} examNumber 准考证号
*/
@@ -47,37 +41,28 @@ export const enrollmentApi = {
params: {
idCard,
examNumber
},
header: {
isToken: false // 不需要登录token
}
})
},
/**
* 获取学院列表
* 获取学院列表(须登录)
*/
getCollegeList: () => {
return request({
url: '/zsjy/enrollment/info/colleges',
method: 'GET',
header: {
isToken: false // 不需要登录token,支持对外公开访问
}
method: 'GET'
})
},
/**
* 根据学院ID获取专业列表
* 根据学院ID获取专业列表(须登录)
* @param {string|number} collegeId 学院ID
*/
getMajorsByCollegeId: (collegeId) => {
return request({
url: `/zsjy/enrollment/info/majors/${collegeId}`,
method: 'GET',
header: {
isToken: false // 不需要登录token,支持对外公开访问
}
method: 'GET'
})
}
}
+1 -7
View File
@@ -1464,14 +1464,11 @@ export default {
submitData.isBoarding = 0;
}
// 调用提交接口
// 调用提交接口(须登录,携带 Token)
const response = await request({
url: '/zsjy/enrollment/info/submit',
method: 'POST',
data: submitData,
header: {
isToken: false, // 不需要登录token
},
});
uni.showToast({
@@ -1564,9 +1561,6 @@ export default {
idCard: this.queryData.idCard,
examNumber: this.queryData.examNumber,
},
header: {
isToken: false, // 不需要登录token
},
});
if (response.data) {
+4 -3
View File
@@ -245,7 +245,7 @@ const doSocialLogin = (type: string) => {
// 固定的 CAS 地址与生产前端地址(按需求写死)
const CAS_BASE = 'http://td.hzdyjsxy.cn:19081';
const CAS_CLIENT_ID = '0i7tcT5tULqvmmbbV8';
const CAS_CLIENT_SECRET = '68c4f7641523452aaf5ef6d0432869f7';
// client_secret 仅后端持有,前端禁止携带
const FRONTEND_REDIRECT_URI_ENCODED = encodeURIComponent('http://web.hzdyjsxy.cn:18081');
const CAS_REDIRECT_KEY = 'cas_login_redirect';
/** 单点登录门户域名(Referer 匹配) */
@@ -371,7 +371,7 @@ const extractCodeFromRoute = (route: any): string | null => {
};
/**
* 使用 code 去请求 accessToken,并打印返回信息
* 使用 code 经后端换取本系统登录令牌(换票密钥仅后端使用)
*/
const fetchAccessToken = async (code: string) => {
try {
@@ -396,7 +396,8 @@ const fetchAccessToken = async (code: string) => {
body = txt;
}
}
console.log('[Backend -> CAS accessToken response]', { status: res.status, ok: res.ok, body });
// 不打印 body(可能含 accessToken)
console.log('[CAS login] backend response', { status: res.status, ok: res.ok });
// 如果后端返回了登录令牌,前端直接用它完成登录后的行为(存 token & 跳转)
// 兼容多种返回结构:{ data: { access_token: '...' } } 或 { access_token: '...' } 或 raw LoginVo
+1 -1
View File
@@ -11,7 +11,7 @@ DB_CONFIG = {
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"password": "nsyOfyOyoFCr%Brj-k_5PqbW",
"charset": "utf8mb4"
}
@@ -11,7 +11,7 @@ DB_CONFIG = {
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"password": "nsyOfyOyoFCr%Brj-k_5PqbW",
"charset": "utf8mb4",
"autocommit": False, # 手动控制事务
"connect_timeout": 60, # 连接超时时间(秒)
+1 -1
View File
@@ -3,7 +3,7 @@ DB_HOST="47.99.164.66"
DB_PORT="3306"
DB_NAME="hzdyjs_xg"
DB_USER="root"
DB_PASS="hn02le.34lkdLKD"
DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW"
# Script configuration
LOG_DIR="${SCRIPT_DIR}/student_class_change"
+1 -1
View File
@@ -3,7 +3,7 @@ DB_HOST="47.99.164.66"
DB_PORT="3306"
DB_NAME="hzdyjs_xg"
DB_USER="root"
DB_PASS="hn02le.34lkdLKD"
DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW"
# Script configuration
LOG_DIR="${SCRIPT_DIR}/student_id_card_refresh"
@@ -97,7 +97,7 @@ DB_HOST="47.99.164.66" # 数据库主机
DB_PORT="3306" # 数据库端口
DB_NAME="hzdyjs_xg" # 数据库名
DB_USER="root" # 数据库用户
DB_PASS="hn02le.34lkdLKD" # 数据库密码
DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW" # 数据库密码
# Script configuration
LOG_DIR="${SCRIPT_DIR}/student_id_card_refresh" # 日志目录
+1 -1
View File
@@ -11,7 +11,7 @@ DB_CONFIG = {
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"password": "nsyOfyOyoFCr%Brj-k_5PqbW",
"charset": "utf8mb4"
}
+1 -1
View File
@@ -14,7 +14,7 @@ DB_CONFIG = {
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"password": "nsyOfyOyoFCr%Brj-k_5PqbW",
"charset": "utf8mb4"
}
@@ -10,7 +10,7 @@ SOURCE_DB_HOST="47.99.164.66"
SOURCE_DB_PORT="3306"
SOURCE_DB_NAME="hzdyjs_xg"
SOURCE_DB_USER="root"
SOURCE_DB_PASS="hn02le.34lkdLKD"
SOURCE_DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW"
# ---------- 目标库(认证中间库,读写) ----------
# 研发环境:与源库同库,仅新增中间表
@@ -19,7 +19,7 @@ TARGET_DB_HOST="47.99.164.66"
TARGET_DB_PORT="3306"
TARGET_DB_NAME="hzdyjs_xg"
TARGET_DB_USER="root"
TARGET_DB_PASS="hn02le.34lkdLKD"
TARGET_DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW"
# ---------- 同步策略 ----------
# 同步模式:full=全量,incremental=增量(按 update_time)
@@ -11,7 +11,7 @@ DB_CONFIG = {
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"password": "nsyOfyOyoFCr%Brj-k_5PqbW",
"charset": "utf8mb4"
}
@@ -11,7 +11,7 @@
host = 47.99.164.66
port = 3306
user = root
password = hn02le.34lkdLKD
password = nsyOfyOyoFCr%%Brj-k_5PqbW
database = hzdyjs_xg
[api]
+1 -1
View File
@@ -3,7 +3,7 @@ DB_HOST="47.99.164.66"
DB_PORT="3306"
DB_NAME="hzdyjs_xg"
DB_USER="root"
DB_PASS="hn02le.34lkdLKD"
DB_PASS="nsyOfyOyoFCr%Brj-k_5PqbW"
# Script configuration
LOG_DIR="${SCRIPT_DIR}/student_status_change"
+174
View File
@@ -0,0 +1,174 @@
# 20260922 安全扫描修改
> 针对扫描报告中免登录接口暴露、敏感信息泄露及可写风险的整改记录。
> 扫描样例:`GET /prod-api/zsjy/enrollment/info/query?examNumber=123456` 可免登返回身份证、手机号等。
## 一、本次处理范围
| 优先级 | 项 | 处理结果 |
|--------|----|----------|
| P0 | `POST /rs/init/update-menu` 未授权改菜单 | **已整改** |
| P0 | Swagger `/api-docs`、`/swagger-ui/**` | **已下线** |
| P0 | Demo `/demo/mail/**`、`/demo/queue/delayed/**` | **已下线(运行包不再引入)** |
| P0 | 招生 5 个免登接口(含扫描点名 `query`) | **已整改**(须登录) |
| P1 | `GET /cas/accessToken` 密钥暴露与日志明文 | **部分整改**(前端去 secret + 日志脱敏;后端 secret 外置待做) |
| P1 | `POST /rs/application/submit` 匿名滥投 | **已整改**(须登录) |
---
## 二、已完成修改明细
### 1. P0:`POST /rs/init/update-menu` 须登录
**问题**:标注 `@SaIgnore`,未登录即可删改 `sys_menu` / `sys_role_menu`。
**修改**:
- 移除 `@SaIgnore`
- 增加 `@SaCheckPermission("system:menu:edit")`
**文件**:
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java`
**验证**:未带 Token → 401;无 `system:menu:edit` → 无权限。
---
### 2. P0:Swagger / api-docs 下线
**问题**:`security.excludes` 放行文档路径,且 `springdoc` 开启,便于枚举全量接口。
**修改**:
- `springdoc.api-docs.enabled` / `swagger-ui.enabled` → `false`
- 从 `security.excludes` 移除 `/api-docs`、`/swagger-ui/**`、`/v3/api-docs/**`、`/webjars/**` 等
**文件**:
- `backend/ruoyi-admin/src/main/resources/application.yml`
**验证**:访问 `/prod-api/swagger-ui.html`、`/prod-api/api-docs` 不可用。
---
### 3. P0:Demo 模块下线
**问题**:`ruoyi-demo` 打入运行包,邮件、延迟队列等接口带 `@SaIgnore`。
**修改**:注释掉运行路径上所有 `ruoyi-demo` 依赖(admin + 各业务模块),Controller 不再注册。
**文件**:
- `backend/ruoyi-admin/pom.xml`
- `backend/ruoyi-modules/ruoyi-hq/pom.xml`
- `backend/ruoyi-modules/ruoyi-rs/pom.xml`
- `backend/ruoyi-modules/ruoyi-cw/pom.xml`
- `backend/ruoyi-modules/ruoyi-xg/pom.xml`
- `backend/ruoyi-modules/ruoyi-zsjy/pom.xml`
- `backend/ruoyi-modules/ruoyi-jcbt/pom.xml`
- `backend/ruoyi-modules/ruoyi-xskh/pom.xml`
- `backend/ruoyi-modules/ruoyi-bzrkh/pom.xml`
- `backend/ruoyi-modules/ruoyi-aqbw/pom.xml`
**说明**:源码目录 `ruoyi-modules/ruoyi-demo` 仍保留,默认构建/部署不加载。
**验证**:`/prod-api/demo/mail/**`、`/prod-api/demo/queue/delayed/**` → 404。
---
### 4. P0:招生报名相关 5 个接口须登录(扫描报告点名)
**问题**:以下接口标注 `@SaIgnore`,未登录即可访问;其中 `query` 仅凭准考证号即可返回身份证、手机号、住址等敏感信息。
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/zsjy/enrollment/info/query` | 按身份证/准考证查报名 |
| POST | `/zsjy/enrollment/info/submit` | 学生提交报名 |
| GET | `/zsjy/enrollment/info/myStatus` | 查本人报名状态 |
| GET | `/zsjy/enrollment/info/colleges` | 学院列表 |
| GET | `/zsjy/enrollment/info/majors/{collegeId}` | 专业列表 |
**修改**:
- 后端:移除上述 5 个方法的 `@SaIgnore`,走统一登录校验
- 移动端:去掉 `isToken: false`,请求携带 Token
**文件**:
- `backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java`
- `mobile/src/api/enrollment.js`
- `mobile/src/pages/enrollment/index.vue`
**影响**:新生报名页须先登录再查询/提交;未带 Token 访问上述接口返回 401。
**验证**:
- 无 Token 访问 `GET /prod-api/zsjy/enrollment/info/query?examNumber=123456` → 401
- 登录后携带 Token 可正常查询/提交
---
### 5. P1:CAS 换票 — 前端去 secret + 日志脱敏
**问题**:
- 前端写死 `CAS_CLIENT_SECRET`(虽未参与换票,但会进浏览器包)
- 后端日志打印含 secret 的完整 URL、access_token、完整 profile
**修改**:
- 删除前端 `CAS_CLIENT_SECRET`;换票仍只调 `GET /cas/accessToken?code=...`
- 前端控制台不再打印含 Token 的响应 body
- `CasController` 日志仅保留 base/client_id/长度等非敏感信息;失败响应不再回传 CAS 原始报文
**文件**:
- `web/src/views/login.vue`
- `backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java`
- `docs/开发文档/统一身份认证接入说明.md`(同步说明)
**对登录影响**:无(前端本未使用 secret;日志脱敏不改换票逻辑)。
**未做(后续建议)**:
- 后端 `CLIENT_SECRET` 配置外置 + 向 CAS 轮换密钥
- `/cas/accessToken` IP 限流
---
### 6. P1:`POST /rs/application/submit` 须登录
**问题**:`@SaIgnore` + `security.excludes` 放行,可匿名灌简历数据。
**修改**:
- 移除 `@SaIgnore`
- 从 `security.excludes` 删除 `/rs/application/submit`
- 提交时绑定 `applicantId = LoginHelper.getUserId()`
**文件**:
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java`
- `backend/ruoyi-admin/src/main/resources/application.yml`
- `docs/开发文档/API/招聘模块API.md`
**影响**:招聘门户投递须先登录;`/rs/job/public/**` 公开浏览不受影响。
---
## 三、部署与验证清单
1. 重新编译打包后端(demo 依赖变更需完整构建),部署后重启。
2. 前端若已发布含 CAS secret 的旧包,需重新构建发布 `web`。
3. 移动端报名相关改动需重新发布 `mobile`。
4. 抽查:
- [ ] `POST /rs/init/update-menu` 无 Token → 401
- [ ] `/swagger-ui.html`、`/api-docs` 不可用
- [ ] `/demo/mail/**` → 404
- [ ] `GET /zsjy/enrollment/info/query?examNumber=123456` 无 Token → 401
- [ ] 招生 submit / myStatus / colleges / majors 无 Token → 401
- [ ] CAS 统一认证登录仍可用
- [ ] `POST /rs/application/submit` 无 Token → 401;登录后可提交
---
## 四、相关分项说明(可作附录查阅)
| 文档 | 内容 |
|------|------|
| `问题整改/P0-rs-init-update-menu免登整改说明.md` | 菜单更新接口 |
| `问题整改/P0-Swagger与Demo下线整改说明.md` | Swagger / Demo |
| `问题整改/P1-CAS换票密钥与日志脱敏整改说明.md` | CAS |
| `问题整改/P1-rs-application-submit须登录整改说明.md` | 应聘提交 |
---
**文档日期**:2026-09-22
**主题**:安全扫描修改
@@ -0,0 +1,30 @@
# P0:Swagger / Demo 模块下线整改说明
## 问题
1. **Swagger / api-docs 免登暴露**:`security.excludes` 放行 `/api-docs`、`/swagger-ui/**` 等,且 `springdoc.*.enabled=true`,扫描器可枚举全量接口。
2. **Demo 模块打入运行包**:`/demo/mail/**`、`/demo/queue/delayed/**` 等带 `@SaIgnore`,存在滥用发信等风险。
## 整改
### 1. 关闭并收回 Swagger 白名单
文件:`backend/ruoyi-admin/src/main/resources/application.yml`
- `springdoc.api-docs.enabled` → `false`
- `springdoc.swagger-ui.enabled` → `false`
- 从 `security.excludes` 移除全部 api-docs / swagger-ui / webjars 路径
### 2. 运行包不再引入 demo
- `backend/ruoyi-admin/pom.xml`:注释掉 `ruoyi-demo` 依赖
- 各业务模块 pom(hq/rs/cw/xg/zsjy/jcbt/xskh/bzrkh/aqbw):同步注释 `ruoyi-demo` 传递依赖,避免再次打入 admin 包
> 源码目录 `ruoyi-modules/ruoyi-demo` 仍保留,便于本地开发需要时再启用;默认构建/部署不再加载其 Controller。
## 验证
重启后端后:
1. 访问 `/prod-api/swagger-ui.html`、`/prod-api/api-docs` → 不可用或 404/鉴权失败
2. 访问 `/prod-api/demo/mail/sendSimpleMessage`、`/prod-api/demo/queue/delayed/subscribe` → 404(映射不存在)
@@ -0,0 +1,19 @@
# P0:`POST /rs/init/update-menu` 免登录整改说明
## 问题
`RsInitController#updateMenu` 标注了 `@SaIgnore`,未登录即可删除并重建 `sys_menu` / `sys_role_menu`(menu_id 2000–2028),属于未授权写操作。
## 整改
- 移除 `@SaIgnore`,恢复 Sa-Token 登录校验。
- 增加 `@SaCheckPermission("system:menu:edit")`,仅具备系统菜单编辑权限的账号可调用。
## 涉及文件
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java`
## 验证
未带 Token 调用 `POST /prod-api/rs/init/update-menu` 应返回未登录(401)。
已登录但无 `system:menu:edit` 权限应返回无权限。
@@ -0,0 +1,15 @@
# P1:CAS 换票密钥与日志脱敏整改说明
## 改动
1. **前端**:删除 `web/src/views/login.vue` 中未使用的 `CAS_CLIENT_SECRET`;换票仍只调 `GET /cas/accessToken?code=...`;控制台不再打印含 Token 的响应 body。
2. **后端**:`CasController` 日志不再打印带 `client_secret` 的完整 URL、`access_token`、完整 profile;失败响应也不再回传 CAS 原始报文。
## 对登录的影响
无。前端本就未使用 secret;日志脱敏不改变换票与签发 Token 逻辑。
## 后续建议(未做)
- 将后端 `CLIENT_SECRET` 外置到配置/环境变量,并向 CAS 侧轮换密钥。
- 对 `/cas/accessToken` 增加 IP 限流。
@@ -0,0 +1,15 @@
# P1:`POST /rs/application/submit` 须登录整改说明
## 问题
接口标注 `@SaIgnore`,且在 `security.excludes` 中放行,未登录即可任意提交应聘信息(姓名、手机号等写入库),存在灌数据/滥投风险。
## 整改
1. 移除 `RsApplicationController#submitApplication` 的 `@SaIgnore`
2. 从 `application.yml` → `security.excludes` 删除 `/rs/application/submit`
3. 提交时绑定 `applicantId = LoginHelper.getUserId()`
## 影响
招聘门户投递须先登录;未带 Token 调用返回未登录。职位公开浏览接口(`/rs/job/public/**`)不受影响。
@@ -0,0 +1,30 @@
-- ============================================================
-- P4 整改:应用库账号(禁止业务使用 root)
-- 说明:
-- 1. 用现有 root 登录后执行本脚本(生产 172.16.0.67、测试库需各执行一遍)
-- 2. 仅授权业务库 hzdyjs_xg.*,不含 *.* 全局 DBA 权限
-- 3. 执行后修改应用配置并重启,再用下方「验证」确认
-- ============================================================
-- 账号:hzdyjs_app
-- 密码:Hzdyjs@App#n4SQi8AfPHldNj49
CREATE USER IF NOT EXISTS 'hzdyjs_app'@'%' IDENTIFIED BY 'Hzdyjs@App#n4SQi8AfPHldNj49';
CREATE USER IF NOT EXISTS 'hzdyjs_app'@'localhost' IDENTIFIED BY 'Hzdyjs@App#n4SQi8AfPHldNj49';
CREATE USER IF NOT EXISTS 'hzdyjs_app'@'172.16.0.%' IDENTIFIED BY 'Hzdyjs@App#n4SQi8AfPHldNj49';
-- 业务库完整权限(表级 ALL,仍不是全局 DBA)
GRANT ALL PRIVILEGES ON `hzdyjs_xg`.* TO 'hzdyjs_app'@'%';
GRANT ALL PRIVILEGES ON `hzdyjs_xg`.* TO 'hzdyjs_app'@'localhost';
GRANT ALL PRIVILEGES ON `hzdyjs_xg`.* TO 'hzdyjs_app'@'172.16.0.%';
FLUSH PRIVILEGES;
-- ---------- 验证(可选)----------
-- SHOW GRANTS FOR 'hzdyjs_app'@'%';
-- 期望类似:GRANT ALL PRIVILEGES ON `hzdyjs_xg`.* TO `hzdyjs_app`@`%`
-- 不应出现:ON *.* 、SUPER、FILE、GRANT OPTION 等全局高危权限
-- 应用侧验证:
-- SELECT USER(), CURRENT_USER();
-- 期望:hzdyjs_app@...
+13
View File
@@ -0,0 +1,13 @@
-- P5 配套:关闭开放注册,缩小 /auth/register 免登攻击面
-- 在业务库 hzdyjs_xg 执行
UPDATE sys_config
SET config_value = 'false',
update_time = NOW()
WHERE config_key = 'sys.account.registerUser';
-- 验证
SELECT config_key, config_value
FROM sys_config
WHERE config_key = 'sys.account.registerUser';
-- 期望 config_value = false
+13
View File
@@ -0,0 +1,13 @@
-- P6:关闭开放注册(与 P5 配套,缩小免登 /auth/register 攻击面)
-- 在业务库 hzdyjs_xg 执行
UPDATE sys_config
SET config_value = 'false',
update_time = NOW()
WHERE config_key = 'sys.account.registerUser';
-- 验证
SELECT config_id, config_key, config_value, remark
FROM sys_config
WHERE config_key = 'sys.account.registerUser';
-- 期望 config_value = false
@@ -0,0 +1,35 @@
# P6 整改说明:注册接口免登可被滥用
## 问题
`AuthController` 类级 `@SaIgnore`,`POST /prod-api/auth/register` 无需登录即可访问。若数据库 `sys.account.registerUser=true`,可被滥用于撞库、刷注册,并曾作为 SQL 注入入口(见 P5)。
## 整改措施(已落地)
| # | 措施 | 位置 |
|---|------|------|
| 1 | 配置层总开关 `user.register.enabled`,**默认 false** | `application.yml` |
| 2 | Controller 最先判断配置开关,关闭则直接拒绝 | `AuthController.register` |
| 3 | Service 二次拦截,防止绕过 Controller | `SysRegisterService.register` |
| 4 | IP 限流:60 秒内最多 5 次 | `@RateLimiter` |
| 5 | 仍保留库开关 `sys.account.registerUser` 二次判断 | 原有逻辑 |
| 6 | 配套 SQL 将库开关置为 false | `P6-关闭开放注册.sql` |
生效条件(同时满足才会真正注册成功):
1. `user.register.enabled=true`(配置,默认已关)
2. `sys.account.registerUser=true`(数据库)
校园场景保持两者均为 false 即可。
## 发布注意
- 需重新编译发布 backend
- 生产执行 `P6-关闭开放注册.sql`(若 P5 已执行过可跳过)
- 验证:`POST /prod-api/auth/register` 应返回「当前系统没有开启注册功能」
## 与 P5 关系
- P5:修 `tenantId` SQL 注入(格式白名单)
- P6:缩小免登注册攻击面(默认关闭 + 限流)
- 两者互补,建议一并发布
+29
View File
@@ -0,0 +1,29 @@
-- ============================================================
-- P7:重置业务管理员 js01 密码(BCrypt,与系统 Hutool BCrypt 一致)
-- 新明文密码:Hzdyjs@Js01#K9mP2xQ7w
-- 请在生产/测试库分别执行;执行后通知管理员用新密码登录并自行再改一次
-- ============================================================
-- 执行前确认账号存在
SELECT user_id, tenant_id, user_name, nick_name, status, del_flag,
LEFT(password, 10) AS pwd_prefix
FROM sys_user
WHERE user_name = 'js01'
AND del_flag = '0';
-- 重置密码
UPDATE sys_user
SET password = '$2a$10$8oUw5Tz8x6vQJlcQrcQZT.J/vjUAe2JjSNL/QdDT2XJqg.wX67csO',
update_time = NOW()
WHERE user_name = 'js01'
AND del_flag = '0';
-- 验证(应影响 1 行;哈希前缀一般为 $2a$10$)
SELECT user_id, user_name, nick_name, status,
LEFT(password, 29) AS pwd_prefix,
update_time
FROM sys_user
WHERE user_name = 'js01'
AND del_flag = '0';
-- 可选:清理该用户在线会话需在 Redis/后台强制下线,SQL 无法直接踢 token
@@ -1,22 +1,40 @@
# SQL 注入漏洞整改说明(tenantId)
# SQL 注入漏洞整改说明(tenantId / P5)
## 背景
攻防演练报告指出智慧校园(mobile H5:`ykt.hzdyjsxy.cn:18081/#/`)存在 SQL 注入,可拖取 `sys_user` / `sys_config` 等数据。
攻防演练报告:免登接口 `POST /prod-api/auth/register` 的 `tenantId` 经多租户插件拼入 SQL,可 `extractvalue` 拖库(RuoYi-Vue-Plus XVE-2026-50962 同类问题)。
实际利用入口为后端免登接口 `POST /prod-api/auth/register` 的 `tenantId` 字段,经多租户插件拼入 SQL(与 RuoYi-Vue-Plus XVE-2026-50962 同类问题)。
## 整改措施(代码已落地)
## 整改内容(2026-09-17)
| # | 措施 | 改动位置 |
|---|------|----------|
| 1 | sink 白名单:租户 ID 仅允许 `^[0-9]{1,20}$`,非法直接抛错,不拼 SQL | `PlusTenantLineHandler`、`TenantHelper.checkTenantIdFormat` |
| # | 措施 | 位置 |
|---|------|------|
| 1 | sink 白名单:租户 ID 仅 `^[0-9]{1,20}$`,非法不拼 SQL | `PlusTenantLineHandler`、`TenantHelper.checkTenantIdFormat` |
| 2 | 注册接口先 `checkTenant`,再查注册开关 | `AuthController.register` |
| 3 | 所有 `TenantHelper.dynamic/setDynamic` 入口统一格式校验;登录 `checkTenant` 增加格式校验 | `TenantHelper`、`SysLoginService.checkTenant` |
| 4 | 删除心理咨询调试任意 SQL 接口 | 删除 `XljkConsultationDebugController` |
| 3 | 登录/动态租户入口统一格式校验 | `SysLoginService.checkTenant`、`TenantHelper.setDynamic` |
| 4 | 入参 Bean 校验 `@Pattern` | `LoginBody.tenantId`(含注册) |
| 5 | `selectRegisterEnabled` 入口再校验 | `SysConfigServiceImpl` |
| 6 | 社交登录绑定参数校验 | `AuthController.authBinding` |
| 7 | 删除心理咨询调试任意 SQL 接口 | 已删除 `XljkConsultationDebugController` |
## 发布注意
## 配套 SQL(生产建议执行)
- 需重新编译并发布 **backend** 后生效;仅发 mobile 前端无效。
- 建议同步轮换初始密码、微信 AppSecret,并审计异常访问。
- 生产环境确认 `sys.account.registerUser=false`(关闭开放注册)。
关闭开放注册,缩小免登攻击面:
```sql
UPDATE sys_config
SET config_value = 'false',
update_time = NOW()
WHERE config_key = 'sys.account.registerUser'
AND deleted = 0;
```
## 发布与验证
1. 重新编译发布 **backend**(仅发前端无效)
2. 用报告同类 payload 打 `/prod-api/auth/register`,应返回租户编号格式不正确 / 未开启注册,**不应**再出现 XPATH / SQL 报错泄密
3. 正常 `tenantId=000000` 登录不受影响
## 状态
- 代码:已完成
- 生产生效:取决于是否已用含本修复的包重启
+1
View File
@@ -0,0 +1 @@
{"code":404,"msg":"No endpoint GET /actuator/metrics.","data":null}
+9
View File
@@ -0,0 +1,9 @@
HTTP/1.1 200 OK
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Transfer-Encoding: chunked
Content-Type: application/json
Date: Sun, 20 Sep 2026 03:34:05 GMT
+1
View File
@@ -0,0 +1 @@
{"code":404,"msg":"No endpoint GET /actuator/heapdump.","data":null}
+1
View File
@@ -0,0 +1 @@
{"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e", "grantType": "password", "tenantId": "000000' AND extractvalue(1,concat(0x7e,(SELECT version()))) AND '1'='1", "username": "zzq001", "password": "ttt", "userType": "sys_user", "code": "", "uuid": ""}
+1
View File
@@ -0,0 +1 @@
{"code":404,"msg":"No endpoint GET /actuator/env.","data":null}
+1
View File
@@ -0,0 +1 @@
{"code": 401, "msg": "no basic auth", "data": null}
+2
View File
@@ -0,0 +1,2 @@
{"code":500,"msg":"租户编号格式不正确, 用户密码长度必须在5到30个字符之间","data":null}
HTTP:200
+2
View File
@@ -0,0 +1,2 @@
{"code":500,"msg":"当前系统没有开启注册功能!","data":null}
HTTP:200
+1
View File
@@ -0,0 +1 @@
{"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e", "grantType": "password", "tenantId": "000000", "username": "ptest_p6_1789875176", "password": "Test@123456", "userType": "sys_user", "code": "", "uuid": ""}
+2
View File
@@ -0,0 +1,2 @@
{"code":200,"msg":"操作成功","data":{"tenantEnabled":true,"voList":[{"tenantId":"000000","companyName":"杭州第一技师学院","domain":null}]}}
HTTP:200
+22
View File
@@ -0,0 +1,22 @@
# 本地测试环境 P1–P9 验证记录
- 验证时间:2026-09-20
- 环境:本机 backend `:8080` + web `:80`(`/prod-api` 代理)
- 测试库:`47.99.164.66 / hzdyjs_xg`
| 编号 | 结论 | 验证证据 |
|------|------|----------|
| P1 | 通过 | `ruoyi:123456` → 401;`ruoyi:HzdyjsMon@Dev#9Kx7mQ2pL` → Basic 鉴权通过 |
| P2 | 通过 | `/actuator/heapdump` 返回 `No endpoint GET /actuator/heapdump`,无堆转储文件 |
| P3 | 通过(攻击链已断) | 依赖 P1/P2;无法再远程 dump 内存凭据 |
| P4 | 通过 | 配置 `username: hzdyjs_app`;库账号可连;`CURRENT_USER=hzdyjs_app@%` |
| P5 | 通过 | 注入 `tenantId` → `租户编号格式不正确`,无 XPATH/SQL 泄密 |
| P6 | 通过 | 正常注册 → `当前系统没有开启注册功能!`;库 `registerUser=false` |
| P7 | 本地 N/A / 生产已改 | 测试库无 `js01`;生产已执行 P7 脚本并确认哈希更新 |
| P8 | 通过(入口已堵) | heapdump/注入入口不可再利用;历史泄露数据需事件侧收尾 |
| P9 | 通过 | 敏感端点均不可用;仅残留空的 `/actuator` 发现页(无子端点) |
## 补充说明
- `/actuator` 根路径在正确监控密码下仍可能返回空 `_links`(仅 self),**不含 health/heapdump/env**,不影响安全结论。
- 前端 `http://127.0.0.1/` 与 `GET /auth/tenant/list` 正常(HTTP 200)。