feat: add current user context

master
chenyuan 4 weeks ago
parent f0e10a5567
commit 8c49659d68

@ -0,0 +1,186 @@
# Current User Context Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
**Goal:** Add no-argument current-user access for backend code and return the same complete profile from GET /api/v1/auth/me.
**Architecture:** PlatformTokenVerifier parses verified profile claims into VerifiedPlatformToken. LocalSsoAccountService saves the profile in platform_user_snapshot. CurrentUserService reads that snapshot from the Jwt in SecurityContext, and AuthController delegates to it.
**Tech Stack:** Java 8, Spring Boot 2.7, Spring Security, MyBatis-Plus, H2, JUnit 5, MockMvc.
## Global Constraints
- Keep Java 8 and ApiResponse.
- userId and raw roleid are long; school, college, major, class, and student identifiers are String.
- Do not put profile data in application JWTs or expose passwords or Tokens.
- Business requests only read the local snapshot.
---
### Task 1: Parse complete profile claims
**Files:**
- Modify: src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java
- Modify: src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java
- Modify: src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java
**Interfaces:** VerifiedPlatformToken gains schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleId, name, classId, className, studentId.
- [ ] **Step 1: Write the failing test**
~~~java
@Test
void parsesCompleteUserProfileFromVerifiedToken() throws Exception {
VerifiedPlatformToken token = verifier.verify(token(487L, "tzs001", "new-password", 2L,
now.plus(Duration.ofMinutes(5)), completeProfile()));
assertThat(token.getSchoolId()).isEqualTo("610000");
assertThat(token.getCollegeName()).isEqualTo("Computer College");
assertThat(token.getMajorName()).isEqualTo("Software Engineering");
assertThat(token.getRoleId()).isEqualTo(2L);
assertThat(token.getName()).isEqualTo("Test Student");
assertThat(token.getClassId()).isEqualTo("202401");
assertThat(token.getStudentId()).isEqualTo("20240001");
}
~~~
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=PlatformTokenVerifierTest test -DforkCount=0 -B
Expected: FAIL because the new getters do not exist.
- [ ] **Step 3: Minimal implementation**
Add immutable fields and a full constructor to VerifiedPlatformToken while retaining the old constructor for existing callers. In PlatformTokenVerifier, preserve parsed roleid as roleId; parse name with username fallback and all remaining profile fields via:
~~~java
private String optionalText(JsonNode payload, String field) {
JsonNode node = payload.path(field);
return node.isMissingNode() || node.isNull() ? null : node.asText();
}
~~~
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=PlatformTokenVerifierTest test -DforkCount=0 -B
Expected: PASS.
### Task 2: Persist the profile snapshot
**Files:**
- Modify: src/main/resources/schema.sql
- Modify: src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/PlatformUserSnapshotEntity.java
- Modify: src/main/java/com/yau/digitalrmb/identity/application/LocalSsoAccountService.java
- Modify: src/test/java/com/yau/digitalrmb/identity/LocalSsoAccountServiceTest.java
**Interfaces:** synchronize(VerifiedPlatformToken) upserts every complete profile field in platform_user_snapshot.
- [ ] **Step 1: Write the failing test**
~~~java
@Test
void synchronizesCompleteProfileIntoLocalSnapshot() {
service.synchronize(completeToken(603L, "sso603", 2L));
Map<String, Object> row = new JdbcTemplate(dataSource).queryForMap(
"SELECT school_id, college_name, major_name, role_id, class_name, student_id "
+ "FROM platform_user_snapshot WHERE platform_user_id = 603");
assertThat(row).containsEntry("school_id", "610000")
.containsEntry("college_name", "Computer College")
.containsEntry("major_name", "Software Engineering")
.containsEntry("role_id", 2L)
.containsEntry("class_name", "Class 1")
.containsEntry("student_id", "20240001");
}
~~~
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=LocalSsoAccountServiceTest test -DforkCount=0 -B
Expected: FAIL because the columns do not exist.
- [ ] **Step 3: Minimal implementation**
Add nullable school_id, school_name, college_id, college_name, major_id, major_name, role_id, class_id, class_name, student_id columns to the table creation and idempotent ADD COLUMN IF NOT EXISTS statements afterward. Map them with TableField in the snapshot Entity and copy token fields in upsertSnapshot.
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=LocalSsoAccountServiceTest test -DforkCount=0 -B
Expected: PASS.
### Task 3: Implement the common current-user service
**Files:**
- Create: src/main/java/com/yau/digitalrmb/security/application/CurrentUser.java
- Create: src/main/java/com/yau/digitalrmb/security/application/CurrentUserService.java
- Create: src/test/java/com/yau/digitalrmb/security/CurrentUserServiceTest.java
**Interfaces:** CurrentUserService.getCurrentUser() returns schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleid, userId, username, name, classId, className, studentid.
- [ ] **Step 1: Write the failing test**
Synchronize a complete profile, put JwtAuthenticationToken for that user into SecurityContextHolder, call currentUserService.getCurrentUser(), and assert all fields. Also test no snapshot and assert BusinessException ErrorCode.UNAUTHORIZED. Clear SecurityContextHolder in AfterEach.
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=CurrentUserServiceTest test -DforkCount=0 -B
Expected: FAIL because the service does not exist.
- [ ] **Step 3: Minimal implementation**
~~~java
public CurrentUser getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份无效");
}
long userId = parseUserId((Jwt) authentication.getPrincipal());
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(userId);
if (snapshot == null) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份不存在");
}
return map(snapshot);
}
~~~
map converts the complete snapshot, and parseUserId maps malformed subjects to UNAUTHORIZED.
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=CurrentUserServiceTest test -DforkCount=0 -B
Expected: PASS.
### Task 4: Extend /auth/me
**Files:**
- Modify: src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java
- Modify: src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java
- Modify: src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java
- Modify: src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java
**Interfaces:** GET /api/v1/auth/me returns exactly the 13 CurrentUser fields in data.
- [ ] **Step 1: Write the failing endpoint test**
Synchronize complete Token data and assert data.userId, username, name, schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleid, classId, className, studentid. Update local login assertion to username.
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=CurrentUserAndLogoutTest,AuthControllerTest test -DforkCount=0 -B
Expected: FAIL because the response DTO lacks the fields.
- [ ] **Step 3: Minimal implementation**
Replace CurrentUserResponse with the 13 CurrentUser fields. Inject CurrentUserService and have /me map currentUserService.getCurrentUser() into the response; do not query the Mapper in that method.
- [ ] **Step 4: Verify green and build**
Run: mvn -Dtest=CurrentUserAndLogoutTest,AuthControllerTest test -DforkCount=0 -B
Expected: PASS.
Run: mvn test -DforkCount=0 -B
Expected: PASS.
Run: mvn package -DskipTests -B
Expected: BUILD SUCCESS.

