diff --git a/src/main/java/com/yau/digitalrmb/security/application/BootstrapAdminAuthenticator.java b/src/main/java/com/yau/digitalrmb/security/application/BootstrapAdminAuthenticator.java deleted file mode 100644 index f4ecde8..0000000 --- a/src/main/java/com/yau/digitalrmb/security/application/BootstrapAdminAuthenticator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.yau.digitalrmb.security.application; - -import com.yau.digitalrmb.security.config.SecurityProperties; -import org.springframework.context.annotation.Profile; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Component; - -@Component -@Profile({"local", "test"}) -public class BootstrapAdminAuthenticator { - private final SecurityProperties properties; - private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - private final String passwordHash; - - public BootstrapAdminAuthenticator(SecurityProperties properties) { - this.properties = properties; - this.passwordHash = passwordEncoder.encode(properties.getBootstrapAdmin().getPassword()); - } - - public boolean matches(String username, String password) { - return properties.getBootstrapAdmin().getUsername().equals(username) - && passwordEncoder.matches(password, passwordHash); - } -} diff --git a/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java b/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java new file mode 100644 index 0000000..94a8c0d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/security/application/LocalAccountAuthenticationService.java @@ -0,0 +1,50 @@ +package com.yau.digitalrmb.security.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity; +import com.yau.digitalrmb.identity.infrastructure.persistence.entity.UserEntity; +import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper; +import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.UserMapper; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +public class LocalAccountAuthenticationService { + private final UserMapper userMapper; + private final PlatformUserSnapshotMapper snapshotMapper; + private final PasswordEncoder passwordEncoder; + private final JwtTokenService jwtTokenService; + + public LocalAccountAuthenticationService(UserMapper userMapper, + PlatformUserSnapshotMapper snapshotMapper, + PasswordEncoder passwordEncoder, + JwtTokenService jwtTokenService) { + this.userMapper = userMapper; + this.snapshotMapper = snapshotMapper; + this.passwordEncoder = passwordEncoder; + this.jwtTokenService = jwtTokenService; + } + + public JwtTokenService.Token login(String username, String rawPassword) { + UserEntity user = userMapper.selectOne(new LambdaQueryWrapper() + .eq(UserEntity::getUsername, username) + .eq(UserEntity::getEnabled, true) + .last("LIMIT 1")); + if (user == null || !passwordEncoder.matches(rawPassword, user.getPasswordHash())) { + throw invalidCredentials(); + } + PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(user.getId()); + if (snapshot == null) { + throw invalidCredentials(); + } + return jwtTokenService.issueFor(user.getId(), snapshot.getAccount(), Set.of(snapshot.getRoleKey())); + } + + private BusinessException invalidCredentials() { + return new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误"); + } +} diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java b/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java index 874e404..a5e0961 100644 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java +++ b/src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java @@ -4,6 +4,7 @@ import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUse import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper; import com.yau.digitalrmb.security.application.JwtTokenService; import com.yau.digitalrmb.security.application.LoginExchangeCodeService; +import com.yau.digitalrmb.security.application.LocalAccountAuthenticationService; import com.yau.digitalrmb.security.application.RefreshTokenService; import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ErrorCode; @@ -29,13 +30,23 @@ public class AuthController { private final LoginExchangeCodeService exchangeCodeService; private final RefreshTokenService refreshTokenService; private final PlatformUserSnapshotMapper snapshotMapper; + private final LocalAccountAuthenticationService localAccountAuthenticationService; public AuthController(JwtTokenService tokenService, LoginExchangeCodeService exchangeCodeService, - RefreshTokenService refreshTokenService, PlatformUserSnapshotMapper snapshotMapper) { + RefreshTokenService refreshTokenService, PlatformUserSnapshotMapper snapshotMapper, + LocalAccountAuthenticationService localAccountAuthenticationService) { this.tokenService = tokenService; this.exchangeCodeService = exchangeCodeService; this.refreshTokenService = refreshTokenService; this.snapshotMapper = snapshotMapper; + this.localAccountAuthenticationService = localAccountAuthenticationService; + } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + JwtTokenService.Token token = localAccountAuthenticationService.login(request.username(), request.password()); + return ApiResponse.success(new LoginResponse(token.accessToken(), "Bearer", token.expiresIn()), + MDC.get(TraceIdFilter.MDC_KEY)); } @PostMapping("/session/exchange") diff --git a/src/main/java/com/yau/digitalrmb/security/interfaces/BootstrapLoginController.java b/src/main/java/com/yau/digitalrmb/security/interfaces/BootstrapLoginController.java deleted file mode 100644 index e578f97..0000000 --- a/src/main/java/com/yau/digitalrmb/security/interfaces/BootstrapLoginController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.yau.digitalrmb.security.interfaces; - -import com.yau.digitalrmb.security.application.BootstrapAdminAuthenticator; -import com.yau.digitalrmb.security.application.JwtTokenService; -import com.yau.digitalrmb.shared.api.ApiResponse; -import com.yau.digitalrmb.shared.api.ErrorCode; -import com.yau.digitalrmb.shared.exception.BusinessException; -import com.yau.digitalrmb.shared.web.TraceIdFilter; -import jakarta.validation.Valid; -import org.slf4j.MDC; -import org.springframework.context.annotation.Profile; -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; - -@Profile({"local", "test"}) -@RestController -@RequestMapping("/api/v1/auth") -public class BootstrapLoginController { - private final BootstrapAdminAuthenticator authenticator; - private final JwtTokenService tokenService; - - public BootstrapLoginController(BootstrapAdminAuthenticator authenticator, JwtTokenService tokenService) { - this.authenticator = authenticator; - this.tokenService = tokenService; - } - - @PostMapping("/login") - public ApiResponse login(@Valid @RequestBody LoginRequest request) { - if (!authenticator.matches(request.username(), request.password())) { - throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误"); - } - JwtTokenService.Token token = tokenService.issueFor(request.username()); - return ApiResponse.success(new LoginResponse(token.accessToken(), "Bearer", token.expiresIn()), - MDC.get(TraceIdFilter.MDC_KEY)); - } -} diff --git a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java index cf4dbff..7e24ea5 100644 --- a/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java @@ -1,6 +1,11 @@ package com.yau.digitalrmb.security; import com.jayway.jsonpath.JsonPath; +import com.yau.digitalrmb.identity.application.PlatformIdentityProjectionService; +import com.yau.digitalrmb.platformintegration.domain.PlatformActor; +import com.yau.digitalrmb.platformintegration.domain.PlatformCredential; +import com.yau.digitalrmb.platformintegration.domain.PlatformRole; +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; @@ -14,20 +19,26 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest(properties = { - "security.jwt.secret=0123456789012345678901234567890123456789012345678901234567890123", - "security.bootstrap-admin.username=admin", - "security.bootstrap-admin.password=ChangeMe123!" -}) +@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class AuthControllerTest { @Autowired private MockMvc mvc; + @Autowired + private PlatformIdentityProjectionService projectionService; + + @BeforeEach + void setUp() { + PlatformActor actor = new PlatformActor(301L, 3L, "tzs001", "教师", PlatformRole.TEACHER, + java.time.Instant.parse("2026-01-01T00:00:00Z")); + projectionService.project(new PlatformCredential(actor, "123qwe")); + } + @Test void loginIssuesTokenAndTokenProtectsEndpoint() throws Exception { - String body = "{\"username\":\"admin\",\"password\":\"ChangeMe123!\"}"; + String body = "{\"username\":\"tzs001\",\"password\":\"123qwe\"}"; String response = mvc.perform(post("/api/v1/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(body))