diff --git a/.gitignore b/.gitignore index 8f40fbe9..25c9bd27 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ # Common target/ -build/ \ No newline at end of file +build/ + +# Claude Code local settings +.claude/settings.local.json \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/AUTH_USAGE_GUIDE.md b/examples/fit-example/07-http-client-proxy/AUTH_USAGE_GUIDE.md new file mode 100644 index 00000000..d7182033 --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/AUTH_USAGE_GUIDE.md @@ -0,0 +1,211 @@ +# HTTP Client Authentication Usage Guide + +本文档演示了 fit-framework HTTP 客户端代理系统中各种身份认证方式的使用方法。 + +## 1. 概述 + +`@RequestAuth` 注解提供了统一的身份认证解决方案,支持多种认证类型和应用级别: + +### 认证类型 (AuthType) +- **BEARER**: Bearer Token 认证 +- **BASIC**: HTTP Basic 认证 +- **API_KEY**: API Key 认证(支持 Header、Query、Cookie) +- **CUSTOM**: 自定义认证(通过 Provider) + +### 应用级别 +- **接口级别**: 应用于整个接口的所有方法 +- **方法级别**: 应用于特定方法(会覆盖接口级别) +- **参数级别**: 通过方法参数动态设置(最高优先级) + +## 2. 静态认证配置 + +### 2.1 Bearer Token 认证 + +```java +// 接口级别静态配置 +@RequestAuth(type = AuthType.BEARER, value = "your-static-token") +public interface YourClient { + + // 方法级别覆盖 + @RequestAuth(type = AuthType.BEARER, value = "method-specific-token") + String someMethod(); +} +``` + +### 2.2 Basic 认证 + +```java +@RequestAuth(type = AuthType.BASIC, username = "admin", password = "secret") +String basicAuthMethod(); +``` + +### 2.3 API Key 认证 + +```java +// Header 中的 API Key +@RequestAuth(type = AuthType.API_KEY, name = "X-API-Key", value = "your-api-key") +String headerApiKeyMethod(); + +// Query 参数中的 API Key +@RequestAuth(type = AuthType.API_KEY, name = "api_key", value = "your-key", location = Source.QUERY) +String queryApiKeyMethod(); +``` + +## 3. 动态认证配置 + +### 3.1 参数驱动的认证 + +```java +// 动态 Bearer Token +String dynamicBearer(@RequestAuth(type = AuthType.BEARER) String token); + +// 动态 API Key +String dynamicApiKey(@RequestAuth(type = AuthType.API_KEY, name = "X-Dynamic-Key") String apiKey); +``` + +### 3.2 Provider 模式 + +#### 创建 Provider + +```java +@Component +public class DynamicTokenProvider implements AuthProvider { + @Override + public Authorization provide() { + // 从 TokenManager、缓存或其他来源获取 token + String token = TokenManager.getCurrentToken(); + return Authorization.createBearer(token); + } +} +``` + +#### 使用 Provider + +```java +@RequestAuth(type = AuthType.BEARER, provider = DynamicTokenProvider.class) +String providerBasedMethod(); +``` + +## 4. 组合认证 + +可以在不同级别同时应用多种认证: + +```java +@HttpProxy +@RequestAddress(protocol = "http", host = "localhost", port = "8080") +// 接口级别:默认 API Key +@RequestAuth(type = AuthType.API_KEY, name = "X-Service-Key", value = "service-key") +public interface CombinedAuthClient { + + // 方法级别:添加 Bearer Token(会与接口级别的 API Key 共存) + @RequestAuth(type = AuthType.BEARER, provider = TokenProvider.class) + String combinedAuth( + // 参数级别:用户上下文 API Key + @RequestAuth(type = AuthType.API_KEY, name = "X-User-Context") String userToken + ); +} +``` + +## 5. 完整示例 + +### TestAuthClient 接口 + +```java +@HttpProxy +@RequestAddress(protocol = "http", host = "localhost", port = "8080") +@RequestMapping(path = "/http-server/auth") +@RequestAuth(type = AuthType.API_KEY, name = "X-Service-Key", value = "service-default-key") +public interface TestAuthClient { + + // 1. 静态 Bearer Token + @GetMapping(path = "/bearer-static") + @RequestAuth(type = AuthType.BEARER, value = "static-bearer-token-12345") + String testBearerStatic(); + + // 2. 动态 Bearer Token + @GetMapping(path = "/bearer-dynamic") + String testBearerDynamic(@RequestAuth(type = AuthType.BEARER) String token); + + // 3. Basic 认证 + @GetMapping(path = "/basic-static") + @RequestAuth(type = AuthType.BASIC, username = "admin", password = "secret123") + String testBasicStatic(); + + // 4. Header API Key + @GetMapping(path = "/apikey-header-static") + @RequestAuth(type = AuthType.API_KEY, name = "X-API-Key", value = "static-api-key-67890") + String testApiKeyHeaderStatic(); + + // 5. Query API Key + @GetMapping(path = "/apikey-query-static") + @RequestAuth(type = AuthType.API_KEY, name = "api_key", value = "query-api-key-111", location = Source.QUERY) + String testApiKeyQueryStatic(); + + // 6. 动态 API Key + @GetMapping(path = "/apikey-dynamic") + String testApiKeyDynamic(@RequestAuth(type = AuthType.API_KEY, name = "X-Dynamic-Key") String apiKey); + + // 7. Provider 模式 + @GetMapping(path = "/dynamic-provider") + @RequestAuth(type = AuthType.BEARER, provider = DynamicTokenProvider.class) + String testDynamicProvider(); + + // 8. 自定义认证 + @GetMapping(path = "/custom-provider") + @RequestAuth(type = AuthType.CUSTOM, provider = CustomSignatureProvider.class) + String testCustomProvider(); + + // 9. 组合认证 + @GetMapping(path = "/combined-auth") + @RequestAuth(type = AuthType.BEARER, provider = DynamicTokenProvider.class) + String testCombinedAuth(@RequestAuth(type = AuthType.API_KEY, name = "X-User-Context") String userToken); +} +``` + +## 6. 注意事项 + +1. **优先级**: 参数级别 > 方法级别 > 接口级别 +2. **Provider**: 需要标记为 `@Component` 并在容器中可用 +3. **组合认证**: 不同级别的认证会叠加,相同级别的认证会覆盖 +4. **安全性**: 避免在代码中硬编码敏感信息,优先使用 Provider 模式 + +## 7. 快速启动和测试 + +### 启动应用 + +本示例基于 FIT 框架,启动方式如下: + +```bash +# 1. 编译整个项目(在 fit-framework 根目录) +mvn clean install + +# 2. 启动服务器端 +# 方式一:在 IDEA 中运行 plugin-http-server 模块的 main 方法 +# 方式二:命令行运行 JAR 文件(编译后在 target 目录) +java -jar plugin-http-server/target/plugin-http-server-*.jar +``` + +### 验证启动成功 + +查看日志中是否包含以下信息: + +``` +[INFO] [main] [modelengine.fitframework.runtime.aggregated.AggregatedFitRuntime] FIT application started. +[INFO] [netty-http-server-thread-0] [modelengine.fit.http.server.netty.NettyHttpClassicServer] Start netty http server successfully. [httpPort=8080] +``` + +### 快速测试 + +```bash +# 测试基本连接 +curl http://localhost:8080/http-server/auth/bearer-static \ + -H "Authorization: Bearer static-bearer-token-12345" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Bearer Static Auth: Bearer static-bearer-token-12345 +``` + +## 8. 下一步 + +- 查看 [CURL_TEST_EXAMPLES.md](./CURL_TEST_EXAMPLES.md) 了解如何测试这些认证场景 +- 查看 [run_tests.sh](./run_tests.sh) 了解如何批量执行测试 \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/CURL_TEST_EXAMPLES.md b/examples/fit-example/07-http-client-proxy/CURL_TEST_EXAMPLES.md new file mode 100644 index 00000000..dfb0d83a --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/CURL_TEST_EXAMPLES.md @@ -0,0 +1,205 @@ +# CURL 测试用例 + +本文档提供了用于测试 HTTP 客户端认证功能的 curl 命令示例。 + +## 前置条件 + +1. 编译整个项目:在项目根目录执行 `mvn clean install` +2. 启动服务器端:按照 [README](../../../README.md) 中的说明启动服务器 +3. 确保服务器运行在 `http://localhost:8080`(FIT 框架默认端口) + +## 测试用例 + +### 1. Bearer Token 静态认证 + +```bash +# 测试静态 Bearer Token +curl -X GET "http://localhost:8080/http-server/auth/bearer-static" \ + -H "Authorization: Bearer static-bearer-token-12345" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Bearer Static Auth: Bearer static-bearer-token-12345 +``` + +### 2. Bearer Token 动态认证 + +```bash +# 测试动态 Bearer Token +curl -X GET "http://localhost:8080/http-server/auth/bearer-dynamic" \ + -H "Authorization: Bearer dynamic-bearer-token-67890" + +# 期望响应:Bearer Dynamic Auth: Bearer dynamic-bearer-token-67890 +``` + +### 3. Basic 认证 + +```bash +# 测试 Basic 认证(admin:secret123 的 base64 编码) +curl -X GET "http://localhost:8080/http-server/auth/basic-static" \ + -H "Authorization: Basic YWRtaW46c2VjcmV0MTIz" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Basic Static Auth: Basic YWRtaW46c2VjcmV0MTIz +``` + +### 4. API Key Header 静态认证 + +```bash +# 测试 Header 中的 API Key +curl -X GET "http://localhost:8080/http-server/auth/apikey-header-static" \ + -H "X-API-Key: static-api-key-67890" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:API Key Header Static: static-api-key-67890, Service Key: service-default-key +``` + +### 5. API Key Query 静态认证 + +```bash +# 测试 Query 参数中的 API Key +curl -X GET "http://localhost:8080/http-server/auth/apikey-query-static?api_key=query-api-key-111" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:API Key Query Static: query-api-key-111, Service Key: service-default-key +``` + +### 6. API Key 动态认证 + +```bash +# 测试动态 API Key +curl -X GET "http://localhost:8080/http-server/auth/apikey-dynamic" \ + -H "X-Dynamic-Key: dynamic-api-key-999" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:API Key Dynamic: dynamic-api-key-999, Service Key: service-default-key +``` + +### 7. 动态 Provider 认证 + +```bash +# 测试动态 Token Provider +curl -X GET "http://localhost:8080/http-server/auth/dynamic-provider" \ + -H "Authorization: Bearer provider-generated-token-123" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Dynamic Provider Auth: Bearer provider-generated-token-123, Service Key: service-default-key +``` + +### 8. 自定义 Provider 认证 + +```bash +# 测试自定义签名 Provider +curl -X GET "http://localhost:8080/http-server/auth/custom-provider" \ + -H "X-Timestamp: 1640995200000" \ + -H "X-Signature: custom-signature-abc123" \ + -H "X-App-Id: test-app-001" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Custom Provider Auth - Timestamp: 1640995200000, Signature: custom-signature-abc123, AppId: test-app-001, Service Key: service-default-key +``` + +### 9. 方法级别覆盖 + +```bash +# 测试方法级别的认证覆盖 +curl -X GET "http://localhost:8080/http-server/auth/method-override" \ + -H "X-API-Key: method-override-key-456" + +# 期望响应:Method Override Auth: method-override-key-456 +``` + +### 10. 组合认证 + +```bash +# 测试组合认证(多种认证方式同时使用) +curl -X GET "http://localhost:8080/http-server/auth/combined-auth" \ + -H "Authorization: Bearer combined-auth-token-789" \ + -H "X-User-Context: user-context-key-abc" \ + -H "X-Service-Key: service-default-key" + +# 期望响应:Combined Auth - Authorization: Bearer combined-auth-token-789, UserContext: user-context-key-abc, Service Key: service-default-key +``` + +## 错误场景测试 + +### 1. 缺少必需的认证头 + +```bash +# 测试缺少 Authorization 头 +curl -X GET "http://localhost:8080/http-server/auth/bearer-static" + +# 期望:400 Bad Request 或相应的错误响应 +``` + +### 2. 错误的认证格式 + +```bash +# 测试错误的 Bearer Token 格式 +curl -X GET "http://localhost:8080/http-server/auth/bearer-static" \ + -H "Authorization: InvalidFormat token-123" + +# 期望:401 Unauthorized 或相应的错误响应 +``` + +### 3. 缺少 API Key + +```bash +# 测试缺少 API Key +curl -X GET "http://localhost:8080/http-server/auth/apikey-header-static" \ + -H "X-Service-Key: service-default-key" + +# 期望:400 Bad Request 或相应的错误响应 +``` + +## 批量测试 + +使用提供的脚本进行批量测试: + +```bash +# 确保脚本有执行权限 +chmod +x ./run_tests.sh + +# 运行所有测试用例 +./run_tests.sh + +# 运行特定类型的测试 +./run_tests.sh bearer # Bearer Token 相关测试 +./run_tests.sh apikey # API Key 相关测试 +./run_tests.sh basic # Basic 认证测试 +./run_tests.sh provider # Provider 模式测试 +./run_tests.sh error # 错误场景测试 + +# 详细模式运行 +./run_tests.sh -v bearer + +# 自定义超时时间 +./run_tests.sh -t 30 all +``` + +## 验证清单 + +- [ ] 所有静态认证配置正常工作 +- [ ] 动态认证(参数驱动)正常工作 +- [ ] Provider 模式正常工作 +- [ ] 认证优先级正确(参数 > 方法 > 接口) +- [ ] 组合认证场景正常工作 +- [ ] 错误场景返回正确的状态码 +- [ ] 服务级别的默认认证始终包含在请求中 + +## 注意事项 + +1. **Base64 编码**: Basic 认证需要对 `username:password` 进行 base64 编码 +2. **Headers 大小写**: HTTP 头的大小写在某些服务器上可能敏感 +3. **Query 参数编码**: 确保特殊字符正确进行 URL 编码 +4. **Provider 依赖**: 使用 Provider 的测试需要确保相应的 Bean 已注册 + +## 故障排除 + +如果测试失败,请检查: + +1. 服务器是否正常启动(查看启动日志中是否有 "FIT application started" 信息) +2. 端口 8080 是否被占用 +3. 认证头的格式是否正确 +4. Provider Bean 是否正确注册(使用 `@Component` 注解) +5. 项目是否已正确编译(`mvn clean install`) +6. 查看服务器日志获取详细错误信息 \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/ApiKeyProvider.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/ApiKeyProvider.java new file mode 100644 index 00000000..84746aba --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/ApiKeyProvider.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.auth; + +import modelengine.fit.http.client.proxy.Authorization; +import modelengine.fit.http.client.proxy.auth.AuthProvider; +import modelengine.fit.http.server.handler.Source; +import modelengine.fitframework.annotation.Component; + +/** + * API Key 提供器示例。 + *
提供动态的 API Key 鉴权。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +@Component +public class ApiKeyProvider implements AuthProvider { + @Override + public Authorization provide() { + // 模拟从配置或环境变量获取 API Key + String apiKey = "api-key-" + System.currentTimeMillis(); + return Authorization.createApiKey("X-API-Key", apiKey, Source.HEADER); + } +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/CustomSignatureProvider.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/CustomSignatureProvider.java new file mode 100644 index 00000000..1d107633 --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/CustomSignatureProvider.java @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.auth; + +import modelengine.fit.http.client.proxy.Authorization; +import modelengine.fit.http.client.proxy.RequestBuilder; +import modelengine.fit.http.client.proxy.auth.AuthProvider; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 自定义签名鉴权提供器示例。 + *
演示如何实现复杂的自定义鉴权逻辑,如签名算法。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +@Component +public class CustomSignatureProvider implements AuthProvider { + @Override + public Authorization provide() { + return new CustomSignatureAuthorization(); + } + + /** + * 自定义签名鉴权实现。 + * 在每次请求时生成时间戳和签名。 + */ + private static class CustomSignatureAuthorization implements Authorization { + private static final String SECRET_KEY = "my-secret-key"; + + @Override + public void set(String key, Object value) { + // 自定义鉴权不需要外部设置参数 + } + + @Override + public void assemble(RequestBuilder builder) { + String timestamp = String.valueOf(System.currentTimeMillis()); + String signature = generateSignature(timestamp); + + builder.header("X-Timestamp", timestamp); + builder.header("X-Signature", signature); + builder.header("X-App-Id", "fit-example-app"); + } + + private String generateSignature(String timestamp) { + try { + String data = timestamp + SECRET_KEY; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8)); + + // 简单的十六进制转换 + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to generate signature", e); + } + } + } +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/DynamicTokenProvider.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/DynamicTokenProvider.java new file mode 100644 index 00000000..081e8587 --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/auth/DynamicTokenProvider.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.auth; + +import modelengine.fit.http.client.proxy.Authorization; +import modelengine.fit.http.client.proxy.auth.AuthProvider; +import modelengine.fitframework.annotation.Component; + +/** + * 动态 Token 提供器示例。 + *
模拟从某个 Token 管理器获取动态 Token 的场景。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +@Component +public class DynamicTokenProvider implements AuthProvider { + @Override + public Authorization provide() { + // 模拟动态获取 token + String dynamicToken = "dynamic-token-" + System.currentTimeMillis(); + return Authorization.createBearer(dynamicToken); + } +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthClient.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthClient.java new file mode 100644 index 00000000..61df588a --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthClient.java @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.client; + +import modelengine.fit.example.auth.ApiKeyProvider; +import modelengine.fit.example.auth.CustomSignatureProvider; +import modelengine.fit.example.auth.DynamicTokenProvider; +import modelengine.fit.http.annotation.GetMapping; +import modelengine.fit.http.annotation.HttpProxy; +import modelengine.fit.http.annotation.RequestAddress; +import modelengine.fit.http.annotation.RequestAuth; +import modelengine.fit.http.annotation.RequestMapping; +import modelengine.fit.http.client.proxy.auth.AuthType; +import modelengine.fit.http.server.handler.Source; + +/** + * 鉴权测试客户端实现。 + *
演示各种 @RequestAuth 注解的使用方式。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +@HttpProxy +@RequestAddress(protocol = "http", host = "localhost", port = "8080") +@RequestMapping(path = "/http-server/auth") +/** + * 接口级别的默认鉴权:API Key + */ +@RequestAuth(type = AuthType.API_KEY, name = "X-Service-Key", value = "service-default-key") +public interface TestAuthClient extends TestAuthInterface { + @Override + @GetMapping(path = "/bearer-static") + /** + * 方法级别覆盖:使用 Bearer Token + */ + @RequestAuth(type = AuthType.BEARER, value = "static-bearer-token-12345") + String testBearerStatic(); + + @Override + @GetMapping(path = "/bearer-dynamic") + /** + * 方法级别覆盖:使用参数驱动的 Bearer Token + */ + String testBearerDynamic(@RequestAuth(type = AuthType.BEARER) String token); + + @Override + @GetMapping(path = "/basic-static") + /** + * 方法级别覆盖:使用 Basic Auth + */ + @RequestAuth(type = AuthType.BASIC, username = "admin", password = "secret123") + String testBasicStatic(); + + @Override + @GetMapping(path = "/apikey-header-static") + /** + * 方法级别覆盖:API Key 在 Header 中 + */ + @RequestAuth(type = AuthType.API_KEY, name = "X-API-Key", value = "static-api-key-67890") + String testApiKeyHeaderStatic(); + + @Override + @GetMapping(path = "/apikey-query-static") + /** + * 方法级别覆盖:API Key 在 Query 参数中 + */ + @RequestAuth(type = AuthType.API_KEY, name = "api_key", value = "query-api-key-111", location = Source.QUERY) + String testApiKeyQueryStatic(); + + @Override + @GetMapping(path = "/apikey-dynamic") + /** + * 参数驱动的 API Key + */ + String testApiKeyDynamic(@RequestAuth(type = AuthType.API_KEY, name = "X-Dynamic-Key") String apiKey); + + @Override + @GetMapping(path = "/dynamic-provider") + /** + * 方法级别覆盖:使用动态 Token Provider + */ + @RequestAuth(type = AuthType.BEARER, provider = DynamicTokenProvider.class) + String testDynamicProvider(); + + @Override + @GetMapping(path = "/custom-provider") + /** + * 方法级别覆盖:使用自定义签名 Provider + */ + @RequestAuth(type = AuthType.CUSTOM, provider = CustomSignatureProvider.class) + String testCustomProvider(); + + @Override + @GetMapping(path = "/method-override") + /** + * 方法级别覆盖:使用 API Key Provider + */ + @RequestAuth(type = AuthType.API_KEY, provider = ApiKeyProvider.class) + String testMethodOverride(); + + @Override + @GetMapping(path = "/combined-auth") + /** + * 组合鉴权:服务级 API Key + 用户 Token + */ + @RequestAuth(type = AuthType.BEARER, provider = DynamicTokenProvider.class) + String testCombinedAuth(@RequestAuth(type = AuthType.API_KEY, name = "X-User-Context") String userToken); +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthInterface.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthInterface.java new file mode 100644 index 00000000..acecf93b --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/client/TestAuthInterface.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.client; + +/** + * 鉴权测试接口定义。 + * 包含各种鉴权场景的测试方法。 + * + * @author 季聿阶 + * @since 2025-01-01 + */ +public interface TestAuthInterface { + /** + * 测试Bearer Token静态鉴权。 + * + * @return 鉴权测试结果 + */ + String testBearerStatic(); + + /** + * 测试Bearer Token参数驱动鉴权。 + * + * @param token Bearer Token + * @return 鉴权测试结果 + */ + String testBearerDynamic(String token); + + /** + * 测试Basic Auth静态鉴权。 + * + * @return 鉴权测试结果 + */ + String testBasicStatic(); + + /** + * 测试API Key Header静态鉴权。 + * + * @return 鉴权测试结果 + */ + String testApiKeyHeaderStatic(); + + /** + * 测试API Key Query参数静态鉴权。 + * + * @return 鉴权测试结果 + */ + String testApiKeyQueryStatic(); + + /** + * 测试API Key参数驱动鉴权。 + * + * @param apiKey API Key值 + * @return 鉴权测试结果 + */ + String testApiKeyDynamic(String apiKey); + + /** + * 测试动态Provider鉴权。 + * + * @return 鉴权测试结果 + */ + String testDynamicProvider(); + + /** + * 测试自定义签名Provider鉴权。 + * + * @return 鉴权测试结果 + */ + String testCustomProvider(); + + /** + * 测试方法级别覆盖接口级别鉴权。 + * + * @return 鉴权测试结果 + */ + String testMethodOverride(); + + /** + * 测试多种鉴权组合(虽然我们说不支持多重鉴权,但可能有组合场景)。 + * + * @param userToken 用户Token + * @return 鉴权测试结果 + */ + String testCombinedAuth(String userToken); +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/controller/TestClientController.java b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/controller/TestClientController.java index 11f4ea59..f5cf101e 100644 --- a/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/controller/TestClientController.java +++ b/examples/fit-example/07-http-client-proxy/plugin-http-client/src/main/java/modelengine/fit/example/controller/TestClientController.java @@ -6,6 +6,7 @@ package modelengine.fit.example.controller; +import modelengine.fit.example.client.TestAuthClient; import modelengine.fit.example.client.TestInterface; import modelengine.fit.example.client.TestRequestAddress; import modelengine.fit.example.client.TestRequestAddressClass; @@ -33,6 +34,7 @@ public class TestClientController { private final TestRequestAddressClass t2; private final TestRequestAddressInClassMapping t3; private final TestRequestAddressInMethodMapping t4; + private final TestAuthClient authClient; /** * Constructs a TestClientController with the specified test interfaces. @@ -41,13 +43,15 @@ public class TestClientController { * @param t2 The TestRequestAddressClass interface. * @param t3 The TestRequestAddressInClassMapping interface. * @param t4 The TestRequestAddressInMethodMapping interface. + * @param authClient The TestAuthClient interface for auth testing. */ public TestClientController(TestRequestAddress t1, TestRequestAddressClass t2, TestRequestAddressInClassMapping t3, - TestRequestAddressInMethodMapping t4) { + TestRequestAddressInMethodMapping t4, TestAuthClient authClient) { this.t1 = t1; this.t2 = t2; this.t3 = t3; this.t4 = t4; + this.authClient = authClient; } /** @@ -89,4 +93,41 @@ public Object test(@RequestQuery("type") String type, @RequestQuery("method") St throw new IllegalArgumentException("Invalid method: " + method); } } + + /** + * Endpoint for running HTTP client auth tests. + * This method allows testing different authentication scenarios. + * + * @param method The auth test method to invoke. + * @param token Optional token parameter for dynamic auth tests. + * @return The result of the invoked auth method. + */ + @GetMapping(path = "/auth-test") + public Object authTest(@RequestQuery("method") String method, + @RequestQuery(value = "token", required = false) String token) { + switch (method) { + case "bearerStatic": + return authClient.testBearerStatic(); + case "bearerDynamic": + return authClient.testBearerDynamic(token != null ? token : "dynamic-test-token"); + case "basicStatic": + return authClient.testBasicStatic(); + case "apiKeyHeaderStatic": + return authClient.testApiKeyHeaderStatic(); + case "apiKeyQueryStatic": + return authClient.testApiKeyQueryStatic(); + case "apiKeyDynamic": + return authClient.testApiKeyDynamic(token != null ? token : "dynamic-api-key"); + case "dynamicProvider": + return authClient.testDynamicProvider(); + case "customProvider": + return authClient.testCustomProvider(); + case "methodOverride": + return authClient.testMethodOverride(); + case "combinedAuth": + return authClient.testCombinedAuth(token != null ? token : "user-context-token"); + default: + throw new IllegalArgumentException("Invalid auth method: " + method); + } + } } \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/plugin-http-server/src/main/java/modelengine/fit/example/controller/TestAuthServerController.java b/examples/fit-example/07-http-client-proxy/plugin-http-server/src/main/java/modelengine/fit/example/controller/TestAuthServerController.java new file mode 100644 index 00000000..55f4bda4 --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/plugin-http-server/src/main/java/modelengine/fit/example/controller/TestAuthServerController.java @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.example.controller; + +import modelengine.fit.http.annotation.GetMapping; +import modelengine.fit.http.annotation.RequestHeader; +import modelengine.fit.http.annotation.RequestMapping; +import modelengine.fit.http.annotation.RequestQuery; +import modelengine.fitframework.annotation.Component; + +/** + * 鉴权测试服务端控制器。 + *
用于验证各种鉴权场景的 HTTP 请求。
+ * + * @author 季聿阶 + * @since 2025-09-30 + */ +@Component +@RequestMapping(path = "/http-server/auth") +public class TestAuthServerController { + @GetMapping(path = "/bearer-static") + public String testBearerStatic(@RequestHeader(name = "Authorization") String authorization) { + return "Bearer Static Auth: " + authorization; + } + + @GetMapping(path = "/bearer-dynamic") + public String testBearerDynamic(@RequestHeader(name = "Authorization") String authorization) { + return "Bearer Dynamic Auth: " + authorization; + } + + @GetMapping(path = "/basic-static") + public String testBasicStatic(@RequestHeader(name = "Authorization") String authorization) { + return "Basic Static Auth: " + authorization; + } + + @GetMapping(path = "/apikey-header-static") + public String testApiKeyHeaderStatic(@RequestHeader(name = "X-API-Key") String apiKey, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = "API Key Header Static: " + apiKey; + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } + + @GetMapping(path = "/apikey-query-static") + public String testApiKeyQueryStatic(@RequestQuery(name = "api_key") String apiKey, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = "API Key Query Static: " + apiKey; + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } + + @GetMapping(path = "/apikey-dynamic") + public String testApiKeyDynamic(@RequestHeader(name = "X-Dynamic-Key") String apiKey, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = "API Key Dynamic: " + apiKey; + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } + + @GetMapping(path = "/dynamic-provider") + public String testDynamicProvider(@RequestHeader(name = "Authorization") String authorization, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = "Dynamic Provider Auth: " + authorization; + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } + + @GetMapping(path = "/custom-provider") + public String testCustomProvider(@RequestHeader(name = "X-Timestamp") String timestamp, + @RequestHeader(name = "X-Signature") String signature, + @RequestHeader(name = "X-App-Id") String appId, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = String.format("Custom Provider Auth - Timestamp: %s, Signature: %s, AppId: %s", + timestamp, signature, appId); + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } + + @GetMapping(path = "/method-override") + public String testMethodOverride(@RequestHeader(name = "X-API-Key") String apiKey) { + return "Method Override Auth: " + apiKey; + } + + @GetMapping(path = "/combined-auth") + public String testCombinedAuth(@RequestHeader(name = "Authorization") String authorization, + @RequestHeader(name = "X-User-Context") String userContext, + @RequestHeader(name = "X-Service-Key", required = false) String serviceKey) { + String result = String.format("Combined Auth - Authorization: %s, UserContext: %s", + authorization, userContext); + if (serviceKey != null) { + result += ", Service Key: " + serviceKey; + } + return result; + } +} \ No newline at end of file diff --git a/examples/fit-example/07-http-client-proxy/run_tests.sh b/examples/fit-example/07-http-client-proxy/run_tests.sh new file mode 100755 index 00000000..3c897eda --- /dev/null +++ b/examples/fit-example/07-http-client-proxy/run_tests.sh @@ -0,0 +1,286 @@ +#!/bin/bash + +# HTTP Client Authentication Test Script +# 用于批量执行 HTTP 客户端认证功能的测试用例 + +set -e # 遇到错误时退出 + +# 配置 +BASE_URL="http://localhost:8080/http-server/auth" +TIMEOUT=10 +VERBOSE=false +TEST_TYPE="" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 帮助信息 +show_help() { + echo "HTTP Client Authentication Test Script" + echo "" + echo "Usage: $0 [OPTIONS] [TEST_TYPE]" + echo "" + echo "Options:" + echo " -h, --help 显示此帮助信息" + echo " -v, --verbose 详细输出模式" + echo " -t, --timeout 请求超时时间(秒),默认 10" + echo " -u, --url 服务器基础 URL,默认 http://localhost:8080/http-server/auth" + echo "" + echo "Test Types:" + echo " all 运行所有测试(默认)" + echo " bearer 只运行 Bearer Token 相关测试" + echo " basic 只运行 Basic 认证测试" + echo " apikey 只运行 API Key 相关测试" + echo " provider 只运行 Provider 相关测试" + echo " error 只运行错误场景测试" + echo "" + echo "Examples:" + echo " $0 # 运行所有测试" + echo " $0 bearer # 只运行 Bearer Token 测试" + echo " $0 -v apikey # 详细模式运行 API Key 测试" + echo " $0 -t 30 provider # 30秒超时运行 Provider 测试" +} + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -t|--timeout) + TIMEOUT="$2" + shift 2 + ;; + -u|--url) + BASE_URL="$2" + shift 2 + ;; + all|bearer|basic|apikey|provider|error) + TEST_TYPE="$1" + shift + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +# 默认运行所有测试 +if [ -z "$TEST_TYPE" ]; then + TEST_TYPE="all" +fi + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" +} + +log_error() { + echo -e "${RED}[FAIL]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +# 显示服务器信息(不进行连接检查) +show_server_info() { + log_info "目标服务器: $BASE_URL" + log_info "如果测试失败,请确保服务器已启动:mvn spring-boot:run -pl plugin-http-server" +} + +# 执行单个测试 +run_test() { + local test_name="$1" + local curl_cmd="$2" + local expected_pattern="$3" + + if [ "$VERBOSE" = true ]; then + log_info "执行测试: $test_name" + log_info "命令: $curl_cmd" + else + printf "%-40s" "$test_name" + fi + + # 执行 curl 命令 + local response + local exit_code + response=$(eval "$curl_cmd" 2>&1) + exit_code=$? + + if [ $exit_code -ne 0 ]; then + if [ "$VERBOSE" = true ]; then + log_error "请求失败: $response" + else + echo -e "${RED}FAIL${NC}" + fi + return 1 + fi + + # 检查响应 + if [[ "$response" == *"$expected_pattern"* ]]; then + if [ "$VERBOSE" = true ]; then + log_success "测试通过" + log_info "响应: $response" + else + echo -e "${GREEN}PASS${NC}" + fi + return 0 + else + if [ "$VERBOSE" = true ]; then + log_error "响应不匹配期望模式" + log_info "期望包含: $expected_pattern" + log_info "实际响应: $response" + else + echo -e "${RED}FAIL${NC}" + fi + return 1 + fi +} + +# Bearer Token 测试 +run_bearer_tests() { + log_info "运行 Bearer Token 测试..." + + run_test "Bearer Static Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/bearer-static\" -H \"Authorization: Bearer static-bearer-token-12345\" -H \"X-Service-Key: service-default-key\"" \ + "Bearer Static Auth: Bearer static-bearer-token-12345" + + run_test "Bearer Dynamic Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/bearer-dynamic\" -H \"Authorization: Bearer dynamic-bearer-token-67890\"" \ + "Bearer Dynamic Auth: Bearer dynamic-bearer-token-67890" +} + +# Basic 认证测试 +run_basic_tests() { + log_info "运行 Basic 认证测试..." + + # admin:secret123 的 base64 编码 + run_test "Basic Static Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/basic-static\" -H \"Authorization: Basic YWRtaW46c2VjcmV0MTIz\" -H \"X-Service-Key: service-default-key\"" \ + "Basic Static Auth: Basic YWRtaW46c2VjcmV0MTIz" +} + +# API Key 测试 +run_apikey_tests() { + log_info "运行 API Key 测试..." + + run_test "API Key Header Static" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/apikey-header-static\" -H \"X-API-Key: static-api-key-67890\" -H \"X-Service-Key: service-default-key\"" \ + "API Key Header Static: static-api-key-67890" + + run_test "API Key Query Static" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/apikey-query-static?api_key=query-api-key-111\" -H \"X-Service-Key: service-default-key\"" \ + "API Key Query Static: query-api-key-111" + + run_test "API Key Dynamic" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/apikey-dynamic\" -H \"X-Dynamic-Key: dynamic-api-key-999\" -H \"X-Service-Key: service-default-key\"" \ + "API Key Dynamic: dynamic-api-key-999" +} + +# Provider 测试 +run_provider_tests() { + log_info "运行 Provider 测试..." + + run_test "Dynamic Provider Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/dynamic-provider\" -H \"Authorization: Bearer provider-generated-token-123\" -H \"X-Service-Key: service-default-key\"" \ + "Dynamic Provider Auth: Bearer provider-generated-token-123" + + run_test "Custom Provider Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/custom-provider\" -H \"X-Timestamp: 1640995200000\" -H \"X-Signature: custom-signature-abc123\" -H \"X-App-Id: test-app-001\" -H \"X-Service-Key: service-default-key\"" \ + "Custom Provider Auth" + + run_test "Method Override Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/method-override\" -H \"X-API-Key: method-override-key-456\"" \ + "Method Override Auth: method-override-key-456" + + run_test "Combined Auth" \ + "curl -s --max-time $TIMEOUT -X GET \"$BASE_URL/combined-auth\" -H \"Authorization: Bearer combined-auth-token-789\" -H \"X-User-Context: user-context-key-abc\" -H \"X-Service-Key: service-default-key\"" \ + "Combined Auth" +} + +# 错误场景测试 +run_error_tests() { + log_info "运行错误场景测试..." + + # 这些测试期望返回错误状态码 + log_warning "注意:错误场景测试可能会显示预期的失败结果" + + run_test "Missing Authorization Header" \ + "curl -s --max-time $TIMEOUT -w '%{http_code}' -X GET \"$BASE_URL/bearer-static\"" \ + "400\\|401\\|403" + + run_test "Missing API Key Header" \ + "curl -s --max-time $TIMEOUT -w '%{http_code}' -X GET \"$BASE_URL/apikey-header-static\" -H \"X-Service-Key: service-default-key\"" \ + "400\\|401\\|403" +} + +# 主执行函数 +main() { + echo "==========================================" + echo "HTTP Client Authentication Test Suite" + echo "==========================================" + echo "服务器: $BASE_URL" + echo "超时时间: ${TIMEOUT}s" + echo "测试类型: $TEST_TYPE" + echo "详细模式: $VERBOSE" + echo "==========================================" + + # 显示服务器信息 + show_server_info + + # 统计变量 + local total_tests=0 + local passed_tests=0 + + # 根据测试类型运行测试 + case $TEST_TYPE in + "all") + run_bearer_tests + run_basic_tests + run_apikey_tests + run_provider_tests + ;; + "bearer") + run_bearer_tests + ;; + "basic") + run_basic_tests + ;; + "apikey") + run_apikey_tests + ;; + "provider") + run_provider_tests + ;; + "error") + run_error_tests + ;; + esac + + echo "==========================================" + echo "测试完成!" + echo "==========================================" + + if [ "$TEST_TYPE" = "error" ]; then + log_warning "错误场景测试完成,某些失败是预期的" + fi +} + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuth.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuth.java new file mode 100644 index 00000000..b25489f1 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuth.java @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.annotation; + +import modelengine.fit.http.client.proxy.auth.AuthProvider; +import modelengine.fit.http.client.proxy.auth.AuthType; +import modelengine.fit.http.server.handler.Source; +import modelengine.fitframework.util.StringUtils; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 表示 HTTP 请求的鉴权配置注解。 + *支持 Bearer Token、Basic Auth、API Key 等多种鉴权方式。 + * 可以应用于接口、方法或参数级别,支持静态配置和动态 Provider。
+ * + *使用示例: + *
+ * // 静态 Bearer Token
+ * {@code @RequestAuth(type = AuthType.BEARER, value = "token_value")}
+ *
+ * // API Key in Header
+ * {@code @RequestAuth(type = AuthType.API_KEY, name = "X-API-Key", value = "key_value")}
+ *
+ * // API Key in Query
+ * {@code @RequestAuth(type = AuthType.API_KEY, name = "api_key", value = "key_value", location = Source.QUERY)}
+ *
+ * // Basic Auth
+ * {@code @RequestAuth(type = AuthType.BASIC, username = "user", password = "pass")}
+ *
+ * // Dynamic Provider
+ * {@code @RequestAuth(type = AuthType.BEARER, provider = TokenProvider.class)}
+ *
+ * // Parameter-driven
+ * public User getUser({@code @RequestAuth(type = AuthType.BEARER)} String token);
+ *
+ *
+ * @author 季聿阶
+ * @since 2025-09-30
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Repeatable(RequestAuths.class)
+public @interface RequestAuth {
+ /**
+ * 鉴权类型。
+ *
+ * @return 表示鉴权类型的 {@link AuthType}。
+ */
+ AuthType type();
+
+ /**
+ * 鉴权值,用于静态配置。
+ * 仅对 API Key 有效,可以是 HEADER、QUERY 或 COOKIE。 + * 默认为 HEADER。
+ * + * @return 表示鉴权参数位置的 {@link Source}。 + */ + Source location() default Source.HEADER; + + /** + * Basic Auth 的用户名,仅当 type=BASIC 时有效。 + * + * @return 表示用户名的 {@link String}。 + */ + String username() default StringUtils.EMPTY; + + /** + * Basic Auth 的密码,仅当 type=BASIC 时有效。 + * + * @return 表示密码的 {@link String}。 + */ + String password() default StringUtils.EMPTY; + + /** + * 动态鉴权提供器类。 + *当指定时,将忽略静态配置(value、username、password 等), + * 通过 Provider 动态获取鉴权信息。
+ * + * @return 表示鉴权提供器类的 {@link Class}。 + */ + Class extends AuthProvider> provider() default AuthProvider.class; +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuths.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuths.java new file mode 100644 index 00000000..dfe833fb --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/annotation/RequestAuths.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 容器注解,用于支持多个 {@link RequestAuth} 注解的组合使用。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +public @interface RequestAuths { + /** + * 多个鉴权配置。 + * + * @return 表示多个鉴权配置的 {@link RequestAuth} 数组。 + */ + RequestAuth[] value(); +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthProvider.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthProvider.java new file mode 100644 index 00000000..cb063099 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthProvider.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.client.proxy.auth; + +import modelengine.fit.http.client.proxy.Authorization; + +/** + * 鉴权提供器接口。 + *用于动态提供鉴权信息,支持复杂的鉴权逻辑和动态 token 获取。
+ * + *实现类通常需要标记为 {@code @Component} 以便被框架自动发现和注入。
+ * + *使用示例: + *
+ * {@code @Component}
+ * public class TokenProvider implements AuthProvider {
+ * {@code @Override}
+ * public Authorization provide() {
+ * String token = TokenManager.getCurrentToken();
+ * return Authorization.createBearer(token);
+ * }
+ * }
+ *
+ *
+ * @author 季聿阶
+ * @since 2025-09-30
+ */
+public interface AuthProvider {
+ /**
+ * 提供鉴权信息。
+ * 此方法会在每次 HTTP 请求时被调用,用于获取最新的鉴权信息。
+ * + * @return 表示鉴权信息的 {@link Authorization} 对象。 + */ + Authorization provide(); +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthType.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthType.java new file mode 100644 index 00000000..948231e9 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/auth/AuthType.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.client.proxy.auth; + +/** + * 表示 HTTP 请求的鉴权类型枚举。 + *定义了框架支持的各种鉴权方式。
+ * + * @author 季聿阶 + * @since 2025-09-30 + */ +public enum AuthType { + /** + * Bearer Token 鉴权。 + *通常用于 JWT Token 等场景,会在 Authorization 头中添加 "Bearer {token}"。
+ */ + BEARER, + + /** + * Basic 鉴权。 + *使用用户名和密码进行基础认证,会在 Authorization 头中添加 "Basic {base64(username:password)}"。
+ */ + BASIC, + + /** + * API Key 鉴权。 + *使用 API 密钥进行认证,可以放在 Header、Query 参数或 Cookie 中。
+ */ + API_KEY, + + /** + * 自定义鉴权。 + *通过 AuthProvider 提供自定义的鉴权逻辑,支持复杂的鉴权场景。
+ */ + CUSTOM +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/scanner/AnnotationParser.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/scanner/AnnotationParser.java index 8704672e..c7f438ce 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/scanner/AnnotationParser.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/scanner/AnnotationParser.java @@ -22,6 +22,7 @@ import modelengine.fit.http.annotation.RequestHeader; import modelengine.fit.http.annotation.RequestMapping; import modelengine.fit.http.annotation.RequestQuery; +import modelengine.fit.http.annotation.RequestAuth; import modelengine.fit.http.client.proxy.PropertyValueApplier; import modelengine.fit.http.client.proxy.scanner.entity.Address; import modelengine.fit.http.client.proxy.scanner.entity.HttpInfo; @@ -31,7 +32,9 @@ import modelengine.fit.http.client.proxy.scanner.resolver.RequestFormResolver; import modelengine.fit.http.client.proxy.scanner.resolver.RequestHeaderResolver; import modelengine.fit.http.client.proxy.scanner.resolver.RequestQueryResolver; +import modelengine.fit.http.client.proxy.scanner.resolver.RequestAuthResolver; import modelengine.fit.http.client.proxy.support.applier.MultiDestinationsPropertyValueApplier; +import modelengine.fit.http.client.proxy.support.applier.StaticAuthApplier; import modelengine.fit.http.client.proxy.support.setter.DestinationSetterInfo; import modelengine.fitframework.util.ArrayUtils; import modelengine.fitframework.util.ReflectionUtils; @@ -74,6 +77,7 @@ public class AnnotationParser { annotationParsers.put(RequestBody.class, new RequestBodyResolver()); annotationParsers.put(RequestForm.class, new RequestFormResolver()); annotationParsers.put(PathVariable.class, new PathVariableResolver()); + annotationParsers.put(RequestAuth.class, new RequestAuthResolver()); } private final ValueFetcher valueFetcher; @@ -98,9 +102,14 @@ public Map负责将 {@link RequestAuth} 注解转换为可用于设置 HTTP 请求鉴权信息的 {@link DestinationSetterInfo} 对象。
+ * + * @author 季聿阶 + * @since 2025-09-30 + */ +public class RequestAuthResolver implements ParamResolver用于处理类级别和方法级别的 @RequestAuth 注解,将静态鉴权信息应用到 HTTP 请求中。
+ * + * @author 季聿阶 + * @since 2025-09-30 + */ +public class StaticAuthApplier implements PropertyValueApplier { + private final AuthDestinationSetter authSetter; + + /** + * 使用指定的鉴权注解初始化 {@link StaticAuthApplier} 的新实例。 + * + * @param authAnnotation 表示鉴权注解的 {@link RequestAuth}。 + */ + public StaticAuthApplier(RequestAuth authAnnotation) { + this.authSetter = new AuthDestinationSetter(authAnnotation); + } + + @Override + public void apply(RequestBuilder requestBuilder, Object value) { + // 静态鉴权不需要参数值,传入 null 即可 + authSetter.set(requestBuilder, null); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetter.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetter.java new file mode 100644 index 00000000..fc6e4eaf --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetter.java @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.client.proxy.support.setter; + +import modelengine.fit.http.annotation.RequestAuth; +import modelengine.fit.http.client.proxy.Authorization; +import modelengine.fit.http.client.proxy.DestinationSetter; +import modelengine.fit.http.client.proxy.RequestBuilder; +import modelengine.fit.http.client.proxy.auth.AuthProvider; +import modelengine.fit.http.client.proxy.auth.AuthType; +import modelengine.fit.http.server.handler.Source; +import modelengine.fitframework.ioc.BeanContainer; +import modelengine.fitframework.util.StringUtils; + +/** + * 表示向 HTTP 请求设置鉴权信息的 {@link DestinationSetter}。 + *支持多种鉴权类型和动态 Provider。
+ * + * @author 季聿阶 + * @since 2025-09-30 + */ +public class AuthDestinationSetter implements DestinationSetter { + private final RequestAuth authAnnotation; + private final BeanContainer beanContainer; + + /** + * 使用指定的鉴权注解初始化 {@link AuthDestinationSetter} 的新实例。 + * + * @param authAnnotation 表示鉴权注解的 {@link RequestAuth}。 + */ + public AuthDestinationSetter(RequestAuth authAnnotation) { + this(authAnnotation, null); + } + + /** + * 使用指定的鉴权注解和 Bean 容器初始化 {@link AuthDestinationSetter} 的新实例。 + * + * @param authAnnotation 表示鉴权注解的 {@link RequestAuth}。 + * @param beanContainer 表示 Bean 容器的 {@link BeanContainer}。 + */ + public AuthDestinationSetter(RequestAuth authAnnotation, BeanContainer beanContainer) { + this.authAnnotation = authAnnotation; + this.beanContainer = beanContainer; + } + + @Override + public void set(RequestBuilder requestBuilder, Object value) { + Authorization authorization = createAuthorization(value); + if (authorization != null) { + authorization.assemble(requestBuilder); + } + } + + private Authorization createAuthorization(Object value) { + // 如果指定了 Provider,优先使用 Provider + if (authAnnotation.provider() != AuthProvider.class) { + if (beanContainer != null) { + AuthProvider provider = beanContainer.beans().get(authAnnotation.provider()); + if (provider != null) { + return provider.provide(); + } else { + throw new IllegalStateException("AuthProvider " + authAnnotation.provider().getName() + " not found in container"); + } + } else { + // TODO: MVP 版本暂时不支持 Provider,后续版本再实现 + throw new UnsupportedOperationException("AuthProvider support is not implemented in this version"); + } + } + + // 基于注解类型创建 Authorization + AuthType type = authAnnotation.type(); + switch (type) { + case BEARER: + String token = getBearerToken(value); + if (StringUtils.isNotEmpty(token)) { + return Authorization.createBearer(token); + } + break; + case BASIC: + String username = getBasicUsername(); + String password = getBasicPassword(); + if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) { + return Authorization.createBasic(username, password); + } + break; + case API_KEY: + String keyName = getApiKeyName(); + String keyValue = getApiKeyValue(value); + Source location = authAnnotation.location(); + if (StringUtils.isNotEmpty(keyName) && StringUtils.isNotEmpty(keyValue)) { + return Authorization.createApiKey(keyName, keyValue, location); + } + break; + case CUSTOM: + // CUSTOM 类型必须使用 Provider + throw new IllegalArgumentException("CUSTOM auth type requires a provider"); + } + + return null; + } + + private String getBearerToken(Object value) { + // 如果是参数驱动,使用参数值 + if (value instanceof String) { + return (String) value; + } + // 否则使用注解中的静态值 + return StringUtils.isNotEmpty(authAnnotation.value()) ? authAnnotation.value() : null; + } + + private String getBasicUsername() { + return StringUtils.isNotEmpty(authAnnotation.username()) ? authAnnotation.username() : null; + } + + private String getBasicPassword() { + return StringUtils.isNotEmpty(authAnnotation.password()) ? authAnnotation.password() : null; + } + + private String getApiKeyName() { + return StringUtils.isNotEmpty(authAnnotation.name()) ? authAnnotation.name() : null; + } + + private String getApiKeyValue(Object value) { + // 如果是参数驱动,使用参数值 + if (value instanceof String) { + return (String) value; + } + // 否则使用注解中的静态值 + return StringUtils.isNotEmpty(authAnnotation.value()) ? authAnnotation.value() : null; + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/scanner/resolver/RequestAuthResolverTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/scanner/resolver/RequestAuthResolverTest.java new file mode 100644 index 00000000..9e06f1e3 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/scanner/resolver/RequestAuthResolverTest.java @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.client.proxy.scanner.resolver; + +import modelengine.fit.http.annotation.RequestAuth; +import modelengine.fit.http.client.proxy.auth.AuthType; +import modelengine.fit.http.client.proxy.support.setter.AuthDestinationSetter; +import modelengine.fit.http.client.proxy.support.setter.DestinationSetterInfo; +import modelengine.fit.http.server.handler.Source; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * RequestAuthResolver 的单元测试。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +class RequestAuthResolverTest { + private RequestAuthResolver resolver; + + @BeforeEach + void setUp() { + resolver = new RequestAuthResolver(); + } + + @Test + void testResolveBearerAuth() { + // 创建Bearer Token注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.BEARER, "test-token", "", Source.HEADER, "", "", null); + String jsonPath = "$.token"; + + // 解析注解 + DestinationSetterInfo setterInfo = resolver.resolve(authAnnotation, jsonPath); + + // 验证结果 + assertNotNull(setterInfo); + assertInstanceOf(AuthDestinationSetter.class, setterInfo.destinationSetter()); + assertEquals(jsonPath, setterInfo.sourcePath()); + } + + @Test + void testResolveApiKeyAuth() { + // 创建API Key注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.API_KEY, "api-key-value", "X-API-Key", + Source.HEADER, "", "", null); + String jsonPath = "$.apiKey"; + + // 解析注解 + DestinationSetterInfo setterInfo = resolver.resolve(authAnnotation, jsonPath); + + // 验证结果 + assertNotNull(setterInfo); + assertInstanceOf(AuthDestinationSetter.class, setterInfo.destinationSetter()); + assertEquals(jsonPath, setterInfo.sourcePath()); + } + + @Test + void testResolveBasicAuth() { + // 创建Basic Auth注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.BASIC, "", "", Source.HEADER, + "admin", "password", null); + String jsonPath = "$"; + + // 解析注解 + DestinationSetterInfo setterInfo = resolver.resolve(authAnnotation, jsonPath); + + // 验证结果 + assertNotNull(setterInfo); + assertInstanceOf(AuthDestinationSetter.class, setterInfo.destinationSetter()); + assertEquals(jsonPath, setterInfo.sourcePath()); + } + + // 辅助方法:创建RequestAuth注解的模拟对象 + private RequestAuth createRequestAuth(AuthType type, String value, String name, Source location, + String username, String password, Class> provider) { + return new RequestAuth() { + @Override + public AuthType type() { + return type; + } + + @Override + public String value() { + return value; + } + + @Override + public String name() { + return name; + } + + @Override + public Source location() { + return location; + } + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public Class provider() { + return provider != null ? provider : modelengine.fit.http.client.proxy.auth.AuthProvider.class; + } + + @Override + public Class extends Annotation> annotationType() { + return RequestAuth.class; + } + }; + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetterTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetterTest.java new file mode 100644 index 00000000..a21dda24 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/proxy/support/setter/AuthDestinationSetterTest.java @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.client.proxy.support.setter; + +import modelengine.fit.http.annotation.RequestAuth; +import modelengine.fit.http.client.proxy.RequestBuilder; +import modelengine.fit.http.client.proxy.auth.AuthType; +import modelengine.fit.http.server.handler.Source; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.lang.annotation.Annotation; + +import static org.mockito.Mockito.*; + +/** + * AuthDestinationSetter 的单元测试。 + * + * @author 季聿阶 + * @since 2025-09-30 + */ +class AuthDestinationSetterTest { + @Test + void testSetBearerTokenStatic() { + // 创建 Bearer Token 注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.BEARER, "test-bearer-token", "", + Source.HEADER, "", "", null); + + AuthDestinationSetter setter = new AuthDestinationSetter(authAnnotation); + RequestBuilder mockBuilder = Mockito.mock(RequestBuilder.class); + + // 执行设置(静态 token,value 应该为 null) + setter.set(mockBuilder, null); + + // 验证是否调用了正确的 header 方法 + verify(mockBuilder).header("Authorization", "Bearer test-bearer-token"); + } + + @Test + void testSetBearerTokenDynamic() { + // 创建 Bearer Token 注解(没有静态值) + RequestAuth authAnnotation = createRequestAuth(AuthType.BEARER, "", "", + Source.HEADER, "", "", null); + + AuthDestinationSetter setter = new AuthDestinationSetter(authAnnotation); + RequestBuilder mockBuilder = Mockito.mock(RequestBuilder.class); + + // 执行设置(动态 token) + setter.set(mockBuilder, "dynamic-bearer-token"); + + // 验证是否调用了正确的 header 方法 + verify(mockBuilder).header("Authorization", "Bearer dynamic-bearer-token"); + } + + @Test + void testSetBasicAuth() { + // 创建 Basic Auth 注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.BASIC, "", "", + Source.HEADER, "admin", "secret", null); + + AuthDestinationSetter setter = new AuthDestinationSetter(authAnnotation); + RequestBuilder mockBuilder = Mockito.mock(RequestBuilder.class); + + // 执行设置 + setter.set(mockBuilder, null); + + // 验证是否调用了正确的 header 方法(Basic Auth 的 base64 编码) + verify(mockBuilder).header(eq("Authorization"), argThat(value -> + value.toString().startsWith("Basic "))); + } + + @Test + void testSetApiKeyHeader() { + // 创建 API Key Header 注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.API_KEY, "test-api-key", "X-API-Key", + Source.HEADER, "", "", null); + + AuthDestinationSetter setter = new AuthDestinationSetter(authAnnotation); + RequestBuilder mockBuilder = Mockito.mock(RequestBuilder.class); + + // 执行设置 + setter.set(mockBuilder, null); + + // 验证是否调用了正确的 header 方法 + verify(mockBuilder).header("X-API-Key", "test-api-key"); + } + + @Test + void testSetApiKeyQuery() { + // 创建 API Key Query 注解 + RequestAuth authAnnotation = createRequestAuth(AuthType.API_KEY, "test-api-key", "api_key", + Source.QUERY, "", "", null); + + AuthDestinationSetter setter = new AuthDestinationSetter(authAnnotation); + RequestBuilder mockBuilder = Mockito.mock(RequestBuilder.class); + + // 执行设置 + setter.set(mockBuilder, null); + + // 验证是否调用了正确的 query 方法 + verify(mockBuilder).query("api_key", "test-api-key"); + } + + // 辅助方法:创建 RequestAuth 注解的模拟对象 + private RequestAuth createRequestAuth(AuthType type, String value, String name, Source location, + String username, String password, Class> provider) { + return new RequestAuth() { + @Override + public AuthType type() { + return type; + } + + @Override + public String value() { + return value; + } + + @Override + public String name() { + return name; + } + + @Override + public Source location() { + return location; + } + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public Class provider() { + return provider != null ? provider : modelengine.fit.http.client.proxy.auth.AuthProvider.class; + } + + @Override + public Class extends Annotation> annotationType() { + return RequestAuth.class; + } + }; + } +} \ No newline at end of file