feat: add local password login

master
chenyuan 4 weeks ago
parent f017a153a3
commit 83c6fed1f9

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

@ -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<UserEntity>()
.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, "用户名或密码错误");
}
}

@ -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.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper;
import com.yau.digitalrmb.security.application.JwtTokenService; import com.yau.digitalrmb.security.application.JwtTokenService;
import com.yau.digitalrmb.security.application.LoginExchangeCodeService; 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.security.application.RefreshTokenService;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.api.ErrorCode;
@ -29,13 +30,23 @@ public class AuthController {
private final LoginExchangeCodeService exchangeCodeService; private final LoginExchangeCodeService exchangeCodeService;
private final RefreshTokenService refreshTokenService; private final RefreshTokenService refreshTokenService;
private final PlatformUserSnapshotMapper snapshotMapper; private final PlatformUserSnapshotMapper snapshotMapper;
private final LocalAccountAuthenticationService localAccountAuthenticationService;
public AuthController(JwtTokenService tokenService, LoginExchangeCodeService exchangeCodeService, public AuthController(JwtTokenService tokenService, LoginExchangeCodeService exchangeCodeService,
RefreshTokenService refreshTokenService, PlatformUserSnapshotMapper snapshotMapper) { RefreshTokenService refreshTokenService, PlatformUserSnapshotMapper snapshotMapper,
LocalAccountAuthenticationService localAccountAuthenticationService) {
this.tokenService = tokenService; this.tokenService = tokenService;
this.exchangeCodeService = exchangeCodeService; this.exchangeCodeService = exchangeCodeService;
this.refreshTokenService = refreshTokenService; this.refreshTokenService = refreshTokenService;
this.snapshotMapper = snapshotMapper; this.snapshotMapper = snapshotMapper;
this.localAccountAuthenticationService = localAccountAuthenticationService;
}
@PostMapping("/login")
public ApiResponse<LoginResponse> 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") @PostMapping("/session/exchange")

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

@ -1,6 +1,11 @@
package com.yau.digitalrmb.security; package com.yau.digitalrmb.security;
import com.jayway.jsonpath.JsonPath; 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.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; 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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(properties = { @SpringBootTest
"security.jwt.secret=0123456789012345678901234567890123456789012345678901234567890123",
"security.bootstrap-admin.username=admin",
"security.bootstrap-admin.password=ChangeMe123!"
})
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ActiveProfiles("test") @ActiveProfiles("test")
class AuthControllerTest { class AuthControllerTest {
@Autowired @Autowired
private MockMvc mvc; 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 @Test
void loginIssuesTokenAndTokenProtectsEndpoint() throws Exception { 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") String response = mvc.perform(post("/api/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(body)) .content(body))

Loading…
Cancel
Save