From f15ae2f1d8499902c0d6ac7b2bec169ab5a4150e Mon Sep 17 00:00:00 2001 From: zichun Date: Thu, 30 Apr 2026 14:54:25 +0800 Subject: [PATCH] feat(usr): auth controller + login it REQ-USR-004 --- backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java | 27 +++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java create mode 100644 backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java diff --git a/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java b/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java new file mode 100644 index 0000000..293e3d3 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java @@ -0,0 +1,27 @@ +package com.xly.erp.module.usr.controller; + +import com.xly.erp.common.response.Result; +import com.xly.erp.module.usr.dto.LoginDTO; +import com.xly.erp.module.usr.service.UserService; +import com.xly.erp.module.usr.vo.LoginVO; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/usr/auth") +public class AuthController { + + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @PostMapping("/login") + public Result login(@Valid @RequestBody LoginDTO dto) { + return Result.ok(userService.login(dto)); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java b/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java new file mode 100644 index 0000000..bbfe909 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java @@ -0,0 +1,147 @@ +package com.xly.erp.module.usr.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xly.erp.module.usr.security.LoginAttemptStore; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.context.ActiveProfiles; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class AuthControllerIT { + + @Autowired + private TestRestTemplate rest; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private BCryptPasswordEncoder encoder; + + @Autowired + private LoginAttemptStore loginAttemptStore; + + @LocalServerPort + private int port; + + @BeforeEach + @AfterEach + void cleanup() { + jdbcTemplate.update("DELETE FROM tUser WHERE sUserNo LIKE 'sp_test_%'"); + loginAttemptStore.clearFailures("sp_test_login_user"); + loginAttemptStore.clearFailures("sp_test_login_lock"); + } + + @Test + void loginWithValidCredentials_returns200_withTokens() throws Exception { + insertActiveUser("sp_test_login_user", "登录用户1", encoder.encode("666666"), false); + + Map body = new HashMap<>(); + body.put("sUserName", "登录用户1"); + body.put("password", "666666"); + body.put("version", "标准版"); + + ResponseEntity resp = rest.exchange( + url(), HttpMethod.POST, new HttpEntity<>(body, jsonHeaders()), String.class); + + JsonNode jb = objectMapper.readTree(resp.getBody()); + assertThat(jb.get("code").asInt()).isZero(); + JsonNode data = jb.get("data"); + assertThat(data.get("accessToken").asText()).isNotBlank(); + assertThat(data.get("refreshToken").asText()).isNotBlank(); + assertThat(data.get("expiresIn").asInt()).isEqualTo(28800); + assertThat(data.get("user").get("sUserNo").asText()).isEqualTo("sp_test_login_user"); + assertThat(data.get("user").has("sPasswordHash")).isFalse(); + } + + @Test + void loginWithEmptyBody_returns40001() throws Exception { + ResponseEntity resp = rest.exchange( + url(), HttpMethod.POST, new HttpEntity<>("{}", jsonHeaders()), String.class); + assertThat(objectMapper.readTree(resp.getBody()).get("code").asInt()).isEqualTo(40001); + } + + @Test + void loginWithUserNotFound_returns40101() throws Exception { + Map body = new HashMap<>(); + body.put("sUserName", "幽灵用户"); + body.put("password", "x"); + body.put("version", "标准版"); + ResponseEntity resp = rest.exchange( + url(), HttpMethod.POST, new HttpEntity<>(body, jsonHeaders()), String.class); + assertThat(objectMapper.readTree(resp.getBody()).get("code").asInt()).isEqualTo(40101); + } + + @Test + void loginWithWrongPassword_returns40101() throws Exception { + insertActiveUser("sp_test_login_user", "登录用户1", encoder.encode("666666"), false); + + Map body = new HashMap<>(); + body.put("sUserName", "登录用户1"); + body.put("password", "wrong"); + body.put("version", "标准版"); + + ResponseEntity resp = rest.exchange( + url(), HttpMethod.POST, new HttpEntity<>(body, jsonHeaders()), String.class); + assertThat(objectMapper.readTree(resp.getBody()).get("code").asInt()).isEqualTo(40101); + } + + @Test + void loginAfter5WrongPasswords_returns42301() throws Exception { + insertActiveUser("sp_test_login_lock", "锁定用户", encoder.encode("666666"), false); + + Map body = new HashMap<>(); + body.put("sUserName", "锁定用户"); + body.put("password", "wrong"); + body.put("version", "标准版"); + + for (int i = 0; i < LoginAttemptStore.MAX_ATTEMPTS - 1; i++) { + rest.exchange(url(), HttpMethod.POST, new HttpEntity<>(body, jsonHeaders()), String.class); + } + ResponseEntity resp = rest.exchange( + url(), HttpMethod.POST, new HttpEntity<>(body, jsonHeaders()), String.class); + assertThat(objectMapper.readTree(resp.getBody()).get("code").asInt()).isEqualTo(42301); + + loginAttemptStore.clearFailures("锁定用户"); + } + + private void insertActiveUser(String userNo, String userName, String passwordHash, boolean deleted) { + jdbcTemplate.update( + "INSERT INTO tUser (sBrandsId, sSubsidiaryId, tCreateDate, sUserNo, sUserName, " + + "sUserType, sLanguage, bCanModifyDocs, sPasswordHash, sCreatedBy, bDeleted) " + + "VALUES ('XLY','XLY', NOW(), ?, ?, '普通用户', 'zh', 0, ?, 'STUB_ADMIN', ?)", + userNo, userName, passwordHash, deleted ? 1 : 0); + } + + private static HttpHeaders jsonHeaders() { + HttpHeaders h = new HttpHeaders(); + h.setContentType(MediaType.APPLICATION_JSON); + return h; + } + + private String url() { + return "http://localhost:" + port + "/api/usr/auth/login"; + } +} -- libgit2 0.22.2