feat(phase8): US7 用户管理模块(角色变更立即生效、禁用即失效)
- RedisService:新增 hPut/sAdd/sRemove/sMembers Set 操作
- RedisKeyManager:新增 userSessionsKey(userId) = user:sessions:{userId}
- AuthService:login 后将 token 加入 user:sessions 集合;logout 时从集合移除
- UserService:createUser/updateUser/updateRole/updateStatus
- updateRole:DB 写入后更新所有活跃 Token 的 role 字段(立即生效,无需重新登录)
- updateStatus(DISABLED):删除所有活跃 Token(立即失效),清除 sessions 集合
- UserController:5 个端点全部 @RequiresRoles("ADMIN")
- 集成测试:角色变更同一 Token 立即生效;禁用后 Token 立即 401
This commit is contained in:
@@ -22,4 +22,9 @@ public final class RedisKeyManager {
|
|||||||
public static String taskClaimKey(Long taskId) {
|
public static String taskClaimKey(Long taskId) {
|
||||||
return "task:claim:" + taskId;
|
return "task:claim:" + taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** User active sessions set key: user:sessions:{userId} */
|
||||||
|
public static String userSessionsKey(Long userId) {
|
||||||
|
return "user:sessions:" + userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,27 @@ public class RedisService {
|
|||||||
Object val = redisTemplate.opsForHash().get(key, field);
|
Object val = redisTemplate.opsForHash().get(key, field);
|
||||||
return val != null ? val.toString() : null;
|
return val != null ? val.toString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新 Hash 中的单个字段(不改变其他字段和 TTL)。 */
|
||||||
|
public void hPut(String key, String field, String value) {
|
||||||
|
redisTemplate.opsForHash().put(key, field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set operations(用于用户会话跟踪)
|
||||||
|
|
||||||
|
/** 向 Set 添加成员。 */
|
||||||
|
public void sAdd(String key, String member) {
|
||||||
|
redisTemplate.opsForSet().add(key, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Set 移除成员。 */
|
||||||
|
public void sRemove(String key, String member) {
|
||||||
|
redisTemplate.opsForSet().remove(key, (Object) member);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 Set 全部成员;Set 不存在时返回空集合。 */
|
||||||
|
public java.util.Set<String> sMembers(String key) {
|
||||||
|
java.util.Set<String> members = redisTemplate.opsForSet().members(key);
|
||||||
|
return members != null ? members : java.util.Collections.emptySet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.label.module.user.controller;
|
||||||
|
|
||||||
|
import com.label.common.result.PageResult;
|
||||||
|
import com.label.common.result.Result;
|
||||||
|
import com.label.common.shiro.TokenPrincipal;
|
||||||
|
import com.label.module.user.entity.SysUser;
|
||||||
|
import com.label.module.user.service.UserService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresRoles;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户管理接口(5 个端点,全部 ADMIN 权限)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
/** GET /api/users — 分页查询用户列表 */
|
||||||
|
@GetMapping
|
||||||
|
@RequiresRoles("ADMIN")
|
||||||
|
public Result<PageResult<SysUser>> listUsers(
|
||||||
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
return Result.success(userService.listUsers(page, pageSize, principal(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/users — 创建用户 */
|
||||||
|
@PostMapping
|
||||||
|
@RequiresRoles("ADMIN")
|
||||||
|
public Result<SysUser> createUser(@RequestBody Map<String, String> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
return Result.success(userService.createUser(
|
||||||
|
body.get("username"),
|
||||||
|
body.get("password"),
|
||||||
|
body.get("realName"),
|
||||||
|
body.get("role"),
|
||||||
|
principal(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/users/{id} — 更新用户基本信息 */
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@RequiresRoles("ADMIN")
|
||||||
|
public Result<SysUser> updateUser(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
return Result.success(userService.updateUser(
|
||||||
|
id,
|
||||||
|
body.get("realName"),
|
||||||
|
body.get("password"),
|
||||||
|
principal(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/users/{id}/status — 变更用户状态 */
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
@RequiresRoles("ADMIN")
|
||||||
|
public Result<Void> updateStatus(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
userService.updateStatus(id, body.get("status"), principal(request));
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/users/{id}/role — 变更用户角色 */
|
||||||
|
@PutMapping("/{id}/role")
|
||||||
|
@RequiresRoles("ADMIN")
|
||||||
|
public Result<Void> updateRole(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
userService.updateRole(id, body.get("role"), principal(request));
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenPrincipal principal(HttpServletRequest request) {
|
||||||
|
return (TokenPrincipal) request.getAttribute("__token_principal__");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,9 @@ public class AuthService {
|
|||||||
tokenData.put("username", user.getUsername());
|
tokenData.put("username", user.getUsername());
|
||||||
redisService.hSetAll(RedisKeyManager.tokenKey(token), tokenData, tokenTtlSeconds);
|
redisService.hSetAll(RedisKeyManager.tokenKey(token), tokenData, tokenTtlSeconds);
|
||||||
|
|
||||||
|
// 将 token 加入该用户的活跃会话集合(用于角色变更时批量更新/失效)
|
||||||
|
redisService.sAdd(RedisKeyManager.userSessionsKey(user.getId()), token);
|
||||||
|
|
||||||
log.debug("用户登录成功: companyCode={}, username={}", request.getCompanyCode(), request.getUsername());
|
log.debug("用户登录成功: companyCode={}, username={}", request.getCompanyCode(), request.getUsername());
|
||||||
return new LoginResponse(token, user.getId(), user.getUsername(), user.getRole(), tokenTtlSeconds);
|
return new LoginResponse(token, user.getId(), user.getUsername(), user.getRole(), tokenTtlSeconds);
|
||||||
}
|
}
|
||||||
@@ -96,7 +99,14 @@ public class AuthService {
|
|||||||
*/
|
*/
|
||||||
public void logout(String token) {
|
public void logout(String token) {
|
||||||
if (token != null && !token.isBlank()) {
|
if (token != null && !token.isBlank()) {
|
||||||
|
// 从用户会话集合中移除(若 token 仍有效则先读取 userId)
|
||||||
|
String userId = redisService.hGet(RedisKeyManager.tokenKey(token), "userId");
|
||||||
redisService.delete(RedisKeyManager.tokenKey(token));
|
redisService.delete(RedisKeyManager.tokenKey(token));
|
||||||
|
if (userId != null) {
|
||||||
|
try {
|
||||||
|
redisService.sRemove(RedisKeyManager.userSessionsKey(Long.parseLong(userId)), token);
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
log.debug("用户退出,Token 已删除: {}", token);
|
log.debug("用户退出,Token 已删除: {}", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
204
src/main/java/com/label/module/user/service/UserService.java
Normal file
204
src/main/java/com/label/module/user/service/UserService.java
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package com.label.module.user.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.label.common.exception.BusinessException;
|
||||||
|
import com.label.common.redis.RedisKeyManager;
|
||||||
|
import com.label.common.redis.RedisService;
|
||||||
|
import com.label.common.result.PageResult;
|
||||||
|
import com.label.common.shiro.TokenPrincipal;
|
||||||
|
import com.label.module.user.entity.SysUser;
|
||||||
|
import com.label.module.user.mapper.SysUserMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户管理服务(ADMIN 专属)。
|
||||||
|
*
|
||||||
|
* 关键设计:
|
||||||
|
* - 角色变更:DB 写入后立即更新所有活跃 Token 中的 role 字段,无需重新登录
|
||||||
|
* - 状态禁用:DB 写入后删除用户所有活跃 Token(立即失效)
|
||||||
|
* - 使用 user:sessions:{userId} Set 跟踪活跃会话
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(10);
|
||||||
|
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
private final RedisService redisService;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 创建用户 --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新用户(ADMIN 操作)。
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param password 明文密码(将以 BCrypt strength=10 哈希)
|
||||||
|
* @param realName 真实姓名(可选)
|
||||||
|
* @param role 角色(UPLOADER / ANNOTATOR / REVIEWER / ADMIN)
|
||||||
|
* @param principal 当前管理员
|
||||||
|
* @return 新建用户(不含 passwordHash)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public SysUser createUser(String username, String password,
|
||||||
|
String realName, String role,
|
||||||
|
TokenPrincipal principal) {
|
||||||
|
// 校验用户名唯一性
|
||||||
|
SysUser existing = userMapper.selectByCompanyAndUsername(principal.getCompanyId(), username);
|
||||||
|
if (existing != null) {
|
||||||
|
throw new BusinessException("DUPLICATE_USERNAME",
|
||||||
|
"用户名 '" + username + "' 已存在", HttpStatus.CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRole(role);
|
||||||
|
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.setCompanyId(principal.getCompanyId());
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPasswordHash(PASSWORD_ENCODER.encode(password));
|
||||||
|
user.setRealName(realName);
|
||||||
|
user.setRole(role);
|
||||||
|
user.setStatus("ACTIVE");
|
||||||
|
userMapper.insert(user);
|
||||||
|
|
||||||
|
log.debug("用户已创建: userId={}, username={}, role={}", user.getId(), username, role);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 更新基本信息 --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户基本信息(realName、password)。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public SysUser updateUser(Long userId, String realName, String password,
|
||||||
|
TokenPrincipal principal) {
|
||||||
|
SysUser user = getExistingUser(userId, principal.getCompanyId());
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getId, userId)
|
||||||
|
.eq(SysUser::getCompanyId, principal.getCompanyId());
|
||||||
|
|
||||||
|
if (realName != null && !realName.isBlank()) {
|
||||||
|
wrapper.set(SysUser::getRealName, realName);
|
||||||
|
user.setRealName(realName);
|
||||||
|
}
|
||||||
|
if (password != null && !password.isBlank()) {
|
||||||
|
wrapper.set(SysUser::getPasswordHash, PASSWORD_ENCODER.encode(password));
|
||||||
|
}
|
||||||
|
|
||||||
|
userMapper.update(null, wrapper);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 变更角色 --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更用户角色。
|
||||||
|
*
|
||||||
|
* DB 写入后,立即更新该用户所有活跃 Token 中的 role 字段,
|
||||||
|
* 确保角色变更对下一次请求立即生效(无需重新登录)。
|
||||||
|
*
|
||||||
|
* @param userId 目标用户 ID
|
||||||
|
* @param newRole 新角色
|
||||||
|
* @param principal 当前管理员
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateRole(Long userId, String newRole, TokenPrincipal principal) {
|
||||||
|
getExistingUser(userId, principal.getCompanyId());
|
||||||
|
validateRole(newRole);
|
||||||
|
|
||||||
|
// 1. DB 写入
|
||||||
|
userMapper.update(null, new LambdaUpdateWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getId, userId)
|
||||||
|
.eq(SysUser::getCompanyId, principal.getCompanyId())
|
||||||
|
.set(SysUser::getRole, newRole));
|
||||||
|
|
||||||
|
// 2. 更新所有活跃 Token 中的 role 字段(立即生效,无需重新登录)
|
||||||
|
Set<String> tokens = redisService.sMembers(RedisKeyManager.userSessionsKey(userId));
|
||||||
|
tokens.forEach(token ->
|
||||||
|
redisService.hPut(RedisKeyManager.tokenKey(token), "role", newRole));
|
||||||
|
|
||||||
|
// 3. 删除权限缓存(如 Shiro 缓存存在)
|
||||||
|
redisService.delete(RedisKeyManager.userPermKey(userId));
|
||||||
|
|
||||||
|
log.debug("用户角色已变更: userId={}, newRole={}, 更新 {} 个活跃 Token", userId, newRole, tokens.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 变更状态 --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更用户状态(启用/禁用)。
|
||||||
|
*
|
||||||
|
* 禁用时:DB 写入后立即删除该用户所有活跃 Token,现有会话立即失效。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(Long userId, String newStatus, TokenPrincipal principal) {
|
||||||
|
getExistingUser(userId, principal.getCompanyId());
|
||||||
|
|
||||||
|
if (!"ACTIVE".equals(newStatus) && !"DISABLED".equals(newStatus)) {
|
||||||
|
throw new BusinessException("INVALID_STATUS",
|
||||||
|
"状态值不合法,应为 ACTIVE 或 DISABLED", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB 写入
|
||||||
|
userMapper.update(null, new LambdaUpdateWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getId, userId)
|
||||||
|
.eq(SysUser::getCompanyId, principal.getCompanyId())
|
||||||
|
.set(SysUser::getStatus, newStatus));
|
||||||
|
|
||||||
|
// 禁用时:删除所有活跃 Token(立即失效)
|
||||||
|
if ("DISABLED".equals(newStatus)) {
|
||||||
|
Set<String> tokens = redisService.sMembers(RedisKeyManager.userSessionsKey(userId));
|
||||||
|
tokens.forEach(token -> redisService.delete(RedisKeyManager.tokenKey(token)));
|
||||||
|
redisService.delete(RedisKeyManager.userSessionsKey(userId));
|
||||||
|
log.debug("账号已禁用,已删除 {} 个活跃 Token: userId={}", tokens.size(), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除权限缓存
|
||||||
|
redisService.delete(RedisKeyManager.userPermKey(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 查询 --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询当前公司用户列表。
|
||||||
|
*/
|
||||||
|
public PageResult<SysUser> listUsers(int page, int pageSize, TokenPrincipal principal) {
|
||||||
|
pageSize = Math.min(pageSize, 100);
|
||||||
|
Page<SysUser> result = userMapper.selectPage(
|
||||||
|
new Page<>(page, pageSize),
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getCompanyId, principal.getCompanyId())
|
||||||
|
.orderByAsc(SysUser::getCreatedAt));
|
||||||
|
return PageResult.of(result.getRecords(), result.getTotal(), page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 私有工具 --
|
||||||
|
|
||||||
|
private SysUser getExistingUser(Long userId, Long companyId) {
|
||||||
|
SysUser user = userMapper.selectById(userId);
|
||||||
|
if (user == null || !companyId.equals(user.getCompanyId())) {
|
||||||
|
throw new BusinessException("NOT_FOUND", "用户不存在: " + userId, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRole(String role) {
|
||||||
|
if (!List.of("UPLOADER", "ANNOTATOR", "REVIEWER", "ADMIN").contains(role)) {
|
||||||
|
throw new BusinessException("INVALID_ROLE",
|
||||||
|
"角色值不合法: " + role, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.label.integration;
|
||||||
|
|
||||||
|
import com.label.AbstractIntegrationTest;
|
||||||
|
import com.label.module.user.dto.LoginRequest;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户管理集成测试(US7)。
|
||||||
|
*
|
||||||
|
* 测试场景:
|
||||||
|
* 1. 变更角色后权限下一次请求立即生效(无需重新登录)
|
||||||
|
* 2. 禁用账号后现有 Token 下一次请求立即返回 401
|
||||||
|
*/
|
||||||
|
public class UserManagementIntegrationTest extends AbstractIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate restTemplate;
|
||||||
|
|
||||||
|
private String adminToken;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
adminToken = loginAndGetToken("DEMO", "admin", "admin123");
|
||||||
|
assertThat(adminToken).isNotBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 测试 1: 角色变更立即生效 --
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建用户为 ANNOTATOR,变更为 REVIEWER 后同一 Token 立即可访问审批接口")
|
||||||
|
void updateRole_takesEffectImmediately() {
|
||||||
|
String uniqueUsername = "testuser-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
// 1. 创建 ANNOTATOR 用户
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", "Bearer " + adminToken);
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
ResponseEntity<Map> createResp = restTemplate.exchange(
|
||||||
|
baseUrl("/api/users"),
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(Map.of(
|
||||||
|
"username", uniqueUsername,
|
||||||
|
"password", "test1234",
|
||||||
|
"realName", "测试用户",
|
||||||
|
"role", "ANNOTATOR"
|
||||||
|
), headers),
|
||||||
|
Map.class);
|
||||||
|
assertThat(createResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> userData = (Map<String, Object>) createResp.getBody().get("data");
|
||||||
|
Long newUserId = ((Number) userData.get("id")).longValue();
|
||||||
|
|
||||||
|
// 2. 新用户登录获取 Token
|
||||||
|
String userToken = loginAndGetToken("DEMO", uniqueUsername, "test1234");
|
||||||
|
assertThat(userToken).isNotBlank();
|
||||||
|
|
||||||
|
// 3. 验证:ANNOTATOR 无法访问待审批队列(REVIEWER 专属)→ 403
|
||||||
|
ResponseEntity<Map> beforeRoleChange = restTemplate.exchange(
|
||||||
|
baseUrl("/api/tasks/pending-review"),
|
||||||
|
HttpMethod.GET,
|
||||||
|
bearerRequest(userToken),
|
||||||
|
Map.class);
|
||||||
|
assertThat(beforeRoleChange.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||||
|
|
||||||
|
// 4. ADMIN 变更角色为 REVIEWER
|
||||||
|
ResponseEntity<Map> roleResp = restTemplate.exchange(
|
||||||
|
baseUrl("/api/users/" + newUserId + "/role"),
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(Map.of("role", "REVIEWER"), headers),
|
||||||
|
Map.class);
|
||||||
|
assertThat(roleResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
// 5. 验证:同一 Token 下次请求立即具有 REVIEWER 权限 → 200
|
||||||
|
ResponseEntity<Map> afterRoleChange = restTemplate.exchange(
|
||||||
|
baseUrl("/api/tasks/pending-review"),
|
||||||
|
HttpMethod.GET,
|
||||||
|
bearerRequest(userToken),
|
||||||
|
Map.class);
|
||||||
|
assertThat(afterRoleChange.getStatusCode())
|
||||||
|
.as("角色变更后同一 Token 应立即具有 REVIEWER 权限")
|
||||||
|
.isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 测试 2: 禁用账号 Token 立即失效 --
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("禁用账号后,现有 Token 下一次请求立即返回 401")
|
||||||
|
void disableAccount_tokenInvalidatedImmediately() {
|
||||||
|
String uniqueUsername = "testuser-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
// 1. 创建用户
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", "Bearer " + adminToken);
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
ResponseEntity<Map> createResp = restTemplate.exchange(
|
||||||
|
baseUrl("/api/users"),
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(Map.of(
|
||||||
|
"username", uniqueUsername,
|
||||||
|
"password", "test1234",
|
||||||
|
"realName", "测试用户",
|
||||||
|
"role", "ANNOTATOR"
|
||||||
|
), headers),
|
||||||
|
Map.class);
|
||||||
|
assertThat(createResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> userData = (Map<String, Object>) createResp.getBody().get("data");
|
||||||
|
Long newUserId = ((Number) userData.get("id")).longValue();
|
||||||
|
|
||||||
|
// 2. 新用户登录,获取 Token
|
||||||
|
String userToken = loginAndGetToken("DEMO", uniqueUsername, "test1234");
|
||||||
|
assertThat(userToken).isNotBlank();
|
||||||
|
|
||||||
|
// 3. 验证 Token 有效
|
||||||
|
ResponseEntity<Map> meResp = restTemplate.exchange(
|
||||||
|
baseUrl("/api/auth/me"),
|
||||||
|
HttpMethod.GET,
|
||||||
|
bearerRequest(userToken),
|
||||||
|
Map.class);
|
||||||
|
assertThat(meResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
// 4. ADMIN 禁用账号
|
||||||
|
ResponseEntity<Map> disableResp = restTemplate.exchange(
|
||||||
|
baseUrl("/api/users/" + newUserId + "/status"),
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(Map.of("status", "DISABLED"), headers),
|
||||||
|
Map.class);
|
||||||
|
assertThat(disableResp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
// 5. 验证:禁用后,现有 Token 立即失效 → 401
|
||||||
|
ResponseEntity<Map> meAfterDisable = restTemplate.exchange(
|
||||||
|
baseUrl("/api/auth/me"),
|
||||||
|
HttpMethod.GET,
|
||||||
|
bearerRequest(userToken),
|
||||||
|
Map.class);
|
||||||
|
assertThat(meAfterDisable.getStatusCode())
|
||||||
|
.as("禁用账号后现有 Token 应立即失效")
|
||||||
|
.isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 工具方法 --
|
||||||
|
|
||||||
|
private String loginAndGetToken(String companyCode, String username, String password) {
|
||||||
|
LoginRequest req = new LoginRequest();
|
||||||
|
req.setCompanyCode(companyCode);
|
||||||
|
req.setUsername(username);
|
||||||
|
req.setPassword(password);
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||||
|
baseUrl("/api/auth/login"), req, Map.class);
|
||||||
|
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
|
||||||
|
return (String) data.get("token");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpEntity<Void> bearerRequest(String token) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", "Bearer " + token);
|
||||||
|
return new HttpEntity<>(headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user