diff --git a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidator.java b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidator.java index fb43861f..2b3f7d9c 100644 --- a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidator.java +++ b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidator.java @@ -9,9 +9,9 @@ import org.springframework.core.env.Environment; /** * 内部狗粮认证启动不变量校验。 * - *
mock token 会绕过真实 OAuth2 token 校验,因此只要狗粮总开关开启,
- * mock 认证也必须关闭。该校验在应用接流量前 fail-closed,防止 Nacos 或命令行
- * 覆盖 staging 文件后重新打开 {@code test
mock token 与 login-user 请求头都能绕过真实 OAuth2 token 校验,因此只要狗粮总开关开启, + * 两条认证捷径都必须关闭。该校验在应用接流量前 fail-closed,防止 Nacos 或命令行 + * 覆盖 staging 文件后重新打开伪造身份通道。
*/ @Slf4j @RequiredArgsConstructor @@ -31,7 +31,12 @@ public class DogfoodSecurityStartupValidator implements ApplicationRunner { log.error("[dogfoodSecurityStartup] 狗粮态认证配置拒绝启动 key=huijing.security.mock-enable expected=false"); throw new IllegalStateException("狗粮态要求 huijing.security.mock-enable=false"); } - log.info("[dogfoodSecurityStartup] 狗粮态 mock 认证已关闭"); + if (Boolean.TRUE.equals(securityProperties.getLoginUserHeaderEnabled())) { + log.error("[dogfoodSecurityStartup] 狗粮态认证配置拒绝启动 " + + "key=huijing.security.login-user-header-enabled expected=false"); + throw new IllegalStateException("狗粮态要求 huijing.security.login-user-header-enabled=false"); + } + log.info("[dogfoodSecurityStartup] 狗粮态 mock 认证与 login-user 请求头信任均已关闭"); } } diff --git a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/SecurityProperties.java b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/SecurityProperties.java index f9279d72..ad6bd94f 100644 --- a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/SecurityProperties.java +++ b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/config/SecurityProperties.java @@ -27,6 +27,15 @@ public class SecurityProperties { @NotEmpty(message = "Token Parameter 不能为空") private String tokenParameter = "token"; + /** + * 是否信任服务间透传的 login-user 请求头。 + * + *默认开启以保持现有微服务 RPC 行为;外部流量可直达服务的环境必须显式关闭, + * 避免客户端伪造该请求头绕过 OAuth2 token 校验。
+ */ + @NotNull(message = "login-user 请求头信任开关不能为空") + private Boolean loginUserHeaderEnabled = true; + /** * mock 模式的开关 */ diff --git a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilter.java b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilter.java index 794c4893..a88f6ae4 100644 --- a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilter.java +++ b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/main/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilter.java @@ -130,6 +130,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { private LoginUser buildLoginUserByHeader(HttpServletRequest request) { String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER); + if (!Boolean.TRUE.equals(securityProperties.getLoginUserHeaderEnabled())) { + // 不解析、不记录客户端可控的身份头,避免认证绕过与恶意请求放大日志量。 + return null; + } if (StrUtil.isEmpty(loginUserStr)) { return null; } diff --git a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidatorTest.java b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidatorTest.java index b9e2bc12..1a0ce96c 100644 --- a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidatorTest.java +++ b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/config/DogfoodSecurityStartupValidatorTest.java @@ -20,6 +20,32 @@ class DogfoodSecurityStartupValidatorTest { () -> new DogfoodSecurityStartupValidator(properties, environment).run(null)); } + /** 狗粮态必须拒绝重新开启服务间身份头信任。 */ + @Test + void shouldRejectLoginUserHeaderTrustWhenDogfoodIsEnabled() { + SecurityProperties properties = new SecurityProperties(); + properties.setMockEnable(false); + properties.setLoginUserHeaderEnabled(true); + MockEnvironment environment = new MockEnvironment() + .withProperty("huijing.dogfood.enabled", "true"); + + // 单体狗粮环境可被外部直达,服务间身份头不能作为认证凭据。 + assertThrows(IllegalStateException.class, + () -> new DogfoodSecurityStartupValidator(properties, environment).run(null)); + } + + /** mock 与身份头信任均关闭时,狗粮认证配置允许启动。 */ + @Test + void shouldAcceptSafeAuthenticationConfigurationWhenDogfoodIsEnabled() { + SecurityProperties properties = new SecurityProperties(); + properties.setMockEnable(false); + properties.setLoginUserHeaderEnabled(false); + MockEnvironment environment = new MockEnvironment() + .withProperty("huijing.dogfood.enabled", "true"); + + assertDoesNotThrow(() -> new DogfoodSecurityStartupValidator(properties, environment).run(null)); + } + @Test void shouldAllowMockAuthenticationOutsideDogfood() { SecurityProperties properties = new SecurityProperties(); diff --git a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilterTest.java b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilterTest.java index e3a99d15..61a29f62 100644 --- a/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilterTest.java +++ b/game-cloud/huijing-framework/huijing-spring-boot-starter-security/src/test/java/com/wanxiang/huijing/framework/security/core/filter/TokenAuthenticationFilterTest.java @@ -2,7 +2,9 @@ package com.wanxiang.huijing.framework.security.core.filter; import com.wanxiang.huijing.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; import com.wanxiang.huijing.framework.common.pojo.CommonResult; +import com.wanxiang.huijing.framework.common.util.json.JsonUtils; import com.wanxiang.huijing.framework.security.config.SecurityProperties; +import com.wanxiang.huijing.framework.security.core.LoginUser; import com.wanxiang.huijing.framework.security.core.handler.AuthenticationEntryPointImpl; import com.wanxiang.huijing.framework.security.core.util.SecurityFrameworkUtils; import com.wanxiang.huijing.framework.web.config.WebProperties; @@ -16,7 +18,11 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -61,4 +67,52 @@ class TokenAuthenticationFilterTest { assertNull(SecurityFrameworkUtils.getLoginUser()); } + /** 验证关闭身份头信任后,伪造头不能让受保护接口获得登录态。 */ + @Test + void forgedLoginUserHeaderShouldRemainUnauthenticatedWhenHeaderTrustIsDisabled() throws Exception { + SecurityProperties properties = new SecurityProperties(); + properties.setLoginUserHeaderEnabled(false); + TokenAuthenticationFilter filter = new TokenAuthenticationFilter( + properties, mock(GlobalExceptionHandler.class), mock(OAuth2TokenCommonApi.class)); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app-api/protected"); + request.addHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER, encodeLoginUserHeader(1L)); + MockHttpServletResponse response = new MockHttpServletResponse(); + AuthenticationEntryPointImpl authenticationEntryPoint = new AuthenticationEntryPointImpl(); + + filter.doFilter(request, response, (currentRequest, currentResponse) -> { + // staging 不信任客户端可控的服务间身份头,受保护接口必须沿匿名路径返回 401。 + assertNull(SecurityFrameworkUtils.getLoginUser()); + authenticationEntryPoint.commence(request, response, + new InsufficientAuthenticationException("未认证")); + }); + + assertEquals(401, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"code\":401")); + assertNull(SecurityFrameworkUtils.getLoginUser()); + } + + /** 验证默认配置继续接受既有 Feign RPC 透传身份。 */ + @Test + void loginUserHeaderShouldKeepExistingRpcBehaviorByDefault() throws Exception { + SecurityProperties properties = new SecurityProperties(); + TokenAuthenticationFilter filter = new TokenAuthenticationFilter( + properties, mock(GlobalExceptionHandler.class), mock(OAuth2TokenCommonApi.class)); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app-api/protected"); + request.addHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER, encodeLoginUserHeader(7L)); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, (currentRequest, currentResponse) -> { + // 非 staging profile 未覆盖配置时继续接受 Feign 透传身份,避免破坏既有 RPC 调用。 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + assertNotNull(loginUser); + assertEquals(7L, loginUser.getId()); + }); + } + + /** 构造与 Feign RPC 拦截器相同格式的服务间身份头。 */ + private static String encodeLoginUserHeader(Long userId) { + LoginUser loginUser = new LoginUser().setId(userId).setUserType(1); + return URLEncoder.encode(JsonUtils.toJsonString(loginUser), StandardCharsets.UTF_8); + } + } diff --git a/game-cloud/huijing-server/src/main/java/com/wanxiang/huijing/server/config/DogfoodStartupValidator.java b/game-cloud/huijing-server/src/main/java/com/wanxiang/huijing/server/config/DogfoodStartupValidator.java index 81db4976..8e6c7e33 100644 --- a/game-cloud/huijing-server/src/main/java/com/wanxiang/huijing/server/config/DogfoodStartupValidator.java +++ b/game-cloud/huijing-server/src/main/java/com/wanxiang/huijing/server/config/DogfoodStartupValidator.java @@ -2,10 +2,11 @@ package com.wanxiang.huijing.server.config; import com.wanxiang.huijing.framework.env.config.DogfoodProperties; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.core.env.Environment; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.env.Profiles; import org.springframework.stereotype.Component; import java.time.Clock; @@ -38,6 +39,11 @@ public class DogfoodStartupValidator implements ApplicationRunner { @Override public void run(ApplicationArguments args) { + if (environment.acceptsProfiles(Profiles.of("staging")) && !dogfoodProperties.isEnabled()) { + // active profile 是部署边界,不能由同一配置源关闭狗粮总开关后绕过其余守卫。 + log.error("[dogfoodStartup] staging 拒绝启动 key=huijing.dogfood.enabled expected=true"); + throw new IllegalStateException("staging 环境要求 huijing.dogfood.enabled=true"); + } if (!dogfoodProperties.isEnabled()) { return; } diff --git a/game-cloud/huijing-server/src/main/resources/application-staging.yaml b/game-cloud/huijing-server/src/main/resources/application-staging.yaml index 00c3587d..0e931961 100644 --- a/game-cloud/huijing-server/src/main/resources/application-staging.yaml +++ b/game-cloud/huijing-server/src/main/resources/application-staging.yaml @@ -169,6 +169,7 @@ huijing: enable: false security: mock-enable: false # 狗粮态禁止 test