feat(auth): Gateway JwtAuthenticationFilter 增加路径-userType 校验
- /api/admin/** 路径只允许 userType=ADMIN 访问 - /api/member/** 路径只允许 userType=MEMBER 访问 - 越权访问返回 403 Forbidden - 请求头增加 X-User-Type 传递 userType - isPublicPath 统一 /api/member/auth/ 为公开路径 - 补充 userType 路径校验相关单元测试
This commit is contained in:
+26
-2
@@ -42,6 +42,13 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
||||
return exchange.getResponse().setComplete();
|
||||
}
|
||||
|
||||
// 路径-userType 校验:防止越权访问
|
||||
String userType = jwtUtil.getUserTypeFromToken(token);
|
||||
if (!isUserTypeAllowedForPath(path, userType)) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
|
||||
return exchange.getResponse().setComplete();
|
||||
}
|
||||
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
@@ -49,17 +56,34 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
||||
.header("X-User-Id", String.valueOf(userId))
|
||||
.header("X-Member-Id", String.valueOf(userId))
|
||||
.header("X-Username", username)
|
||||
.header("X-User-Type", userType != null ? userType : "UNKNOWN")
|
||||
.build();
|
||||
|
||||
return chain.filter(exchange.mutate().request(modifiedRequest).build());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验路径与 userType 是否匹配
|
||||
* - /api/admin/** 路径只允许 userType=ADMIN
|
||||
* - /api/member/** 路径只允许 userType=MEMBER
|
||||
* - 其他路径不做 userType 校验
|
||||
*/
|
||||
private boolean isUserTypeAllowedForPath(String path, String userType) {
|
||||
if (path.startsWith("/api/admin/")) {
|
||||
return "ADMIN".equals(userType);
|
||||
}
|
||||
if (path.startsWith("/api/member/")) {
|
||||
return "MEMBER".equals(userType);
|
||||
}
|
||||
// 非特定前缀路径不做 userType 校验
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isPublicPath(String path) {
|
||||
return path.startsWith("/api/auth/") ||
|
||||
path.equals("/actuator/health") ||
|
||||
path.equals("/api/member/auth/miniapp/login") ||
|
||||
path.equals("/api/member/auth/mp/callback") ||
|
||||
path.startsWith("/api/member/auth/") ||
|
||||
path.equals("/api/auth/login") ||
|
||||
path.startsWith("/api/checkIn/") ||
|
||||
path.startsWith("/actuator/info");
|
||||
|
||||
+160
@@ -105,6 +105,40 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_MemberAuth() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/member/auth/miniapp/login").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_CheckIn() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/checkIn/").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_NoAuthHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
||||
@@ -152,6 +186,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
@@ -163,6 +198,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
verify(jwtUtil).isTokenExpired(validToken);
|
||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(jwtUtil).getUserTypeFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@@ -224,6 +260,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
@@ -235,6 +272,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
verify(jwtUtil).isTokenExpired(validToken);
|
||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(jwtUtil).getUserTypeFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@@ -251,6 +289,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
@@ -263,6 +302,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
|
||||
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
|
||||
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
|
||||
assert modifiedRequest.getHeaders().getFirst("X-User-Type").equals("ADMIN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -295,6 +335,7 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(validToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
@@ -308,4 +349,123 @@ class GatewayJwtAuthenticationFilterTest {
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
// ========== userType 路径校验测试 ==========
|
||||
|
||||
@Test
|
||||
void testAdminPath_WithAdminToken_ShouldPass() {
|
||||
String adminToken = "admin.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(adminToken)).thenReturn("admin");
|
||||
when(jwtUtil.getUserIdFromToken(adminToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdminPath_WithMemberToken_ShouldBeForbidden() {
|
||||
String memberToken = "member.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/admin/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + memberToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(jwtUtil.validateToken(memberToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(memberToken)).thenReturn(false);
|
||||
when(jwtUtil.getUserTypeFromToken(memberToken)).thenReturn("MEMBER");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMemberPath_WithMemberToken_ShouldPass() {
|
||||
String memberToken = "member.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/member/info")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + memberToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(memberToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(memberToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(memberToken)).thenReturn("123");
|
||||
when(jwtUtil.getUserIdFromToken(memberToken)).thenReturn(123L);
|
||||
when(jwtUtil.getUserTypeFromToken(memberToken)).thenReturn("MEMBER");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMemberPath_WithAdminToken_ShouldBeForbidden() {
|
||||
String adminToken = "admin.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/member/info")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNonPrefixedPath_NoUserTypeCheck() {
|
||||
String adminToken = "admin.jwt.token";
|
||||
// /api/users 不以 /api/admin/ 或 /api/member/ 开头,不做 userType 校验
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(adminToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(adminToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(adminToken)).thenReturn("admin");
|
||||
when(jwtUtil.getUserIdFromToken(adminToken)).thenReturn(1L);
|
||||
when(jwtUtil.getUserTypeFromToken(adminToken)).thenReturn("ADMIN");
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user