From 0e7918b31e7b0401098f675ec00450abe47cedac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Wed, 3 Jun 2026 11:24:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20Gateway=20JwtAuthenticationFilter?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=E8=B7=AF=E5=BE=84-userType=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/admin/** 路径只允许 userType=ADMIN 访问 - /api/member/** 路径只允许 userType=MEMBER 访问 - 越权访问返回 403 Forbidden - 请求头增加 X-User-Type 传递 userType - isPublicPath 统一 /api/member/auth/ 为公开路径 - 补充 userType 路径校验相关单元测试 --- .../filter/JwtAuthenticationFilter.java | 28 ++- .../GatewayJwtAuthenticationFilterTest.java | 160 ++++++++++++++++++ 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index 54c4a4e..07754f0 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -42,6 +42,13 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory 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 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 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 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 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 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 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 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 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 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 result = filter.apply(new JwtAuthenticationFilter.Config()) + .filter(exchange, chain); + + StepVerifier.create(result) + .verifyComplete(); + + verify(chain).filter(any(ServerWebExchange.class)); + } }