@ -59,6 +59,16 @@ public class LocalSsoAccountService {
snapshot.setAccount(token.getUsername());
snapshot.setDisplayName(token.getDisplayName());
snapshot.setRoleKey(token.getRoleKey());
snapshot.setSchoolId(token.getSchoolId());
snapshot.setSchoolName(token.getSchoolName());
snapshot.setCollegeId(token.getCollegeId());
snapshot.setCollegeName(token.getCollegeName());
snapshot.setMajorId(token.getMajorId());
snapshot.setMajorName(token.getMajorName());
snapshot.setRoleId(token.getRoleId());
snapshot.setClassId(token.getClassId());
snapshot.setClassName(token.getClassName());
snapshot.setStudentId(token.getStudentId());
snapshot.setSourceUpdatedAt(LocalDateTime.now());
snapshot.setSyncedAt(LocalDateTime.now());
if (newSnapshot) {

@ -22,6 +22,36 @@ public class PlatformUserSnapshotEntity {
private String roleKey;
@TableField("school_id")
private String schoolId;
@TableField("school_name")
private String schoolName;
@TableField("college_id")
private String collegeId;
@TableField("college_name")
private String collegeName;
@TableField("major_id")
private String majorId;
@TableField("major_name")
private String majorName;
@TableField("role_id")
private Long roleId;
@TableField("class_id")
private String classId;
@TableField("class_name")
private String className;
@TableField("student_id")
private String studentId;
private LocalDateTime sourceUpdatedAt;
@TableField("synced_at")

@ -54,8 +54,16 @@ public class PlatformTokenVerifier {
String username = requiredText(payload, "username");
String password = requiredText(payload, "password");
long roleId = requiredLong(payload, "roleid");
String displayName = payload.path("name").asText(username);
return new VerifiedPlatformToken(userId, username, displayName, password, roleId == 3L ? "TEACHER" : "STUDENT");
String displayName = optionalText(payload, "name");
if (displayName == null || displayName.trim().isEmpty()) {
displayName = username;
}
return new VerifiedPlatformToken(userId, username, displayName, password, roleId,
roleId == 3L ? "TEACHER" : "STUDENT", optionalText(payload, "schoolId"),
optionalText(payload, "schoolName"), optionalText(payload, "collegeId"),
optionalText(payload, "collegeName"), optionalText(payload, "majorId"),
optionalText(payload, "majorName"), optionalText(payload, "classId"),
optionalText(payload, "className"), optionalText(payload, "studentid"));
} catch (PlatformTokenException exception) {
throw exception;
} catch (Exception exception) {
@ -76,5 +84,6 @@ public class PlatformTokenVerifier {
private long requiredPositiveLong(JsonNode payload, String name) { long value = requiredLong(payload, name); if (value <= 0) throw invalid(); return value; }
private long requiredLong(JsonNode payload, String name) { JsonNode node = payload.path(name); if (!node.canConvertToLong()) throw invalid(); return node.asLong(); }
private String requiredText(JsonNode payload, String name) { String value = payload.path(name).asText(); if (value == null || value.trim().isEmpty()) throw invalid(); return value; }
private String optionalText(JsonNode payload, String name) { JsonNode node = payload.path(name); return node.isMissingNode() || node.isNull() ? null : node.asText(); }
private PlatformTokenException invalid() { return new PlatformTokenException("Invalid platform token"); }
}

@ -8,13 +8,43 @@ public class VerifiedPlatformToken {
private final String username;
private final String displayName;
private final String rawPassword;
private final long roleId;
private final String roleKey;
private final String schoolId;
private final String schoolName;
private final String collegeId;
private final String collegeName;
private final String majorId;
private final String majorName;
private final String classId;
private final String className;
private final String studentId;
public VerifiedPlatformToken(long userId, String username, String displayName, String rawPassword, String roleKey) {
this(userId, username, displayName, rawPassword, "TEACHER".equals(roleKey) ? 3L : 2L, roleKey,
null, null, null, null, null, null, null, null, null);
}
public VerifiedPlatformToken(long userId, String username, String displayName, String rawPassword, long roleId,
String roleKey, String schoolId, String schoolName, String collegeId,
String collegeName, String majorId, String majorName, String classId,
String className, String studentId) {
this.userId = userId;
this.username = username;
this.displayName = displayName;
this.rawPassword = rawPassword;
this.roleId = roleId;
this.roleKey = roleKey;
this.schoolId = schoolId;
this.schoolName = schoolName;
this.collegeId = collegeId;
this.collegeName = collegeName;
this.majorId = majorId;
this.majorName = majorName;
this.classId = classId;
this.className = className;
this.studentId = studentId;
}
public String getName() { return displayName; }
}

@ -0,0 +1,22 @@
package com.yau.digitalrmb.security.application;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CurrentUser {
private final String schoolId;
private final String schoolName;
private final String collegeId;
private final String collegeName;
private final String majorId;
private final String majorName;
private final Long roleid;
private final long userId;
private final String username;
private final String name;
private final String classId;
private final String className;
private final String studentid;
}

@ -0,0 +1,46 @@
package com.yau.digitalrmb.security.application;
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity;
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
@Service
public class CurrentUserService {
private final PlatformUserSnapshotMapper snapshotMapper;
public CurrentUserService(PlatformUserSnapshotMapper snapshotMapper) {
this.snapshotMapper = snapshotMapper;
}
public CurrentUser getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) {
throw unauthorized("用户身份无效");
}
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(parseUserId((Jwt) authentication.getPrincipal()));
if (snapshot == null) {
throw unauthorized("用户身份不存在");
}
return new CurrentUser(snapshot.getSchoolId(), snapshot.getSchoolName(), snapshot.getCollegeId(),
snapshot.getCollegeName(), snapshot.getMajorId(), snapshot.getMajorName(), snapshot.getRoleId(),
snapshot.getPlatformUserId(), snapshot.getAccount(), snapshot.getDisplayName(), snapshot.getClassId(),
snapshot.getClassName(), snapshot.getStudentId());
}
private long parseUserId(Jwt jwt) {
try {
return Long.parseLong(jwt.getSubject());
} catch (NumberFormatException exception) {
throw unauthorized("用户身份无效");
}
}
private BusinessException unauthorized(String message) {
return new BusinessException(ErrorCode.UNAUTHORIZED, message);
}
}

@ -1,7 +1,7 @@
package com.yau.digitalrmb.security.interfaces;
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity;
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper;
import com.yau.digitalrmb.security.application.CurrentUser;
import com.yau.digitalrmb.security.application.CurrentUserService;
import com.yau.digitalrmb.security.application.JwtTokenService;
import com.yau.digitalrmb.security.application.LocalAccountAuthenticationService;
import com.yau.digitalrmb.security.application.RefreshTokenService;
@ -9,7 +9,6 @@ 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 java.util.Collections;
import javax.validation.Valid;
import org.slf4j.MDC;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@ -24,13 +23,13 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/v1/auth")
public class AuthController {
private final RefreshTokenService refreshTokenService;
private final PlatformUserSnapshotMapper snapshotMapper;
private final CurrentUserService currentUserService;
private final LocalAccountAuthenticationService localAccountAuthenticationService;
public AuthController(RefreshTokenService refreshTokenService, PlatformUserSnapshotMapper snapshotMapper,
public AuthController(RefreshTokenService refreshTokenService, CurrentUserService currentUserService,
LocalAccountAuthenticationService localAccountAuthenticationService) {
this.refreshTokenService = refreshTokenService;
this.snapshotMapper = snapshotMapper;
this.currentUserService = currentUserService;
this.localAccountAuthenticationService = localAccountAuthenticationService;
}
@ -42,14 +41,12 @@ public class AuthController {
}
@GetMapping("/me")
public ApiResponse<CurrentUserResponse> currentUser(@AuthenticationPrincipal Jwt jwt) {
long userId = userId(jwt);
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(userId);
if (snapshot == null) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份不存在");
}
return ApiResponse.success(new CurrentUserResponse(userId, snapshot.getAccount(), snapshot.getDisplayName(),
Collections.singletonList(snapshot.getRoleKey())), MDC.get(TraceIdFilter.MDC_KEY));
public ApiResponse<CurrentUserResponse> currentUser() {
CurrentUser user = currentUserService.getCurrentUser();
return ApiResponse.success(new CurrentUserResponse(user.getSchoolId(), user.getSchoolName(),
user.getCollegeId(), user.getCollegeName(), user.getMajorId(), user.getMajorName(), user.getRoleid(),
user.getUserId(), user.getUsername(), user.getName(), user.getClassId(), user.getClassName(),
user.getStudentid()), MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/logout")
@ -62,7 +59,7 @@ public class AuthController {
try {
return Long.parseLong(jwt.getSubject());
} catch (NumberFormatException exception) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份无效");
throw new BusinessException(ErrorCode.UNAUTHORIZED, "User identity is invalid");
}
}
}

@ -1,4 +1,22 @@
package com.yau.digitalrmb.security.interfaces;
import lombok.AllArgsConstructor; import lombok.Getter; import java.util.List;
@Getter @AllArgsConstructor
public class CurrentUserResponse { private final long platformUserId; private final String account; private final String displayName; private final List<String> roles; }
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CurrentUserResponse {
private final String schoolId;
private final String schoolName;
private final String collegeId;
private final String collegeName;
private final String majorId;
private final String majorName;
private final Long roleid;
private final long userId;
private final String username;
private final String name;
private final String classId;
private final String className;
private final String studentid;
}

@ -34,9 +34,30 @@ CREATE TABLE IF NOT EXISTS platform_user_snapshot (
display_name VARCHAR(64) NOT NULL,
role_key VARCHAR(16) NOT NULL,
source_updated_at TIMESTAMP NOT NULL,
synced_at TIMESTAMP NOT NULL
synced_at TIMESTAMP NOT NULL,
school_id VARCHAR(64) NULL,
school_name VARCHAR(128) NULL,
college_id VARCHAR(64) NULL,
college_name VARCHAR(128) NULL,
major_id VARCHAR(64) NULL,
major_name VARCHAR(128) NULL,
role_id BIGINT NULL,
class_id VARCHAR(64) NULL,
class_name VARCHAR(128) NULL,
student_id VARCHAR(64) NULL
);
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS school_id VARCHAR(64) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS school_name VARCHAR(128) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS college_id VARCHAR(64) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS college_name VARCHAR(128) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS major_id VARCHAR(64) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS major_name VARCHAR(128) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS role_id BIGINT NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS class_id VARCHAR(64) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS class_name VARCHAR(128) NULL;
ALTER TABLE platform_user_snapshot ADD COLUMN IF NOT EXISTS student_id VARCHAR(64) NULL;
CREATE TABLE IF NOT EXISTS auth_login_exchange_code (
code_hash CHAR(64) PRIMARY KEY,
platform_user_id BIGINT NOT NULL,

@ -10,6 +10,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import javax.sql.DataSource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@ -43,4 +44,25 @@ class LocalSsoAccountServiceTest {
assertThat(passwordEncoder.matches("old-password", passwordHash)).isFalse();
assertThat(roleId).isEqualTo(1001L);
}
@Test
void synchronizesCompleteProfileIntoLocalSnapshot() {
service.synchronize(completeToken(603L, "sso603", 2L));
Map<String, Object> row = new JdbcTemplate(dataSource).queryForMap(
"SELECT school_id, college_name, major_name, role_id, class_name, student_id "
+ "FROM platform_user_snapshot WHERE platform_user_id = 603");
assertThat(row).containsEntry("school_id", "610000")
.containsEntry("college_name", "Computer College")
.containsEntry("major_name", "Software Engineering")
.containsEntry("role_id", 2L)
.containsEntry("class_name", "Class 1")
.containsEntry("student_id", "20240001");
}
private VerifiedPlatformToken completeToken(long userId, String username, long roleId) {
return new VerifiedPlatformToken(userId, username, "Test Student", "password", roleId, "STUDENT",
"610000", "Yan'an University", "100", "Computer College", "101", "Software Engineering",
"202401", "Class 1", "20240001");
}
}

@ -41,6 +41,21 @@ class PlatformTokenVerifierTest {
assertThat(verified.getRoleKey()).isEqualTo("STUDENT");
}
@Test
void parsesCompleteUserProfileFromVerifiedToken() throws Exception {
VerifiedPlatformToken verified = verifier.verify(token(487L, "tzs001", "new-password", 2L,
now.plus(Duration.ofMinutes(5)), completeProfile()));
assertThat(verified.getSchoolId()).isEqualTo("610000");
assertThat(verified.getSchoolName()).isEqualTo("Yan'an University");
assertThat(verified.getCollegeName()).isEqualTo("Computer College");
assertThat(verified.getMajorName()).isEqualTo("Software Engineering");
assertThat(verified.getRoleId()).isEqualTo(2L);
assertThat(verified.getName()).isEqualTo("Test Student");
assertThat(verified.getClassId()).isEqualTo("202401");
assertThat(verified.getStudentId()).isEqualTo("20240001");
}
@Test
void rejectsTamperedExpiredOrIncompleteToken() throws Exception {
String valid = token(487L, "tzs001", "new-password", 3L, now.plus(Duration.ofMinutes(5)));
@ -52,10 +67,29 @@ class PlatformTokenVerifierTest {
}
private String token(long userId, String username, String password, long roleId, Instant expiresAt) throws Exception {
return token(userId, username, password, roleId, expiresAt, new HashMap<String, Object>());
}
private String token(long userId, String username, String password, long roleId, Instant expiresAt,
Map<String, Object> profile) throws Exception {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("userId", userId); claims.put("username", username); claims.put("password", password); claims.put("roleid", roleId);
claims.putAll(profile);
return unsignedToken(claims, expiresAt);
}
private Map<String, Object> completeProfile() {
Map<String, Object> profile = new HashMap<String, Object>();
profile.put("name", "Test Student");
profile.put("schoolId", "610000");
profile.put("schoolName", "Yan'an University");
profile.put("collegeId", "100");
profile.put("collegeName", "Computer College");
profile.put("majorId", "101");
profile.put("majorName", "Software Engineering");
profile.put("classId", "202401");
profile.put("className", "Class 1");
profile.put("studentid", "20240001");
return profile;
}
private String unsignedToken(Map<String, Object> claims, Instant expiresAt) throws Exception {
claims.put("exp", expiresAt.getEpochSecond());
ObjectMapper mapper = new ObjectMapper();

@ -35,8 +35,8 @@ class AuthControllerTest {
mvc.perform(get("/api/v1/auth/me")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.account").value("tzs001"))
.andExpect(jsonPath("$.data.roles[0]").value("STUDENT"));
.andExpect(jsonPath("$.data.userId").value(487))
.andExpect(jsonPath("$.data.username").value("tzs001"));
}
@Test

@ -33,7 +33,9 @@ class CurrentUserAndLogoutTest {
@BeforeEach
void setUp() {
localSsoAccountService.synchronize(new VerifiedPlatformToken(101L, "t001", "Teacher", "password", "TEACHER"));
localSsoAccountService.synchronize(new VerifiedPlatformToken(101L, "t001", "Teacher User", "password", 3L,
"TEACHER", "610000", "Yan'an University", "100", "Computer College", "101",
"Software Engineering", "202401", "Class 1", "20240001"));
teacherJwt = jwtTokenService.issueFor(101L, "t001", Collections.singleton("TEACHER")).accessToken();
refreshToken = refreshTokenService.issue(101L);
}
@ -42,8 +44,19 @@ class CurrentUserAndLogoutTest {
void currentUserIsReadonlyTeacherAndLogoutRevokesOwnRefreshToken() throws Exception {
mvc.perform(get("/api/v1/auth/me").header("Authorization", "Bearer " + teacherJwt))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.account").value("t001"))
.andExpect(jsonPath("$.data.roles[0]").value("TEACHER"));
.andExpect(jsonPath("$.data.userId").value(101))
.andExpect(jsonPath("$.data.username").value("t001"))
.andExpect(jsonPath("$.data.name").value("Teacher User"))
.andExpect(jsonPath("$.data.schoolId").value("610000"))
.andExpect(jsonPath("$.data.schoolName").value("Yan'an University"))
.andExpect(jsonPath("$.data.collegeId").value("100"))
.andExpect(jsonPath("$.data.collegeName").value("Computer College"))
.andExpect(jsonPath("$.data.majorId").value("101"))
.andExpect(jsonPath("$.data.majorName").value("Software Engineering"))
.andExpect(jsonPath("$.data.roleid").value(3))
.andExpect(jsonPath("$.data.classId").value("202401"))
.andExpect(jsonPath("$.data.className").value("Class 1"))
.andExpect(jsonPath("$.data.studentid").value("20240001"));
mvc.perform(post("/api/v1/auth/logout").header("Authorization", "Bearer " + teacherJwt)
.contentType(MediaType.APPLICATION_JSON).content("{\"refreshToken\":\"" + refreshToken + "\"}"))
.andExpect(status().isOk());

@ -0,0 +1,77 @@
package com.yau.digitalrmb.security;
import com.yau.digitalrmb.identity.application.LocalSsoAccountService;
import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken;
import com.yau.digitalrmb.security.application.CurrentUser;
import com.yau.digitalrmb.security.application.CurrentUserService;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import java.time.Instant;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest
@ActiveProfiles("test")
class CurrentUserServiceTest {
@Autowired private CurrentUserService currentUserService;
@Autowired private LocalSsoAccountService localSsoAccountService;
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void readsCompleteProfileForCurrentSecurityContextUser() {
localSsoAccountService.synchronize(completeToken(701L, "sso701", 2L));
authenticateAs(701L);
CurrentUser user = currentUserService.getCurrentUser();
assertThat(user.getUserId()).isEqualTo(701L);
assertThat(user.getUsername()).isEqualTo("sso701");
assertThat(user.getName()).isEqualTo("Test Student");
assertThat(user.getSchoolId()).isEqualTo("610000");
assertThat(user.getCollegeName()).isEqualTo("Computer College");
assertThat(user.getMajorName()).isEqualTo("Software Engineering");
assertThat(user.getRoleid()).isEqualTo(2L);
assertThat(user.getClassId()).isEqualTo("202401");
assertThat(user.getStudentid()).isEqualTo("20240001");
}
@Test
void rejectsCurrentSecurityContextUserWithoutSnapshot() {
authenticateAs(702L);
assertThatThrownBy(() -> currentUserService.getCurrentUser())
.isInstanceOfSatisfying(BusinessException.class,
exception -> assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED));
}
private void authenticateAs(long userId) {
Jwt jwt = Jwt.withTokenValue("test-token")
.header("alg", "none")
.subject(String.valueOf(userId))
.issuedAt(Instant.parse("2026-08-04T00:00:00Z"))
.expiresAt(Instant.parse("2026-08-04T01:00:00Z"))
.build();
AbstractAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private VerifiedPlatformToken completeToken(long userId, String username, long roleId) {
return new VerifiedPlatformToken(userId, username, "Test Student", "password", roleId, "STUDENT",
"610000", "Yan'an University", "100", "Computer College", "101", "Software Engineering",
"202401", "Class 1", "20240001");
}
}
Loading…
Cancel
Save