fix(account): secure login response, add validation, global exception handler
This commit is contained in:
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "系统繁忙,请稍后重试");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.sino.mci.exception;
|
||||
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user