refactor: 会话失效、数据权限和实时推送重构
This commit is contained in:
1 parent
3a35b24476
commit
faf6754bf4
44 files changed
+2145
-515
No files matched your search
@@ -0,0 +1,387 @@
|
||||
package com.youlai.boot.plugin.mybatis;
|
||||
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.common.enums.DataScopeEnum;
|
||||
import com.youlai.boot.security.model.RoleDataScope;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 数据权限处理器单元测试
|
||||
*
|
||||
* @author Ray.Hao
|
||||
*/
|
||||
@DisplayName("数据权限处理器测试")
|
||||
class MyDataPermissionHandlerTest {
|
||||
|
||||
private MyDataPermissionHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new MyDataPermissionHandler();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// 清理安全上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
// ==================== 边界条件测试 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("边界条件测试")
|
||||
class BoundaryTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("未登录用户 - 返回原始where条件")
|
||||
void whenNotLoggedIn_thenReturnOriginalWhere() {
|
||||
// given: 未设置安全上下文
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where, "com.example.mapper.UserMapper.selectList");
|
||||
|
||||
// then: 返回原始where
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("超级管理员 - 跳过数据权限过滤")
|
||||
void whenRootUser_thenReturnOriginalWhere() {
|
||||
// given: 设置超级管理员
|
||||
setSecurityContext(1L, "admin", 1L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_ROOT")),
|
||||
Collections.emptyList());
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where, "com.example.mapper.UserMapper.selectList");
|
||||
|
||||
// then: 返回原始where,不添加数据权限条件
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无数据权限列表 - 返回原始where条件")
|
||||
void whenNoDataScopes_thenReturnOriginalWhere() {
|
||||
// given: 普通用户但无数据权限
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
Collections.emptyList());
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where, "com.example.mapper.UserMapper.selectList");
|
||||
|
||||
// then: 返回原始where
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 单一角色数据权限测试 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("单一角色数据权限测试")
|
||||
class SingleRoleTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("全部数据权限(ALL) - 不添加过滤条件")
|
||||
void whenAllDataScope_thenReturnOriginalWhere() {
|
||||
// given: 角色拥有全部数据权限
|
||||
setSecurityContext(100L, "admin", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_ADMIN")),
|
||||
List.of(RoleDataScope.all("ADMIN")));
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 返回原始where
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("本部门数据权限(DEPT) - 添加dept_id = ?条件")
|
||||
void whenDeptDataScope_thenAddDeptIdCondition() {
|
||||
// given: 角色拥有本部门数据权限
|
||||
Long deptId = 10L;
|
||||
setSecurityContext(100L, "manager", deptId,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_MANAGER")),
|
||||
List.of(RoleDataScope.dept("MANAGER")));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 添加部门过滤条件
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("dept_id = " + deptId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("本人数据权限(SELF) - 添加create_by = ?条件")
|
||||
void whenSelfDataScope_thenAddCreateByCondition() {
|
||||
// given: 角色拥有本人数据权限
|
||||
Long userId = 100L;
|
||||
setSecurityContext(userId, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
List.of(RoleDataScope.self("USER")));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 添加用户过滤条件
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("create_by = " + userId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("自定义部门数据权限(CUSTOM) - 添加dept_id IN (?)条件")
|
||||
void whenCustomDataScope_thenAddDeptIdInCondition() {
|
||||
// given: 角色拥有自定义部门数据权限
|
||||
List<Long> customDeptIds = Arrays.asList(10L, 20L, 30L);
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_CUSTOM")),
|
||||
List.of(RoleDataScope.custom("CUSTOM", customDeptIds)));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 添加自定义部门IN条件
|
||||
assertThat(result).isNotNull();
|
||||
String sql = result.toString();
|
||||
assertThat(sql).contains("dept_id IN");
|
||||
assertThat(sql).contains("10");
|
||||
assertThat(sql).contains("20");
|
||||
assertThat(sql).contains("30");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("自定义部门数据权限(空列表) - 添加1=0条件")
|
||||
void whenCustomDataScopeWithEmptyList_thenAddFalseCondition() {
|
||||
// given: 角色拥有自定义部门权限但列表为空
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_CUSTOM")),
|
||||
List.of(RoleDataScope.custom("CUSTOM", Collections.emptyList())));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 添加1=0条件(无权限)
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("1 = 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("部门及子部门数据权限(DEPT_AND_SUB) - 添加子查询条件")
|
||||
void whenDeptAndSubDataScope_thenAddSubQueryCondition() {
|
||||
// given: 角色拥有部门及子部门数据权限
|
||||
Long deptId = 10L;
|
||||
setSecurityContext(100L, "manager", deptId,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_MANAGER")),
|
||||
List.of(RoleDataScope.deptAndSub("MANAGER")));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 添加子查询条件
|
||||
assertThat(result).isNotNull();
|
||||
String sql = result.toString();
|
||||
assertThat(sql).contains("dept_id IN");
|
||||
assertThat(sql).contains("SELECT");
|
||||
assertThat(sql).contains("sys_dept");
|
||||
assertThat(sql).contains("FIND_IN_SET");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 多角色并集策略测试 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("多角色并集策略测试")
|
||||
class MultiRoleTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("多角色 - 任一角色为ALL时跳过过滤")
|
||||
void whenAnyRoleIsAll_thenSkipFilter() {
|
||||
// given: 用户有两个角色,其中一个是ALL
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER")),
|
||||
List.of(
|
||||
RoleDataScope.all("ADMIN"),
|
||||
RoleDataScope.self("USER")
|
||||
));
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 返回原始where,不添加过滤条件
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多角色 - DEPT和SELF权限合并为OR条件")
|
||||
void whenDeptAndSelfRoles_thenMergeWithOr() {
|
||||
// given: 用户有两个角色,分别拥有部门和本人权限
|
||||
Long deptId = 10L;
|
||||
Long userId = 100L;
|
||||
setSecurityContext(userId, "manager", deptId,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_MANAGER"), new SimpleGrantedAuthority("ROLE_USER")),
|
||||
List.of(
|
||||
RoleDataScope.dept("MANAGER"),
|
||||
RoleDataScope.self("USER")
|
||||
));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 生成OR连接的合并条件
|
||||
assertThat(result).isNotNull();
|
||||
String sql = result.toString();
|
||||
assertThat(sql).contains("OR");
|
||||
assertThat(sql).contains("dept_id = " + deptId);
|
||||
assertThat(sql).contains("create_by = " + userId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多角色 - 多个自定义部门权限合并")
|
||||
void whenMultipleCustomRoles_thenMergeWithOr() {
|
||||
// given: 用户有两个自定义部门权限的角色
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_A"), new SimpleGrantedAuthority("ROLE_B")),
|
||||
List.of(
|
||||
RoleDataScope.custom("A", Arrays.asList(10L, 20L)),
|
||||
RoleDataScope.custom("B", Arrays.asList(30L, 40L))
|
||||
));
|
||||
|
||||
Expression where = null;
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 生成OR连接的IN条件
|
||||
assertThat(result).isNotNull();
|
||||
String sql = result.toString();
|
||||
assertThat(sql).contains("OR");
|
||||
assertThat(sql).contains("dept_id IN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已有where条件 - 新条件用AND连接")
|
||||
void whenExistingWhere_thenAndWithNewCondition() {
|
||||
// given
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
List.of(RoleDataScope.dept("USER")));
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"com.youlai.boot.plugin.mybatis.TestDataPermissionMapper.selectList");
|
||||
|
||||
// then: 结果应该包含原始where和数据权限条件
|
||||
assertThat(result).isInstanceOf(AndExpression.class);
|
||||
String sql = result.toString();
|
||||
assertThat(sql).contains("status = 1");
|
||||
assertThat(sql).contains("dept_id = 10");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 注解配置测试 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("注解配置测试")
|
||||
class AnnotationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("无@DataPermission注解 - 返回原始where")
|
||||
void whenNoAnnotation_thenReturnOriginalWhere() {
|
||||
// given
|
||||
setSecurityContext(100L, "user", 10L,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_USER")),
|
||||
List.of(RoleDataScope.dept("USER")));
|
||||
|
||||
Expression where = new EqualsTo(new Column("status"), new net.sf.jsqlparser.expression.LongValue(1));
|
||||
|
||||
// when: 调用无注解的mapper方法
|
||||
Expression result = handler.getSqlSegment(where,
|
||||
"java.lang.Object.toString");
|
||||
|
||||
// then: 返回原始where
|
||||
assertThat(result).isSameAs(where);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 设置安全上下文
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param deptId 部门ID
|
||||
* @param authorities 权限集合
|
||||
* @param dataScopes 数据权限列表
|
||||
*/
|
||||
private void setSecurityContext(Long userId, String username, Long deptId,
|
||||
Set<SimpleGrantedAuthority> authorities,
|
||||
List<RoleDataScope> dataScopes) {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(userId);
|
||||
userDetails.setUsername(username);
|
||||
userDetails.setDeptId(deptId);
|
||||
userDetails.setDataScopes(dataScopes);
|
||||
userDetails.setAuthorities(authorities);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.youlai.boot.plugin.mybatis;
|
||||
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限测试 Mapper
|
||||
* <p>
|
||||
* 用于测试数据权限拦截器的SQL注入功能
|
||||
*/
|
||||
public interface TestDataPermissionMapper {
|
||||
|
||||
/**
|
||||
* 查询列表(带数据权限过滤)
|
||||
*/
|
||||
@DataPermission
|
||||
List<Object> selectList();
|
||||
|
||||
/**
|
||||
* 查询列表(不带数据权限过滤)
|
||||
*/
|
||||
List<Object> selectListWithoutPermission();
|
||||
|
||||
/**
|
||||
* 多表关联查询(指定别名)
|
||||
*/
|
||||
@DataPermission(deptAlias = "u", userAlias = "u")
|
||||
List<Object> selectWithJoin();
|
||||
|
||||
/**
|
||||
* 自定义列名查询(多表关联场景)
|
||||
*/
|
||||
@DataPermission(
|
||||
deptAlias = "t",
|
||||
deptIdColumnName = "dept_id",
|
||||
userAlias = "t",
|
||||
userIdColumnName = "create_by"
|
||||
)
|
||||
List<Object> selectWithAlias();
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package com.youlai.boot.security.token;
|
||||
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.security.model.RoleDataScope;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* JwtTokenManager 单元测试
|
||||
*
|
||||
* @author Ray.Hao
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtTokenManagerTest {
|
||||
|
||||
@Mock
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private ValueOperations<String, Object> valueOperations;
|
||||
|
||||
private JwtTokenManager tokenManager;
|
||||
private SecurityProperties securityProperties;
|
||||
|
||||
private static final String TEST_SECRET_KEY = "TestSecretKey01234567890123456789";
|
||||
private static final int ACCESS_TOKEN_TTL = 3600;
|
||||
private static final int REFRESH_TOKEN_TTL = 604800;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
securityProperties = createSecurityProperties();
|
||||
|
||||
lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
lenient().when(redisTemplate.hasKey(anyString())).thenReturn(false);
|
||||
|
||||
tokenManager = new JwtTokenManager(securityProperties, redisTemplate);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 生成测试")
|
||||
class GenerateTokenTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("应成功生成有效的访问令牌和刷新令牌")
|
||||
void should_generate_valid_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getAccessToken()).isNotBlank();
|
||||
assertThat(token.getRefreshToken()).isNotBlank();
|
||||
assertThat(token.getTokenType()).isEqualTo("Bearer");
|
||||
assertThat(token.getExpiresIn()).isEqualTo(ACCESS_TOKEN_TTL);
|
||||
assertThat(token.getAccessToken()).isNotEqualTo(token.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生成的 Token 应包含用户信息")
|
||||
void should_contain_user_info() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
Authentication parsed = tokenManager.parseToken(token.getAccessToken());
|
||||
|
||||
assertThat(parsed).isNotNull();
|
||||
assertThat(parsed.getName()).isEqualTo("testuser");
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) parsed.getPrincipal();
|
||||
assertThat(userDetails.getUserId()).isEqualTo(1L);
|
||||
assertThat(userDetails.getDeptId()).isEqualTo(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生成的 Token 应包含角色权限")
|
||||
void should_contain_authorities() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
Authentication parsed = tokenManager.parseToken(token.getAccessToken());
|
||||
|
||||
Collection<?> authorities = parsed.getAuthorities();
|
||||
assertThat(authorities).hasSize(2);
|
||||
assertThat(authorities)
|
||||
.extracting("authority")
|
||||
.containsExactlyInAnyOrder("ROLE_ADMIN", "ROLE_USER");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("生成的 Token 应包含数据权限")
|
||||
void should_contain_data_scopes() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
Authentication parsed = tokenManager.parseToken(token.getAccessToken());
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) parsed.getPrincipal();
|
||||
List<RoleDataScope> dataScopes = userDetails.getDataScopes();
|
||||
|
||||
assertThat(dataScopes).hasSize(2);
|
||||
assertThat(dataScopes.get(0).getRoleCode()).isEqualTo("ADMIN");
|
||||
assertThat(dataScopes.get(0).getDataScope()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 校验测试")
|
||||
class ValidateTokenTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("有效 Token 校验应返回 true")
|
||||
void should_validate_valid_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
boolean isValid = tokenManager.validateToken(token.getAccessToken());
|
||||
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无效签名 Token 校验应返回 false")
|
||||
void should_reject_invalid_signature() {
|
||||
String invalidToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
|
||||
|
||||
boolean isValid = tokenManager.validateToken(invalidToken);
|
||||
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("刷新令牌校验应区分访问令牌")
|
||||
void should_distinguish_refresh_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
// 访问令牌不能作为刷新令牌使用
|
||||
boolean isValidRefreshToken = tokenManager.validateRefreshToken(token.getAccessToken());
|
||||
assertThat(isValidRefreshToken).isFalse();
|
||||
|
||||
// 刷新令牌是有效的
|
||||
boolean isValidRefreshToken2 = tokenManager.validateRefreshToken(token.getRefreshToken());
|
||||
assertThat(isValidRefreshToken2).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 撤销测试")
|
||||
class InvalidateTokenTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("撤销 Token 后校验应返回 false")
|
||||
void should_invalidate_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
// 先验证 Token 有效
|
||||
assertThat(tokenManager.validateToken(token.getAccessToken())).isTrue();
|
||||
|
||||
// 撤销 Token
|
||||
tokenManager.invalidateToken(token.getAccessToken());
|
||||
|
||||
// 验证 Redis 存储了撤销标记
|
||||
verify(valueOperations).set(anyString(), any(Boolean.class), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("撤销空 Token 应安全处理")
|
||||
void should_handle_null_token() {
|
||||
tokenManager.invalidateToken(null);
|
||||
tokenManager.invalidateToken("");
|
||||
|
||||
verify(valueOperations, never()).set(anyString(), any(), anyLong(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("用户会话失效测试")
|
||||
class InvalidateUserSessionsTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("失效用户所有会话后,旧 Token 应无效")
|
||||
void should_invalidate_user_sessions() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
// 模拟用户会话失效后的 Redis 状态
|
||||
when(valueOperations.get(anyString())).thenReturn(System.currentTimeMillis() / 1000 + 1000);
|
||||
|
||||
// 验证 Token 已失效
|
||||
boolean isValid = tokenManager.validateToken(token.getAccessToken());
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("失效空用户 ID 应安全处理")
|
||||
void should_handle_null_user_id() {
|
||||
tokenManager.invalidateUserSessions(null);
|
||||
|
||||
verify(valueOperations, never()).set(anyString(), any(), anyLong(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 刷新测试")
|
||||
class RefreshTokenTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("应成功刷新访问令牌")
|
||||
void should_refresh_access_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken originalToken = tokenManager.generateToken(authentication);
|
||||
|
||||
AuthenticationToken refreshedToken = tokenManager.refreshToken(originalToken.getRefreshToken());
|
||||
|
||||
assertThat(refreshedToken).isNotNull();
|
||||
assertThat(refreshedToken.getAccessToken()).isNotBlank();
|
||||
assertThat(refreshedToken.getAccessToken()).isNotEqualTo(originalToken.getAccessToken());
|
||||
assertThat(refreshedToken.getRefreshToken()).isEqualTo(originalToken.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("使用访问令牌刷新应失败")
|
||||
void should_fail_with_access_token() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
// 使用访问令牌尝试刷新
|
||||
boolean isValidRefresh = tokenManager.validateRefreshToken(token.getAccessToken());
|
||||
assertThat(isValidRefresh).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Token 解析测试")
|
||||
class ParseTokenTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("应正确解析 Token 中的用户信息")
|
||||
void should_parse_user_details() {
|
||||
Authentication authentication = createTestAuthentication();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
Authentication parsed = tokenManager.parseToken(token.getAccessToken());
|
||||
|
||||
assertThat(parsed).isInstanceOf(UsernamePasswordAuthenticationToken.class);
|
||||
assertThat(parsed.getPrincipal()).isInstanceOf(SysUserDetails.class);
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) parsed.getPrincipal();
|
||||
assertThat(userDetails.getUserId()).isEqualTo(1L);
|
||||
assertThat(userDetails.getDeptId()).isEqualTo(100L);
|
||||
assertThat(userDetails.getUsername()).isEqualTo("testuser");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应正确解析自定义部门数据权限")
|
||||
void should_parse_custom_data_scope() {
|
||||
Authentication authentication = createTestAuthenticationWithCustomScope();
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
|
||||
Authentication parsed = tokenManager.parseToken(token.getAccessToken());
|
||||
SysUserDetails userDetails = (SysUserDetails) parsed.getPrincipal();
|
||||
|
||||
List<RoleDataScope> dataScopes = userDetails.getDataScopes();
|
||||
assertThat(dataScopes).hasSize(1);
|
||||
assertThat(dataScopes.get(0).getCustomDeptIds()).containsExactly(10L, 20L, 30L);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 测试数据构建方法 ==========
|
||||
|
||||
private SecurityProperties createSecurityProperties() {
|
||||
SecurityProperties properties = new SecurityProperties();
|
||||
SecurityProperties.SessionConfig sessionConfig = new SecurityProperties.SessionConfig();
|
||||
sessionConfig.setType("jwt");
|
||||
sessionConfig.setAccessTokenTimeToLive(ACCESS_TOKEN_TTL);
|
||||
sessionConfig.setRefreshTokenTimeToLive(REFRESH_TOKEN_TTL);
|
||||
|
||||
SecurityProperties.JwtConfig jwtConfig = new SecurityProperties.JwtConfig();
|
||||
jwtConfig.setSecretKey(TEST_SECRET_KEY);
|
||||
sessionConfig.setJwt(jwtConfig);
|
||||
|
||||
properties.setSession(sessionConfig);
|
||||
properties.setIgnoreUrls(new String[]{"/api/v1/auth/login/**"});
|
||||
properties.setUnsecuredUrls(new String[]{"/doc.html"});
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Authentication createTestAuthentication() {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(1L);
|
||||
userDetails.setUsername("testuser");
|
||||
userDetails.setDeptId(100L);
|
||||
userDetails.setEnabled(true);
|
||||
userDetails.setDataScopes(List.of(
|
||||
new RoleDataScope("ADMIN", 1, null), // 全部数据权限
|
||||
new RoleDataScope("USER", 4, null) // 本人数据权限
|
||||
));
|
||||
userDetails.setAuthorities(Set.of(
|
||||
new SimpleGrantedAuthority("ROLE_ADMIN"),
|
||||
new SimpleGrantedAuthority("ROLE_USER")
|
||||
));
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
private Authentication createTestAuthenticationWithCustomScope() {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(2L);
|
||||
userDetails.setUsername("customuser");
|
||||
userDetails.setDeptId(200L);
|
||||
userDetails.setEnabled(true);
|
||||
userDetails.setDataScopes(List.of(
|
||||
new RoleDataScope("CUSTOM_ROLE", 5, List.of(10L, 20L, 30L)) // 自定义部门权限
|
||||
));
|
||||
userDetails.setAuthorities(Set.of(
|
||||
new SimpleGrantedAuthority("ROLE_CUSTOM")
|
||||
));
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user