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.CreateTenantRequest;
|
||||||
import com.sino.mci.admin.dto.CreateUserRequest;
|
import com.sino.mci.admin.dto.CreateUserRequest;
|
||||||
import com.sino.mci.admin.dto.LoginRequest;
|
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.admin.service.AccountService;
|
||||||
import com.sino.mci.shared.web.ApiResponse;
|
import com.sino.mci.shared.web.ApiResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -17,19 +20,19 @@ public class AccountController {
|
|||||||
private final AccountService accountService;
|
private final AccountService accountService;
|
||||||
|
|
||||||
@PostMapping("/tenant")
|
@PostMapping("/tenant")
|
||||||
public ApiResponse<?> createTenant(@Valid @RequestBody CreateTenantRequest request) {
|
public ApiResponse<Tenant> createTenant(@Valid @RequestBody CreateTenantRequest request) {
|
||||||
return ApiResponse.ok(accountService.createTenant(request));
|
return ApiResponse.ok(accountService.createTenant(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/user")
|
@PostMapping("/user")
|
||||||
public ApiResponse<?> createUser(
|
public ApiResponse<UserResponse> createUser(
|
||||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
@Valid @RequestBody CreateUserRequest request) {
|
@Valid @RequestBody CreateUserRequest request) {
|
||||||
return ApiResponse.ok(accountService.createUser(tenantCode, request));
|
return ApiResponse.ok(accountService.createUser(tenantCode, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ApiResponse<?> login(
|
public ApiResponse<LoginResponse> login(
|
||||||
@RequestHeader("X-Tenant-Code") String tenantCode,
|
@RequestHeader("X-Tenant-Code") String tenantCode,
|
||||||
@Valid @RequestBody LoginRequest request) {
|
@Valid @RequestBody LoginRequest request) {
|
||||||
return ApiResponse.ok(accountService.login(tenantCode, request.getUsername(), request.getPassword()));
|
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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.sino.mci.admin.dto.CreateTenantRequest;
|
import com.sino.mci.admin.dto.CreateTenantRequest;
|
||||||
import com.sino.mci.admin.dto.CreateUserRequest;
|
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.PlatformRole;
|
||||||
import com.sino.mci.admin.entity.PlatformUser;
|
import com.sino.mci.admin.entity.PlatformUser;
|
||||||
import com.sino.mci.admin.entity.Tenant;
|
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.PlatformUserMapper;
|
||||||
import com.sino.mci.admin.repository.TenantMapper;
|
import com.sino.mci.admin.repository.TenantMapper;
|
||||||
import com.sino.mci.admin.repository.UserRoleMapper;
|
import com.sino.mci.admin.repository.UserRoleMapper;
|
||||||
|
import com.sino.mci.exception.BusinessException;
|
||||||
|
import com.sino.mci.exception.UnauthorizedException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.crypto.bcrypt.BCrypt;
|
import org.springframework.security.crypto.bcrypt.BCrypt;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -18,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -30,6 +35,12 @@ public class AccountService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Tenant createTenant(CreateTenantRequest request) {
|
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 tenant = new Tenant();
|
||||||
tenant.setTenantCode(request.getTenantCode());
|
tenant.setTenantCode(request.getTenantCode());
|
||||||
tenant.setTenantName(request.getTenantName());
|
tenant.setTenantName(request.getTenantName());
|
||||||
@@ -40,7 +51,21 @@ public class AccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@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();
|
PlatformUser user = new PlatformUser();
|
||||||
user.setTenantCode(tenantCode);
|
user.setTenantCode(tenantCode);
|
||||||
user.setUsername(request.getUsername());
|
user.setUsername(request.getUsername());
|
||||||
@@ -54,26 +79,63 @@ public class AccountService {
|
|||||||
for (String roleCode : request.getRoleCodes()) {
|
for (String roleCode : request.getRoleCodes()) {
|
||||||
PlatformRole role = roleMapper.selectOne(
|
PlatformRole role = roleMapper.selectOne(
|
||||||
new LambdaQueryWrapper<PlatformRole>().eq(PlatformRole::getRoleCode, roleCode));
|
new LambdaQueryWrapper<PlatformRole>().eq(PlatformRole::getRoleCode, roleCode));
|
||||||
if (role != null) {
|
if (role == null) {
|
||||||
UserRole userRole = new UserRole();
|
throw new BusinessException("角色不存在: " + roleCode);
|
||||||
userRole.setUserId(user.getId());
|
|
||||||
userRole.setRoleId(role.getId());
|
|
||||||
userRoleMapper.insert(userRole);
|
|
||||||
}
|
}
|
||||||
|
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(
|
PlatformUser user = userMapper.selectOne(
|
||||||
new LambdaQueryWrapper<PlatformUser>()
|
new LambdaQueryWrapper<PlatformUser>()
|
||||||
.eq(PlatformUser::getTenantCode, tenantCode)
|
.eq(PlatformUser::getTenantCode, tenantCode)
|
||||||
.eq(PlatformUser::getUsername, username)
|
.eq(PlatformUser::getUsername, username)
|
||||||
.eq(PlatformUser::getStatus, 1));
|
.eq(PlatformUser::getStatus, 1));
|
||||||
if (user == null || !BCrypt.checkpw(password, user.getPassword())) {
|
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.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
|
@Transactional
|
||||||
public class AccountControllerTest {
|
public class AccountControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
Reference in New Issue
Block a user