From 8bdaf53222cdb3146f342c5ae0729b4175b418c3 Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Fri, 4 Sep 2026 14:43:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AF=9D=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=AE=A2=E6=88=B7=E7=AB=AF=20X-Device-Id?= =?UTF-8?q?=EF=BC=88auth=5Fsessions.device=5Fid=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flutter 端每次请求已携带 X-Device-Id;auth 读取该头(截断 128)经 CreateSessionRequest 透传,user 落 auth_sessions.device_id,为多设备会话列表备数据 - 门禁:./mvnw clean test → BUILD SUCCESS,74 测试 0 失败(新增 registerForwardsDeviceIdHeaderToTheSessionRecord + 会话落库断言) Co-Authored-By: Claude Fable 5 --- .../auth/controller/AuthController.java | 15 +++++++++----- .../patbond/auth/service/AuthService.java | 5 +++-- .../auth/controller/AuthControllerTest.java | 20 +++++++++++++++++++ .../common/session/CreateSessionRequest.java | 14 ++++++++++++- .../user/session/SessionRepository.java | 7 ++++--- .../patbond/user/session/SessionService.java | 10 ++++++---- .../SessionLifecycleIntegrationTest.java | 10 +++++++++- 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/patbond-auth/src/main/java/com/patbond/patbond/auth/controller/AuthController.java b/patbond-auth/src/main/java/com/patbond/patbond/auth/controller/AuthController.java index ed97b6b..b82093e 100644 --- a/patbond-auth/src/main/java/com/patbond/patbond/auth/controller/AuthController.java +++ b/patbond-auth/src/main/java/com/patbond/patbond/auth/controller/AuthController.java @@ -53,14 +53,19 @@ public class AuthController { } private AuthService.ClientInfo clientInfo(HttpServletRequest request) { - String userAgent = request.getHeader(HttpHeaders.USER_AGENT); - if (userAgent != null && userAgent.length() > 512) { - userAgent = userAgent.substring(0, 512); - } + String deviceId = truncate(request.getHeader("X-Device-Id"), 128); + String userAgent = truncate(request.getHeader(HttpHeaders.USER_AGENT), 512); String forwarded = request.getHeader("X-Forwarded-For"); String ip = forwarded != null && !forwarded.isBlank() ? forwarded.split(",")[0].trim() : request.getRemoteAddr(); - return new AuthService.ClientInfo(userAgent, ip); + return new AuthService.ClientInfo(deviceId, userAgent, ip); + } + + private static String truncate(String value, int maxLength) { + if (value == null || value.isBlank()) { + return null; + } + return value.length() > maxLength ? value.substring(0, maxLength) : value; } } diff --git a/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java b/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java index addc191..789db55 100644 --- a/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java +++ b/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java @@ -34,7 +34,7 @@ import java.util.UUID; public class AuthService { /** Device metadata forwarded to the session record (observability only). */ - public record ClientInfo(String userAgent, String ipAddress) { + public record ClientInfo(String deviceId, String userAgent, String ipAddress) { } private final UserClient userClient; @@ -88,7 +88,8 @@ public class AuthService { private AuthTokenResponse openSession(UUID userId, ClientInfo clientInfo) { SessionTokens tokens = requireData(sessionClient.create(new CreateSessionRequest( - userId, clientInfo.userAgent(), clientInfo.ipAddress())), "创建会话失败"); + userId, clientInfo.deviceId(), clientInfo.userAgent(), clientInfo.ipAddress())), + "创建会话失败"); return assemble(tokens); } diff --git a/patbond-auth/src/test/java/com/patbond/patbond/auth/controller/AuthControllerTest.java b/patbond-auth/src/test/java/com/patbond/patbond/auth/controller/AuthControllerTest.java index 8153852..128d5ef 100644 --- a/patbond-auth/src/test/java/com/patbond/patbond/auth/controller/AuthControllerTest.java +++ b/patbond-auth/src/test/java/com/patbond/patbond/auth/controller/AuthControllerTest.java @@ -8,6 +8,7 @@ import com.patbond.patbond.auth.support.TestJwtKeys; import com.patbond.patbond.common.error.BusinessException; import com.patbond.patbond.common.error.ErrorCode; import com.patbond.patbond.common.response.ApiResponse; +import com.patbond.patbond.common.session.CreateSessionRequest; import com.patbond.patbond.common.session.RevokeSessionRequest; import com.patbond.patbond.common.session.SessionTokens; import com.patbond.patbond.common.user.UserProfile; @@ -112,6 +113,25 @@ class AuthControllerTest { .andExpect(jsonPath("$.data.expiresAt").doesNotExist()); } + @Test + void registerForwardsDeviceIdHeaderToTheSessionRecord() throws Exception { + when(userClient.createUser(any())).thenReturn(ApiResponse.success( + new UserProfile(USER_ID, "alice", null, "+8613800138000", OffsetDateTime.now()))); + when(sessionClient.create(any())).thenReturn(ApiResponse.success(sessionTokens())); + + mockMvc.perform(post("/api/v1/auth/register") + .header("X-Device-Id", "pixel-8-of-alice") + .contentType(APPLICATION_JSON) + .content(REGISTER_BODY)) + .andExpect(status().isOk()); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(CreateSessionRequest.class); + verify(sessionClient).create(captor.capture()); + assertThat(captor.getValue().getDeviceId()).isEqualTo("pixel-8-of-alice"); + assertThat(captor.getValue().getUserId()).isEqualTo(USER_ID); + } + @Test void registerPropagatesDuplicateUsernameAsConflict() throws Exception { when(userClient.createUser(any())) diff --git a/patbond-common/src/main/java/com/patbond/patbond/common/session/CreateSessionRequest.java b/patbond-common/src/main/java/com/patbond/patbond/common/session/CreateSessionRequest.java index ac81775..6e42069 100644 --- a/patbond-common/src/main/java/com/patbond/patbond/common/session/CreateSessionRequest.java +++ b/patbond-common/src/main/java/com/patbond/patbond/common/session/CreateSessionRequest.java @@ -15,6 +15,9 @@ public class CreateSessionRequest { @NotNull(message = "userId 不能为空") private UUID userId; + @Size(max = 128, message = "deviceId 长度不能超过128位") + private String deviceId; + @Size(max = 512, message = "userAgent 长度不能超过512位") private String userAgent; @@ -24,8 +27,9 @@ public class CreateSessionRequest { public CreateSessionRequest() { } - public CreateSessionRequest(UUID userId, String userAgent, String ipAddress) { + public CreateSessionRequest(UUID userId, String deviceId, String userAgent, String ipAddress) { this.userId = userId; + this.deviceId = deviceId; this.userAgent = userAgent; this.ipAddress = ipAddress; } @@ -38,6 +42,14 @@ public class CreateSessionRequest { this.userId = userId; } + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + public String getUserAgent() { return userAgent; } diff --git a/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionRepository.java b/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionRepository.java index 85da36c..c85f38d 100644 --- a/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionRepository.java +++ b/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionRepository.java @@ -27,13 +27,13 @@ public class SessionRepository { public void insert(UUID id, UUID userId, UUID tokenFamilyId, byte[] refreshTokenHash, String accessTokenJti, OffsetDateTime expiresAt, - String userAgent, String ipAddress) { + String deviceId, String userAgent, String ipAddress) { jdbcClient.sql(""" INSERT INTO identity.auth_sessions (id, user_id, token_family_id, refresh_token_hash, access_token_jti, - expires_at, user_agent, ip_address) + expires_at, device_id, user_agent, ip_address) VALUES (:id, :userId, :familyId, :hash, :jti, :expiresAt, - :userAgent, CAST(:ipAddress AS inet)) + :deviceId, :userAgent, CAST(:ipAddress AS inet)) """) .param("id", id) .param("userId", userId) @@ -41,6 +41,7 @@ public class SessionRepository { .param("hash", refreshTokenHash) .param("jti", accessTokenJti) .param("expiresAt", expiresAt) + .param("deviceId", deviceId) .param("userAgent", userAgent) .param("ipAddress", ipAddress) .update(); diff --git a/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionService.java b/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionService.java index 1d06502..eaea607 100644 --- a/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionService.java +++ b/patbond-user/src/main/java/com/patbond/patbond/user/session/SessionService.java @@ -49,7 +49,7 @@ public class SessionService { /** Opens a new session (= new token family) for a freshly authenticated user. */ public SessionTokens create(CreateSessionRequest request) { return insertSession(request.getUserId(), UuidV7.generate(), - request.getUserAgent(), request.getIpAddress()); + request.getDeviceId(), request.getUserAgent(), request.getIpAddress()); } /** @@ -75,7 +75,8 @@ public class SessionService { } SessionTokens rotated = transactionTemplate.execute(status -> { - SessionTokens tokens = insertSession(session.userId(), session.tokenFamilyId(), null, null); + SessionTokens tokens = insertSession(session.userId(), session.tokenFamilyId(), + null, null, null); if (sessionRepository.markRotated(session.id(), tokens.getSessionId()) != 1) { status.setRollbackOnly(); return null; @@ -98,7 +99,8 @@ public class SessionService { sessionRepository.revokeByTokenHashAndUser(sha256(refreshToken), userId, "logout"); } - private SessionTokens insertSession(UUID userId, UUID familyId, String userAgent, String ipAddress) { + private SessionTokens insertSession(UUID userId, UUID familyId, + String deviceId, String userAgent, String ipAddress) { UUID sessionId = UuidV7.generate(); String jti = UuidV7.generate().toString(); byte[] tokenBytes = new byte[32]; @@ -108,7 +110,7 @@ public class SessionService { .plus(properties.getSession().getRefreshTtl()); sessionRepository.insert(sessionId, userId, familyId, sha256(refreshToken), jti, - expiresAt, userAgent, ipAddress); + expiresAt, deviceId, userAgent, ipAddress); return new SessionTokens(sessionId, userId, jti, refreshToken, expiresAt); } diff --git a/patbond-user/src/test/java/com/patbond/patbond/user/session/SessionLifecycleIntegrationTest.java b/patbond-user/src/test/java/com/patbond/patbond/user/session/SessionLifecycleIntegrationTest.java index e4583f5..516f4da 100644 --- a/patbond-user/src/test/java/com/patbond/patbond/user/session/SessionLifecycleIntegrationTest.java +++ b/patbond-user/src/test/java/com/patbond/patbond/user/session/SessionLifecycleIntegrationTest.java @@ -57,7 +57,8 @@ class SessionLifecycleIntegrationTest { private Map createSession(String userId) throws Exception { String body = mockMvc.perform(internalPost("/internal/sessions") - .content("{\"userId\":\"%s\",\"userAgent\":\"junit\",\"ipAddress\":\"127.0.0.1\"}" + .content(("{\"userId\":\"%s\",\"deviceId\":\"junit-device\"," + + "\"userAgent\":\"junit\",\"ipAddress\":\"127.0.0.1\"}") .formatted(userId))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) @@ -91,6 +92,13 @@ class SessionLifecycleIntegrationTest { assertThat(storedHash).isEqualTo(expected).hasSize(32); // The plaintext token appears nowhere in the row. assertThat(new String(storedHash, StandardCharsets.ISO_8859_1)).isNotEqualTo(refreshToken); + + String deviceId = jdbcClient.sql( + "SELECT device_id FROM identity.auth_sessions WHERE id = :id") + .param("id", UUID.fromString((String) session.get("sessionId"))) + .query(String.class) + .single(); + assertThat(deviceId).isEqualTo("junit-device"); } @Test