From 58284044182cc3fba7fbc2e949b7f8e203db6ea5 Mon Sep 17 00:00:00 2001 From: marsal Date: Tue, 7 Jul 2026 11:48:42 +0800 Subject: [PATCH] fix(account): secure login response, add validation, global exception handler --- .../admin/controller/AccountController.java | 9 +- .../com/sino/mci/admin/dto/LoginResponse.java | 16 ++++ .../com/sino/mci/admin/dto/UserResponse.java | 21 +++++ .../mci/admin/service/AccountService.java | 82 ++++++++++++++++--- .../mci/config/GlobalExceptionHandler.java | 45 ++++++++++ .../sino/mci/exception/BusinessException.java | 19 +++++ .../mci/exception/UnauthorizedException.java | 8 ++ .../controller/AccountControllerTest.java | 2 + 8 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java create mode 100644 backend/mci-server/src/main/java/com/sino/mci/admin/dto/UserResponse.java create mode 100644 backend/mci-server/src/main/java/com/sino/mci/config/GlobalExceptionHandler.java create mode 100644 backend/mci-server/src/main/java/com/sino/mci/exception/BusinessException.java create mode 100644 backend/mci-server/src/main/java/com/sino/mci/exception/UnauthorizedException.java diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java b/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java index 4e51ff6..e72af10 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java +++ b/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java @@ -3,6 +3,9 @@ package com.sino.mci.admin.controller; import com.sino.mci.admin.dto.CreateTenantRequest; import com.sino.mci.admin.dto.CreateUserRequest; import com.sino.mci.admin.dto.LoginRequest; +import com.sino.mci.admin.dto.LoginResponse; +import com.sino.mci.admin.dto.UserResponse; +import com.sino.mci.admin.entity.Tenant; import com.sino.mci.admin.service.AccountService; import com.sino.mci.shared.web.ApiResponse; import jakarta.validation.Valid; @@ -17,19 +20,19 @@ public class AccountController { private final AccountService accountService; @PostMapping("/tenant") - public ApiResponse createTenant(@Valid @RequestBody CreateTenantRequest request) { + public ApiResponse createTenant(@Valid @RequestBody CreateTenantRequest request) { return ApiResponse.ok(accountService.createTenant(request)); } @PostMapping("/user") - public ApiResponse createUser( + public ApiResponse createUser( @RequestHeader("X-Tenant-Code") String tenantCode, @Valid @RequestBody CreateUserRequest request) { return ApiResponse.ok(accountService.createUser(tenantCode, request)); } @PostMapping("/login") - public ApiResponse login( + public ApiResponse login( @RequestHeader("X-Tenant-Code") String tenantCode, @Valid @RequestBody LoginRequest request) { return ApiResponse.ok(accountService.login(tenantCode, request.getUsername(), request.getPassword())); diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java new file mode 100644 index 0000000..dc0e41c --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java @@ -0,0 +1,16 @@ +package com.sino.mci.admin.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class LoginResponse { + + private Long id; + private String tenantCode; + private String username; + private String realName; + private String phone; + private List roleCodes; +} diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/dto/UserResponse.java b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/UserResponse.java new file mode 100644 index 0000000..3e47cd4 --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/UserResponse.java @@ -0,0 +1,21 @@ +package com.sino.mci.admin.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class UserResponse { + + private Long id; + private String tenantCode; + private String username; + private String realName; + private String phone; + private String email; + private Integer status; + private List roleCodes; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java b/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java index d50154f..3c80723 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java +++ b/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java @@ -3,6 +3,8 @@ package com.sino.mci.admin.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sino.mci.admin.dto.CreateTenantRequest; import com.sino.mci.admin.dto.CreateUserRequest; +import com.sino.mci.admin.dto.LoginResponse; +import com.sino.mci.admin.dto.UserResponse; import com.sino.mci.admin.entity.PlatformRole; import com.sino.mci.admin.entity.PlatformUser; import com.sino.mci.admin.entity.Tenant; @@ -11,6 +13,8 @@ import com.sino.mci.admin.repository.PlatformRoleMapper; import com.sino.mci.admin.repository.PlatformUserMapper; import com.sino.mci.admin.repository.TenantMapper; import com.sino.mci.admin.repository.UserRoleMapper; +import com.sino.mci.exception.BusinessException; +import com.sino.mci.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.stereotype.Service; @@ -18,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -30,6 +35,12 @@ public class AccountService { @Transactional public Tenant createTenant(CreateTenantRequest request) { + Tenant existing = tenantMapper.selectOne( + new LambdaQueryWrapper().eq(Tenant::getTenantCode, request.getTenantCode())); + if (existing != null) { + throw new BusinessException("租户编码已存在: " + request.getTenantCode()); + } + Tenant tenant = new Tenant(); tenant.setTenantCode(request.getTenantCode()); tenant.setTenantName(request.getTenantName()); @@ -40,7 +51,21 @@ public class AccountService { } @Transactional - public PlatformUser createUser(String tenantCode, CreateUserRequest request) { + public UserResponse createUser(String tenantCode, CreateUserRequest request) { + Tenant tenant = tenantMapper.selectOne( + new LambdaQueryWrapper().eq(Tenant::getTenantCode, tenantCode)); + if (tenant == null) { + throw new BusinessException("租户不存在: " + tenantCode); + } + + PlatformUser existing = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(PlatformUser::getTenantCode, tenantCode) + .eq(PlatformUser::getUsername, request.getUsername())); + if (existing != null) { + throw new BusinessException("用户名已存在: " + request.getUsername()); + } + PlatformUser user = new PlatformUser(); user.setTenantCode(tenantCode); user.setUsername(request.getUsername()); @@ -54,26 +79,63 @@ public class AccountService { for (String roleCode : request.getRoleCodes()) { PlatformRole role = roleMapper.selectOne( new LambdaQueryWrapper().eq(PlatformRole::getRoleCode, roleCode)); - if (role != null) { - UserRole userRole = new UserRole(); - userRole.setUserId(user.getId()); - userRole.setRoleId(role.getId()); - userRoleMapper.insert(userRole); + if (role == null) { + throw new BusinessException("角色不存在: " + roleCode); } + UserRole userRole = new UserRole(); + userRole.setUserId(user.getId()); + userRole.setRoleId(role.getId()); + userRoleMapper.insert(userRole); } } - return user; + + return toUserResponse(user); } - public PlatformUser login(String tenantCode, String username, String password) { + public LoginResponse login(String tenantCode, String username, String password) { PlatformUser user = userMapper.selectOne( new LambdaQueryWrapper() .eq(PlatformUser::getTenantCode, tenantCode) .eq(PlatformUser::getUsername, username) .eq(PlatformUser::getStatus, 1)); if (user == null || !BCrypt.checkpw(password, user.getPassword())) { - throw new RuntimeException("用户名或密码错误"); + throw new UnauthorizedException("用户名或密码错误"); } - return user; + + LoginResponse response = new LoginResponse(); + response.setId(user.getId()); + response.setTenantCode(user.getTenantCode()); + response.setUsername(user.getUsername()); + response.setRealName(user.getRealName()); + response.setPhone(user.getPhone()); + response.setRoleCodes(loadRoleCodes(user.getId())); + return response; + } + + private UserResponse toUserResponse(PlatformUser user) { + UserResponse response = new UserResponse(); + response.setId(user.getId()); + response.setTenantCode(user.getTenantCode()); + response.setUsername(user.getUsername()); + response.setRealName(user.getRealName()); + response.setPhone(user.getPhone()); + response.setEmail(user.getEmail()); + response.setStatus(user.getStatus()); + response.setRoleCodes(loadRoleCodes(user.getId())); + response.setCreateTime(user.getCreateTime()); + response.setUpdateTime(user.getUpdateTime()); + return response; + } + + private List loadRoleCodes(Long userId) { + return userRoleMapper.selectList( + new LambdaQueryWrapper().eq(UserRole::getUserId, userId)) + .stream() + .map(UserRole::getRoleId) + .distinct() + .map(roleMapper::selectById) + .filter(role -> role != null) + .map(PlatformRole::getRoleCode) + .collect(Collectors.toList()); } } diff --git a/backend/mci-server/src/main/java/com/sino/mci/config/GlobalExceptionHandler.java b/backend/mci-server/src/main/java/com/sino/mci/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..1fa33ae --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/config/GlobalExceptionHandler.java @@ -0,0 +1,45 @@ +package com.sino.mci.config; + +import com.sino.mci.exception.BusinessException; +import com.sino.mci.exception.UnauthorizedException; +import com.sino.mci.shared.web.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final int UNAUTHORIZED_CODE = 401; + private static final int BAD_REQUEST_CODE = 400; + private static final int INTERNAL_ERROR_CODE = 500; + + @ExceptionHandler(UnauthorizedException.class) + public ApiResponse handleUnauthorized(UnauthorizedException e) { + log.warn("Unauthorized: {}", e.getMessage()); + return ApiResponse.fail(UNAUTHORIZED_CODE, e.getMessage()); + } + + @ExceptionHandler(BusinessException.class) + public ApiResponse handleBusiness(BusinessException e) { + log.warn("Business error: {}", e.getMessage()); + return ApiResponse.fail(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponse handleValidation(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .orElse("请求参数校验失败"); + return ApiResponse.fail(BAD_REQUEST_CODE, message); + } + + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception e) { + log.error("Internal server error", e); + return ApiResponse.fail(INTERNAL_ERROR_CODE, "系统繁忙,请稍后重试"); + } +} diff --git a/backend/mci-server/src/main/java/com/sino/mci/exception/BusinessException.java b/backend/mci-server/src/main/java/com/sino/mci/exception/BusinessException.java new file mode 100644 index 0000000..c025f0a --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/exception/BusinessException.java @@ -0,0 +1,19 @@ +package com.sino.mci.exception; + +public class BusinessException extends RuntimeException { + + private final int code; + + public BusinessException(String message) { + this(-1, message); + } + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/backend/mci-server/src/main/java/com/sino/mci/exception/UnauthorizedException.java b/backend/mci-server/src/main/java/com/sino/mci/exception/UnauthorizedException.java new file mode 100644 index 0000000..9d8de33 --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/exception/UnauthorizedException.java @@ -0,0 +1,8 @@ +package com.sino.mci.exception; + +public class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java b/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java index 677aee9..ab8885c 100644 --- a/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java +++ b/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java @@ -6,6 +6,7 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @@ -15,6 +16,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringBootTest @AutoConfigureMockMvc +@Transactional public class AccountControllerTest { @Autowired