fix(account): secure login response, add validation, global exception handler

This commit is contained in:
2026-07-07 11:48:42 +08:00
parent 4f9b3aa211
commit 5828404418
8 changed files with 189 additions and 13 deletions

View File

@@ -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<Tenant> createTenant(@Valid @RequestBody CreateTenantRequest request) {
return ApiResponse.ok(accountService.createTenant(request));
}
@PostMapping("/user")
public ApiResponse<?> createUser(
public ApiResponse<UserResponse> 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<LoginResponse> login(
@RequestHeader("X-Tenant-Code") String tenantCode,
@Valid @RequestBody LoginRequest request) {
return ApiResponse.ok(accountService.login(tenantCode, request.getUsername(), request.getPassword()));

View File

@@ -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<String> roleCodes;
}

View File

@@ -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<String> roleCodes;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -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<Tenant>().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<Tenant>().eq(Tenant::getTenantCode, tenantCode));
if (tenant == null) {
throw new BusinessException("租户不存在: " + tenantCode);
}
PlatformUser existing = userMapper.selectOne(
new LambdaQueryWrapper<PlatformUser>()
.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<PlatformRole>().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<PlatformUser>()
.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<String> loadRoleCodes(Long userId) {
return userRoleMapper.selectList(
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, userId))
.stream()
.map(UserRole::getRoleId)
.distinct()
.map(roleMapper::selectById)
.filter(role -> role != null)
.map(PlatformRole::getRoleCode)
.collect(Collectors.toList());
}
}

View File

@@ -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<Void> handleUnauthorized(UnauthorizedException e) {
log.warn("Unauthorized: {}", e.getMessage());
return ApiResponse.fail(UNAUTHORIZED_CODE, e.getMessage());
}
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusiness(BusinessException e) {
log.warn("Business error: {}", e.getMessage());
return ApiResponse.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> 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<Void> handleException(Exception e) {
log.error("Internal server error", e);
return ApiResponse.fail(INTERNAL_ERROR_CODE, "系统繁忙,请稍后重试");
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,8 @@
package com.sino.mci.exception;
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}

View File

@@ -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