fix(app): 修复 AppController 安全与查询问题

- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
This commit is contained in:
yuhaiming
2026-07-20 15:45:13 +08:00
parent b6c1edaeba
commit 215d6ba167
20 changed files with 664 additions and 91 deletions

View File

@@ -1,9 +1,9 @@
package org.dromara.app.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.json.JSONArray;
@@ -13,10 +13,7 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.bo.*;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.common.core.domain.R;
@@ -732,25 +729,12 @@ public class AppController extends BaseController {
/**
* app用户手机号/邮箱找回忘记密码
*/
@SaIgnore
@ApiEncrypt
@PostMapping("/retrievePassword")
public R<Void> forgot(@RequestBody String body) {
SysUserBo bo = JsonUtils.parseObject(body, SysUserBo.class);
bo.setUserId(LoginHelper.getUserId());
if(Validator.isEmail(bo.getUserName())){
boolean checkPhoneFlag = userService.checkEmailUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.email.not.username");
}
}else if (Validator.isMobile(bo.getUserName())){
boolean checkPhoneFlag = userService.checkPhoneUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.mobile.phone.number.not.username");
}
}
return toAjax(userService.updateUserPas(bo));
public R<Void> forgot(@Validated @RequestBody AppForgotPasswordBo request) {
return toAjax(userService.resetPasswordByVerificationCode(
request.getUsername(), request.getCode(), request.getPassword()));
}
private String formatSecondTime(Date time) {

View File

@@ -0,0 +1,26 @@
package org.dromara.app.domain.bo;
import com.fasterxml.jackson.annotation.JsonAlias;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
/**
* APP 忘记密码请求。
*/
@Data
public class AppForgotPasswordBo {
@JsonAlias("userName")
@NotBlank(message = "账号不能为空")
@Length(min = 2, max = 64, message = "账号长度必须在2到64个字符之间")
private String username;
@JsonAlias("smsCode")
@NotBlank(message = "验证码不能为空")
private String code;
@NotBlank(message = "新密码不能为空")
@Length(min = 5, max = 30, message = "密码长度必须在5到30个字符之间")
private String password;
}

View File

@@ -19,6 +19,7 @@ public class DeviceCommand implements Serializable {
private String commandType;
private String topic;
private Map<String, Object> payload = new HashMap<>();
private Boolean retryEnabled = Boolean.TRUE;
private int retryCount;
private long createdAt;
private long lastSentAt;

View File

@@ -123,6 +123,7 @@ DeviceRegisterHandler implements MqttTopicHandler {
command.setDeviceMac(deviceMac);
command.setCommandType("registerDeviceNo");
command.setTopic("/" + deviceMac.trim().toLowerCase(Locale.ROOT) + "/subscriber/cmd");
command.setRetryEnabled(false);
command.getPayload().put("deviceNo", deviceNo);
command.getPayload().put("deviceMac", deviceMac);

View File

@@ -3,10 +3,7 @@ package org.dromara.app.controller;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.bo.*;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.common.core.domain.R;
@@ -129,6 +126,41 @@ public class AppControllerTest {
.hasMessage("版本配置不存在");
}
@Test
public void retrievePassword_resetsPasswordWithoutReadingLoginState() {
AppController controller = newController();
AppForgotPasswordBo request = new AppForgotPasswordBo();
request.setUsername("alice@example.com");
request.setCode("123456");
request.setPassword("newPassword");
when(userService.resetPasswordByVerificationCode("alice@example.com", "123456", "newPassword"))
.thenReturn(true);
try (MockedStatic<LoginHelper> loginHelper = mockStatic(LoginHelper.class)) {
R<Void> result = controller.forgot(request);
assertThat(result.getCode()).as(result.getMsg()).isEqualTo(200);
loginHelper.verifyNoInteractions();
}
verify(userService).resetPasswordByVerificationCode(
"alice@example.com", "123456", "newPassword");
}
@Test
public void retrievePassword_acceptsLegacyRequestFieldAliases() throws Exception {
AppController controller = newController();
AppForgotPasswordBo request = new ObjectMapper().readValue(
"{\"userName\":\"13305376054\",\"smsCode\":\"654321\",\"password\":\"newPassword\"}",
AppForgotPasswordBo.class);
when(userService.resetPasswordByVerificationCode("13305376054", "654321", "newPassword"))
.thenReturn(true);
R<Void> result = controller.forgot(request);
assertThat(result.getCode()).as(result.getMsg()).isEqualTo(200);
verify(userService).resetPasswordByVerificationCode("13305376054", "654321", "newPassword");
}
@Test
public void uploadImage_uploadsToOssAndReturnsBackendVisibleInfo() {
AppController controller = newController();

View File

@@ -59,6 +59,7 @@ class DeviceRegisterHandlerTest {
assertThat(captor.getValue().getTopic()).isEqualTo("/aa:bb:cc/subscriber/cmd");
assertThat(captor.getValue().getCommandType()).isEqualTo("registerDeviceNo");
assertThat(captor.getValue().getDeviceNo()).isEqualTo("D01");
assertThat(captor.getValue().getRetryEnabled()).isFalse();
}
@Test