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:
张翔
2026-06-03 11:24:24 +08:00
parent 0e73bd4520
commit 0e7918b31e
2 changed files with 186 additions and 2 deletions
@@ -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");
@@ -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));
}
}