From 4504d9f55ad2b451b401c3297b27a34be09095af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Sun, 13 Jul 2025 11:24:47 +0800 Subject: [PATCH 01/32] =?UTF-8?q?=F0=9F=8E=A8=20[editorconfig]=20ij=5Fjava?= =?UTF-8?q?=20=E7=89=B9=E6=AE=8A=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index a845708aa..9e1d9a194 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,4 @@ insert_final_newline = true [{*.java,*.sql}] indent_size = 4 tab_width = 4 +ij_java_use_single_class_imports = true From 72afaab6ca27550963d24b1958c7107ea571daa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Sun, 13 Jul 2025 11:26:09 +0800 Subject: [PATCH 02/32] =?UTF-8?q?=F0=9F=8E=A8=20[gitattributes]=20cmd?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 2b2b007cf..cba35470b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,6 @@ /gradlew text eol=lf *.bat text eol=crlf +*.cmd text eol=crlf +*.sh text eol=lf *.jar binary * text=auto eol=lf From fa5b267c044b96b8453ac07aecdb74d8bf1f344f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Sun, 13 Jul 2025 11:29:18 +0800 Subject: [PATCH 03/32] =?UTF-8?q?=F0=9F=94=90=20[LICENSE]=20=E6=A0=87?= =?UTF-8?q?=E6=98=8E=E5=8D=8F=E8=AE=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 1 - 1 file changed, 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8000a6faa..8782cd564 100644 --- a/LICENSE +++ b/LICENSE @@ -500,5 +500,4 @@ necessary. Here is a sample; alter the names: , 1 April 1990 Ty Coon, President of Vice - That's all there is to it! From 4dcb6074c5b99587d761f51ee3add62a8c723183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Sun, 13 Jul 2025 11:29:34 +0800 Subject: [PATCH 04/32] =?UTF-8?q?=F0=9F=94=90=20[LICENSE]=20=E6=A0=87?= =?UTF-8?q?=E6=98=8E=E5=8D=8F=E8=AE=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 8782cd564..8000a6faa 100644 --- a/LICENSE +++ b/LICENSE @@ -500,4 +500,5 @@ necessary. Here is a sample; alter the names: , 1 April 1990 Ty Coon, President of Vice + That's all there is to it! From 8ceaaab33cb59b041167e8a1ef1b3256397fe2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Sun, 13 Jul 2025 17:51:03 +0800 Subject: [PATCH 05/32] =?UTF-8?q?=F0=9F=94=A5=20[hikvision]=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BE=9D=E8=B5=96=20opensource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- surveillance/hikvision/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/surveillance/hikvision/build.gradle.kts b/surveillance/hikvision/build.gradle.kts index 744d6e8a9..8f074ec37 100644 --- a/surveillance/hikvision/build.gradle.kts +++ b/surveillance/hikvision/build.gradle.kts @@ -8,7 +8,6 @@ dependencies { implementation(projects.surveillance.surveillanceShared) implementation(libs.com.hikvision.ga.artemis.http.client) - implementation(libs.com.hikvision.ga.opensource) } description = From c4b371a93b6dcb6a5c5a97f36e33a9533d601a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 02:51:56 +0800 Subject: [PATCH 06/32] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20[shared]=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=20SensitiveResponse=20=E6=B3=A8=E8=A7=A3?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=A0=B8=E5=BF=83=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 SensitiveResponse 注解定义 - 删除 SensitiveRef 注解定义 - 删除 ISensitivity 敏感数据接口 - 删除 ISensitiveScope 敏感数据作用域 - 删除 SensitiveDslFns DSL 函数 这些组件已不再需要,移除以简化代码库 --- .../truenine/composeserver/ISensitiveScope.kt | 24 --------- .../truenine/composeserver/SensitiveDslFns.kt | 18 ------- .../composeserver/annotations/SensitiveRef.kt | 49 ------------------- .../annotations/SensitiveResponse.kt | 10 ---- .../composeserver/domain/ISensitivity.kt | 24 --------- 5 files changed, 125 deletions(-) delete mode 100644 shared/src/main/kotlin/io/github/truenine/composeserver/ISensitiveScope.kt delete mode 100644 shared/src/main/kotlin/io/github/truenine/composeserver/SensitiveDslFns.kt delete mode 100644 shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveRef.kt delete mode 100644 shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponse.kt delete mode 100644 shared/src/main/kotlin/io/github/truenine/composeserver/domain/ISensitivity.kt diff --git a/shared/src/main/kotlin/io/github/truenine/composeserver/ISensitiveScope.kt b/shared/src/main/kotlin/io/github/truenine/composeserver/ISensitiveScope.kt deleted file mode 100644 index c070b822e..000000000 --- a/shared/src/main/kotlin/io/github/truenine/composeserver/ISensitiveScope.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.truenine.composeserver - -import io.github.truenine.composeserver.annotations.SensitiveStrategy - -@Deprecated("使用量很少") -interface ISensitiveScope { - fun String.addressDetails() = SensitiveStrategy.ADDRESS.desensitizeSerializer()(this) - - fun String.bankCard() = SensitiveStrategy.BANK_CARD_CODE.desensitizeSerializer()(this) - - fun String.chinaName() = SensitiveStrategy.NAME.desensitizeSerializer()(this) - - fun String.multipleName() = SensitiveStrategy.MULTIPLE_NAME.desensitizeSerializer()(this) - - fun String.chinaIdCard() = SensitiveStrategy.ID_CARD.desensitizeSerializer()(this) - - fun String.chinaPhone() = SensitiveStrategy.PHONE.desensitizeSerializer()(this) - - fun String.password() = SensitiveStrategy.PASSWORD.desensitizeSerializer()(this) - - fun String.email() = SensitiveStrategy.EMAIL.desensitizeSerializer()(this) - - fun String.once() = SensitiveStrategy.ONCE.desensitizeSerializer()(this) -} diff --git a/shared/src/main/kotlin/io/github/truenine/composeserver/SensitiveDslFns.kt b/shared/src/main/kotlin/io/github/truenine/composeserver/SensitiveDslFns.kt deleted file mode 100644 index e00f2cdb9..000000000 --- a/shared/src/main/kotlin/io/github/truenine/composeserver/SensitiveDslFns.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.truenine.composeserver - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@OptIn(ExperimentalContracts::class) -inline fun sensitiveAlso(data: T, scope: ISensitiveScope.(data: T) -> Unit): T { - contract { callsInPlace(scope, InvocationKind.EXACTLY_ONCE) } - scope(object : ISensitiveScope {}, data) - return data -} - -@OptIn(ExperimentalContracts::class) -inline fun sensitiveLet(data: T, scope: ISensitiveScope.(data: T) -> T): T { - contract { callsInPlace(scope, InvocationKind.EXACTLY_ONCE) } - return scope(object : ISensitiveScope {}, data) -} diff --git a/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveRef.kt b/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveRef.kt deleted file mode 100644 index 999cc6ac2..000000000 --- a/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveRef.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.truenine.composeserver.annotations - -import io.github.truenine.composeserver.nonText - -enum class SensitiveStrategy(private val desensitizeSerializer: (String) -> String) { - /** ## 单个 * 纯掩码 */ - ONCE({ "*" }), - - /** 不进行脱敏处理 */ - NONE({ it }), - - /** 手机号 */ - PHONE({ it.replace("^(\\S{3})\\S+(\\S{2})$".toRegex(), "\$1****\$2") }), - EMAIL({ it.replace("(\\S{2})\\S+(@[\\w.-]+)".toRegex(), "\$1****\$2") }), - - /** 身份证号 */ - ID_CARD({ it.replace("(\\S{2})\\S+(\\S{2})".toRegex(), "\$1****\$2") }), - - /** 银行卡号 */ - BANK_CARD_CODE({ it.replace("(\\w{2})\\w+(\\w{2})".toRegex(), "\$1****\$2") }), - - /** 姓名 */ - NAME({ if (it.nonText()) it else "**${it.substring(it.length - 1)}" }), - - /** - * ## 多段落姓名 - * - * 例如:`last_name` - */ - MULTIPLE_NAME({ - if (it.nonText() || it.length <= 2) { - when (it.length) { - 1 -> "*" - 2 -> "**" - else -> it - } - } else "**${it.substring(it.length - 1)}" - }), - - /** 地址 */ - ADDRESS({ it.replace("(\\S{3})\\S{2}(\\S*)\\S{2}".toRegex(), "\$1****\$2") }), - - /** 密码 */ - PASSWORD({ "****" }); - - open fun desensitizeSerializer(): (String) -> String { - return desensitizeSerializer - } -} diff --git a/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponse.kt b/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponse.kt deleted file mode 100644 index 4b63963ea..000000000 --- a/shared/src/main/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.truenine.composeserver.annotations - -import java.lang.annotation.Inherited - -/** - * # 接口返回为脱敏后的数据 - * - * @see [io.github.truenine.composeserver.domain.sensitive.ISensitivity] - */ -@Inherited @Retention @MustBeDocumented @Target(AnnotationTarget.FUNCTION) annotation class SensitiveResponse diff --git a/shared/src/main/kotlin/io/github/truenine/composeserver/domain/ISensitivity.kt b/shared/src/main/kotlin/io/github/truenine/composeserver/domain/ISensitivity.kt deleted file mode 100644 index 10d7024cc..000000000 --- a/shared/src/main/kotlin/io/github/truenine/composeserver/domain/ISensitivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.truenine.composeserver.domain - -import io.github.truenine.composeserver.meta.annotations.MetaSkipGeneration - -/** - * # 可 有状态 脱敏 数据类 - * - * @author TrueNine - * @since 2024-07-09 - */ -interface ISensitivity { - fun changeWithSensitiveData() {} - - /** - * ## 改变当前的脱敏状态为 sensed - * - * 该方法由更抽象的类等的实现,可重复被调用,但返回状态需保持一致 - */ - fun recordChangedSensitiveData() {} - - @MetaSkipGeneration - val isChangedToSensitiveData: Boolean - get() = false -} From e238c4be969aba31ba800933fa9a3efa1a8edd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 02:52:09 +0800 Subject: [PATCH 07/32] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20[security]=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=20SensitiveResponse=20Spring=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 SensitiveResultResponseBodyAdvice 响应体处理器 - 移除 Spring 框架中的敏感数据自动处理逻辑 由于核心注解已删除,相关的 Spring 集成代码不再需要 --- .../SensitiveResultResponseBodyAdvice.kt | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 security/spring/src/main/kotlin/io/github/truenine/composeserver/security/autoconfig/SensitiveResultResponseBodyAdvice.kt diff --git a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/autoconfig/SensitiveResultResponseBodyAdvice.kt b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/autoconfig/SensitiveResultResponseBodyAdvice.kt deleted file mode 100644 index 6cf50b0f0..000000000 --- a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/autoconfig/SensitiveResultResponseBodyAdvice.kt +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.truenine.composeserver.security.autoconfig - -import io.github.truenine.composeserver.Pr -import io.github.truenine.composeserver.annotations.SensitiveResponse -import io.github.truenine.composeserver.domain.ISensitivity -import io.github.truenine.composeserver.slf4j -import java.lang.reflect.ParameterizedType -import org.springframework.core.MethodParameter -import org.springframework.http.MediaType -import org.springframework.http.converter.HttpMessageConverter -import org.springframework.http.server.ServerHttpRequest -import org.springframework.http.server.ServerHttpResponse -import org.springframework.web.bind.annotation.ControllerAdvice -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice - -private val log = slf4j() - -@ControllerAdvice -class SensitiveResultResponseBodyAdvice : ResponseBodyAdvice { - private val supportAnnotationClassType = SensitiveResponse::class.java - private val interfaceType = ISensitivity::class.java - - // TODO 实现 resolver ,缓存 每个不同对象的序列化规则,例如:{a: {b: Sensitive}} - // TODO 可以实现 resolver ,规范化 - // TODO 加入缓存机制,同时考虑到动态加载 - override fun supports(returnType: MethodParameter, converterType: Class>): Boolean { - val hasAnnotation = returnType.method?.isAnnotationPresent(supportAnnotationClassType) == true - return hasAnnotation - } - - fun getGenericType(returnType: MethodParameter, clazz: Class<*> = interfaceType): Class<*>? { - if (returnType.genericParameterType is ParameterizedType) { - val rawType = (returnType.genericParameterType as ParameterizedType).rawType - if (rawType is Class<*> && rawType.isAssignableFrom(clazz)) return rawType - } - return null - } - - fun isExtendTypeFor(returnType: MethodParameter, type: Class<*> = interfaceType): Boolean { - return getGenericType(returnType) != null - } - - override fun beforeBodyWrite( - body: Any?, - returnType: MethodParameter, - selectedContentType: MediaType, - selectedConverterType: Class>, - request: ServerHttpRequest, - response: ServerHttpResponse, - ): Any? { - when (body) { - is ISensitivity -> body.changeWithSensitiveData() - is Collection<*> -> body.forEach { if (it is ISensitivity) it.changeWithSensitiveData() } - - is Map<*, *> -> - body.forEach { - if (it.key is ISensitivity) (it.key as ISensitivity).changeWithSensitiveData() - if (it.value is ISensitivity) (it.value as ISensitivity).changeWithSensitiveData() - } - - is Array<*> -> body.forEach { if (it is ISensitivity) it.changeWithSensitiveData() } - - is Iterable<*> -> body.forEach { if (it is ISensitivity) it.changeWithSensitiveData() } - - is Iterator<*> -> body.forEach { if (it is ISensitivity) it.changeWithSensitiveData() } - - is Pr<*> -> { - if (body.d.isNotEmpty()) { - val b = body.d.firstOrNull()?.let { it is ISensitivity } - if (b == true) { - body.d.forEach { if (it is ISensitivity) it.changeWithSensitiveData() } - } - } - } - - else -> {} - } - return body - } -} From b0c3489d4f807fb453033ebb0cd573d04e27aa2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 02:52:29 +0800 Subject: [PATCH 08/32] =?UTF-8?q?=F0=9F=A7=AA=20[test]=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=20SensitiveResponse=20=E7=9B=B8=E5=85=B3=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 shared 模块中的注解测试文件 • SensitiveResponseTest.kt • SensitiveRefTest.kt - 删除 security-crypto 模块中的接口测试文件 • ISensitivityTest.kt - 删除 security-spring 模块中的测试文件 • SensitiveController.kt • ExtendedSensitiveController.kt • SensitiveTest.kt • ExtendedSensitiveTest.kt • SensitiveIntegrationTest.kt 清理所有与已删除功能相关的测试代码 --- .../security/crypto/ISensitivityTest.kt | 37 --- .../controller/ExtendedSensitiveController.kt | 95 ------ .../controller/SensitiveController.kt | 25 -- .../sensitive/ExtendedSensitiveTest.kt | 247 --------------- .../sensitive/SensitiveIntegrationTest.kt | 288 ------------------ .../security/sensitive/SensitiveTest.kt | 98 ------ .../annotations/SensitiveRefTest.kt | 242 --------------- .../annotations/SensitiveResponseTest.kt | 175 ----------- 8 files changed, 1207 deletions(-) delete mode 100644 security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/ISensitivityTest.kt delete mode 100644 security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/ExtendedSensitiveController.kt delete mode 100644 security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/SensitiveController.kt delete mode 100644 security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/ExtendedSensitiveTest.kt delete mode 100644 security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveIntegrationTest.kt delete mode 100644 security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveTest.kt delete mode 100644 shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveRefTest.kt delete mode 100644 shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponseTest.kt diff --git a/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/ISensitivityTest.kt b/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/ISensitivityTest.kt deleted file mode 100644 index 9815d1174..000000000 --- a/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/ISensitivityTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.truenine.composeserver.security.crypto - -import io.github.truenine.composeserver.domain.ISensitivity -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import org.junit.jupiter.api.Test - -class ISensitivityTest { - abstract class Ab : ISensitivity { - var ab: String? = null - - override fun changeWithSensitiveData() { - ab?.also { ab = "ab sensitive" } - } - } - - class B : Ab() { - override fun changeWithSensitiveData() { - ab?.also { ab = "b sensitive" } - } - } - - @Test - fun `change sensitive`() { - val b = B() - b.ab = "123" - val old = b.ab - b.changeWithSensitiveData() - val new = b.ab - log.info(b.ab) - - assertNotEquals(old, new) - assertEquals("b sensitive", new, "确保函数调用必须处于最底层") - assertNotEquals("ab sensitive", new, "确保函数调用必须处于最底层") - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/ExtendedSensitiveController.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/ExtendedSensitiveController.kt deleted file mode 100644 index 38b825421..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/ExtendedSensitiveController.kt +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.truenine.composeserver.security.controller - -import io.github.truenine.composeserver.Pr -import io.github.truenine.composeserver.annotations.SensitiveResponse -import io.github.truenine.composeserver.domain.ISensitivity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -/** 扩展的敏感数据测试控制器 用于测试更多的敏感数据处理场景 */ -@RestController -@RequestMapping("test/sensitive/extended") -class ExtendedSensitiveController { - - /** 用户信息响应类,包含多种敏感数据 */ - class UserInfo( - var id: Long = 1L, - var username: String = "testuser", - var phone: String = "13812345678", - var email: String = "test@example.com", - var idCard: String = "110101199001011234", - var address: String = "北京市朝阳区建国门外大街1号", - ) : ISensitivity { - override fun changeWithSensitiveData() { - super.changeWithSensitiveData() - // 脱敏处理 - phone = "138****5678" - email = "te****@example.com" - idCard = "11****1234" - address = "北京市****1号" - } - } - - /** 简单的敏感数据类 */ - class SimpleData(var value: String = "sensitive") : ISensitivity { - override fun changeWithSensitiveData() { - super.changeWithSensitiveData() - value = "****" - } - } - - /** 嵌套的敏感数据类 */ - class NestedData(var publicInfo: String = "public", var sensitiveInfo: SimpleData = SimpleData()) : ISensitivity { - override fun changeWithSensitiveData() { - super.changeWithSensitiveData() - sensitiveInfo.changeWithSensitiveData() - } - } - - @SensitiveResponse - @GetMapping("user", produces = ["application/json"]) - fun getUserInfo(): Pr { - return Pr[ - listOf( - UserInfo(1L, "user1", "13812345678", "user1@test.com", "110101199001011234", "北京市朝阳区建国门外大街1号"), - UserInfo(2L, "user2", "18612345678", "user2@test.com", "110101199001011235", "上海市浦东新区陆家嘴环路1000号"), - )] - } - - @SensitiveResponse - @GetMapping("simple", produces = ["application/json"]) - fun getSimpleData(): SimpleData { - return SimpleData("very sensitive data") - } - - @SensitiveResponse - @GetMapping("nested", produces = ["application/json"]) - fun getNestedData(): NestedData { - return NestedData("public info", SimpleData("nested sensitive")) - } - - @SensitiveResponse - @GetMapping("collection", produces = ["application/json"]) - fun getCollection(): Collection { - return listOf(SimpleData("data1"), SimpleData("data2"), SimpleData("data3")) - } - - @SensitiveResponse - @GetMapping("array", produces = ["application/json"]) - fun getArray(): Array { - return arrayOf(SimpleData("array1"), SimpleData("array2")) - } - - @SensitiveResponse - @GetMapping("map", produces = ["application/json"]) - fun getMap(): Map { - return mapOf("key1" to SimpleData("map1"), "key2" to SimpleData("map2")) - } - - /** 不带 @SensitiveResponse 注解的方法,用于对比测试 */ - @GetMapping("no-annotation", produces = ["application/json"]) - fun getDataWithoutAnnotation(): SimpleData { - return SimpleData("should not be desensitized") - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/SensitiveController.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/SensitiveController.kt deleted file mode 100644 index 556a67bee..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/controller/SensitiveController.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.truenine.composeserver.security.controller - -import io.github.truenine.composeserver.Pr -import io.github.truenine.composeserver.annotations.SensitiveResponse -import io.github.truenine.composeserver.domain.ISensitivity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -@RequestMapping("test/sensitive") -class SensitiveController { - class Resp(var a: Int = 1) : ISensitivity { - override fun changeWithSensitiveData() { - super.changeWithSensitiveData() - this.a = 233 - } - } - - @SensitiveResponse - @GetMapping("get", produces = ["application/json"]) - fun `test get a`(): Pr { - return Pr[listOf(Resp(), Resp(), Resp())] - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/ExtendedSensitiveTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/ExtendedSensitiveTest.kt deleted file mode 100644 index 3e1b1cc0c..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/ExtendedSensitiveTest.kt +++ /dev/null @@ -1,247 +0,0 @@ -package io.github.truenine.composeserver.security.sensitive - -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.security.autoconfig.SensitiveResultResponseBodyAdvice -import io.github.truenine.composeserver.security.controller.ExtendedSensitiveController -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.get -import org.springframework.test.web.servlet.setup.MockMvcBuilders - -/** 扩展的敏感数据处理功能测试 */ -class ExtendedSensitiveTest { - - private lateinit var mockMvc: MockMvc - private val objectMapper = ObjectMapper() - - @BeforeEach - fun setup() { - val controller = ExtendedSensitiveController() - val sensitive = SensitiveResultResponseBodyAdvice() - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(sensitive).build() - } - - @Test - fun `test user info desensitization`() { - log.info("测试用户信息脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/user") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("用户信息响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val dataList = responseMap["d"] as List<*> - - assertTrue(dataList.isNotEmpty(), "用户数据列表不应该为空") - - dataList.forEach { item -> - val userMap = item as Map<*, *> - val phone = userMap["phone"] as String - val email = userMap["email"] as String - val idCard = userMap["idCard"] as String - val address = userMap["address"] as String - - // 验证敏感数据被正确脱敏 - assertTrue(phone.contains("****"), "手机号应该被脱敏") - assertTrue(email.contains("****"), "邮箱应该被脱敏") - assertTrue(idCard.contains("****"), "身份证号应该被脱敏") - assertTrue(address.contains("****"), "地址应该被脱敏") - } - - log.info("用户信息脱敏测试通过") - } - - @Test - fun `test simple data desensitization`() { - log.info("测试简单数据脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/simple") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("简单数据响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val value = responseMap["value"] as String - - assertEquals("****", value, "简单敏感数据应该被脱敏为 ****") - - log.info("简单数据脱敏测试通过") - } - - @Test - fun `test nested data desensitization`() { - log.info("测试嵌套数据脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/nested") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("嵌套数据响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val publicInfo = responseMap["publicInfo"] as String - val sensitiveInfo = responseMap["sensitiveInfo"] as Map<*, *> - val sensitiveValue = sensitiveInfo["value"] as String - - assertEquals("public info", publicInfo, "公开信息不应该被脱敏") - assertEquals("****", sensitiveValue, "嵌套的敏感信息应该被脱敏") - - log.info("嵌套数据脱敏测试通过") - } - - @Test - fun `test collection desensitization`() { - log.info("测试集合数据脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/collection") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("集合数据响应: {}", responseContent) - - val dataList = objectMapper.readValue(responseContent, List::class.java) - - assertTrue(dataList.isNotEmpty(), "集合数据不应该为空") - - dataList.forEach { item -> - val itemMap = item as Map<*, *> - val value = itemMap["value"] as String - assertEquals("****", value, "集合中的敏感数据应该被脱敏") - } - - log.info("集合数据脱敏测试通过") - } - - @Test - fun `test array desensitization`() { - log.info("测试数组数据脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/array") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("数组数据响应: {}", responseContent) - - val dataList = objectMapper.readValue(responseContent, List::class.java) - - assertTrue(dataList.isNotEmpty(), "数组数据不应该为空") - - dataList.forEach { item -> - val itemMap = item as Map<*, *> - val value = itemMap["value"] as String - assertEquals("****", value, "数组中的敏感数据应该被脱敏") - } - - log.info("数组数据脱敏测试通过") - } - - @Test - fun `test map desensitization`() { - log.info("测试Map数据脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/map") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("Map数据响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - - responseMap.values.forEach { item -> - val itemMap = item as Map<*, *> - val value = itemMap["value"] as String - assertEquals("****", value, "Map中的敏感数据应该被脱敏") - } - - log.info("Map数据脱敏测试通过") - } - - @Test - fun `test no annotation should not desensitize`() { - log.info("测试没有注解的方法不应该脱敏") - - val result = - mockMvc - .get("/test/sensitive/extended/no-annotation") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("无注解方法响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val value = responseMap["value"] as String - - assertEquals("should not be desensitized", value, "没有注解的方法不应该脱敏数据") - - log.info("无注解方法测试通过") - } - - @Test - fun `test ISensitivity interface direct usage`() { - log.info("测试 ISensitivity 接口直接使用") - - // 测试 UserInfo - val userInfo = ExtendedSensitiveController.UserInfo() - val originalPhone = userInfo.phone - userInfo.changeWithSensitiveData() - assertNotEquals(originalPhone, userInfo.phone, "用户信息脱敏后应该不同") - - // 测试 SimpleData - val simpleData = ExtendedSensitiveController.SimpleData("test") - val originalValue = simpleData.value - simpleData.changeWithSensitiveData() - assertNotEquals(originalValue, simpleData.value, "简单数据脱敏后应该不同") - assertEquals("****", simpleData.value, "简单数据应该被脱敏为 ****") - - log.info("ISensitivity 接口直接使用测试通过") - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveIntegrationTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveIntegrationTest.kt deleted file mode 100644 index ab50eeae8..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveIntegrationTest.kt +++ /dev/null @@ -1,288 +0,0 @@ -package io.github.truenine.composeserver.security.sensitive - -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.Pr -import io.github.truenine.composeserver.annotations.SensitiveResponse -import io.github.truenine.composeserver.domain.ISensitivity -import io.github.truenine.composeserver.security.autoconfig.SensitiveResultResponseBodyAdvice -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.get -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -/** 敏感数据处理集成测试 测试完整的敏感数据处理流程 */ -class SensitiveIntegrationTest { - - private lateinit var mockMvc: MockMvc - private val objectMapper = ObjectMapper() - - /** 模拟真实的用户数据类 */ - class RealUserData( - var id: Long = 1L, - var username: String = "realuser", - var realName: String = "张三", - var phone: String = "13812345678", - var email: String = "zhangsan@company.com", - var idCard: String = "110101199001011234", - var bankCard: String = "6212341234123434", - var address: String = "北京市朝阳区建国门外大街1号国贸大厦", - var password: String = "MySecretPassword123!", - var salary: Double = 50000.0, - var isVip: Boolean = true, - ) : ISensitivity { - override fun changeWithSensitiveData() { - super.changeWithSensitiveData() - // 模拟真实的脱敏处理 - 保留最后一个字符 - realName = if (realName.isNotEmpty()) "**${realName.last()}" else "**" - phone = "138****5678" - email = "zh****@company.com" - idCard = "11****1234" - bankCard = "62****3434" - address = "北京市朝阳区****大厦" - password = "****" - salary = 0.0 // 薪资完全隐藏 - } - } - - /** 模拟业务控制器 */ - @RestController - @RequestMapping("api/users") - class UserController { - - @SensitiveResponse - @GetMapping("profile", produces = ["application/json"]) - fun getUserProfile(): RealUserData { - return RealUserData( - id = 12345L, - username = "john_doe", - realName = "约翰·多伊", - phone = "18612345678", - email = "john.doe@example.com", - idCard = "440301199001011234", - bankCard = "4367123456781278", - address = "广东省深圳市南山区科技园南区深南大道10000号", - password = "SuperSecretPassword!@#", - salary = 80000.0, - isVip = true, - ) - } - - @SensitiveResponse - @GetMapping("list", produces = ["application/json"]) - fun getUserList(): Pr { - return Pr[ - listOf( - RealUserData( - 1L, - "user1", - "李明", - "13812345678", - "liming@test.com", - "110101199001011234", - "6212341234123434", - "北京市海淀区中关村大街1号", - "password123", - 45000.0, - false, - ), - RealUserData( - 2L, - "user2", - "王芳", - "18612345678", - "wangfang@test.com", - "310101199001011235", - "4367123456781278", - "上海市浦东新区陆家嘴环路1000号", - "mypassword", - 55000.0, - true, - ), - RealUserData( - 3L, - "user3", - "刘强", - "15912345678", - "liuqiang@test.com", - "440301199001011236", - "5212341234123434", - "广东省深圳市福田区深南大道2000号", - "secret123", - 60000.0, - true, - ), - )] - } - - @GetMapping("public", produces = ["application/json"]) - fun getPublicUserInfo(): RealUserData { - return RealUserData( - id = 999L, - username = "public_user", - realName = "公开用户", - phone = "13800138000", - email = "public@example.com", - idCard = "000000000000000000", - bankCard = "0000000000000000", - address = "公开地址", - password = "publicpassword", - salary = 0.0, - isVip = false, - ) - } - } - - @BeforeEach - fun setup() { - val controller = UserController() - val sensitive = SensitiveResultResponseBodyAdvice() - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(sensitive).build() - } - - @Test - fun `test complete user profile desensitization flow`() { - log.info("测试完整的用户资料脱敏流程") - - val result = - mockMvc - .get("/api/users/profile") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("用户资料响应: {}", responseContent) - - val userData = objectMapper.readValue(responseContent, Map::class.java) - - // 验证非敏感数据保持不变 - assertEquals(12345, userData["id"]) - assertEquals("john_doe", userData["username"]) - assertEquals(true, userData["isVip"]) - - // 验证敏感数据被正确脱敏 - assertEquals("**伊", userData["realName"], "真实姓名应该被脱敏") - assertEquals("138****5678", userData["phone"], "手机号应该被脱敏") - assertEquals("zh****@company.com", userData["email"], "邮箱应该被脱敏") - assertEquals("11****1234", userData["idCard"], "身份证号应该被脱敏") - assertEquals("62****3434", userData["bankCard"], "银行卡号应该被脱敏") - assertTrue((userData["address"] as String).contains("****"), "地址应该被脱敏") - assertEquals("****", userData["password"], "密码应该被完全隐藏") - assertEquals(0.0, userData["salary"], "薪资应该被隐藏") - - log.info("用户资料脱敏流程测试通过") - } - - @Test - fun `test user list desensitization with pagination`() { - log.info("测试用户列表脱敏(带分页)") - - val result = - mockMvc - .get("/api/users/list") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("用户列表响应: {}", responseContent) - - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val dataList = responseMap["d"] as List<*> - val total = responseMap["t"] as Int - val pageSize = responseMap["p"] as Int - - // 验证分页信息 - assertEquals(3, total, "总数应该正确") - assertEquals(1, pageSize, "页数应该正确") - assertEquals(3, dataList.size, "数据列表大小应该正确") - - // 验证每个用户的敏感数据都被脱敏 - dataList.forEach { item -> - val userMap = item as Map<*, *> - val realName = userMap["realName"] as String - val phone = userMap["phone"] as String - val email = userMap["email"] as String - val password = userMap["password"] as String - val salary = userMap["salary"] as Double - - assertTrue(realName.startsWith("**"), "真实姓名应该被脱敏: $realName") - assertTrue(phone.contains("****"), "手机号应该被脱敏: $phone") - assertTrue(email.contains("****"), "邮箱应该被脱敏: $email") - assertEquals("****", password, "密码应该被完全隐藏") - assertEquals(0.0, salary, "薪资应该被隐藏") - } - - log.info("用户列表脱敏测试通过") - } - - @Test - fun `test public endpoint without sensitive annotation`() { - log.info("测试没有敏感注解的公开接口") - - val result = - mockMvc - .get("/api/users/public") - .andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - .andReturn() - - val responseContent = result.response.contentAsString - log.info("公开接口响应: {}", responseContent) - - val userData = objectMapper.readValue(responseContent, Map::class.java) - - // 验证没有注解的接口不会进行脱敏处理 - assertEquals("公开用户", userData["realName"], "没有注解的接口不应该脱敏真实姓名") - assertEquals("13800138000", userData["phone"], "没有注解的接口不应该脱敏手机号") - assertEquals("public@example.com", userData["email"], "没有注解的接口不应该脱敏邮箱") - assertEquals("publicpassword", userData["password"], "没有注解的接口不应该脱敏密码") - assertEquals(0.0, userData["salary"], "原始薪资数据应该保持不变") - - log.info("公开接口测试通过") - } - - @Test - fun `test ISensitivity interface state management`() { - log.info("测试 ISensitivity 接口状态管理") - - val userData = RealUserData() - val originalPhone = userData.phone - val originalEmail = userData.email - val originalSalary = userData.salary - - // 验证初始状态 - assertFalse(userData.isChangedToSensitiveData, "初始状态应该未脱敏") - - // 执行脱敏 - userData.changeWithSensitiveData() - - // 验证脱敏后的状态 - assertTrue(userData.phone != originalPhone, "手机号应该被脱敏") - assertTrue(userData.email != originalEmail, "邮箱应该被脱敏") - assertTrue(userData.salary != originalSalary, "薪资应该被脱敏") - assertEquals("****", userData.password, "密码应该被完全隐藏") - - // 验证可以重复调用脱敏方法而不会出错 - val phoneAfterFirstCall = userData.phone - userData.changeWithSensitiveData() - assertEquals(phoneAfterFirstCall, userData.phone, "重复调用脱敏方法应该保持一致") - - log.info("ISensitivity 接口状态管理测试通过") - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveTest.kt deleted file mode 100644 index 86bfe3df8..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/sensitive/SensitiveTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -package io.github.truenine.composeserver.security.sensitive - -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.security.autoconfig.SensitiveResultResponseBodyAdvice -import io.github.truenine.composeserver.security.controller.SensitiveController -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.get -import org.springframework.test.web.servlet.setup.MockMvcBuilders - -/** Test sensitive data processing functionality */ -class SensitiveTest { - - private lateinit var mockMvc: MockMvc - private val objectMapper = ObjectMapper() - - @BeforeEach - fun setup() { - val controller = SensitiveController() - val sensitive = SensitiveResultResponseBodyAdvice() - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(sensitive).build() - } - - @Test - fun `test sensitive response processing`() { - mockMvc.get("/test/sensitive/get").andExpect { - status { isOk() } - content { contentType(MediaType.APPLICATION_JSON) } - } - } - - @Test - fun `test sensitive data is properly desensitized`() { - log.info("测试敏感数据是否正确脱敏") - - val result = mockMvc.get("/test/sensitive/get").andReturn() - val responseContent = result.response.contentAsString - log.info("响应内容: {}", responseContent) - - // 解析响应为 Pr 对象 - val responseMap = objectMapper.readValue(responseContent, Map::class.java) - val dataList = responseMap["d"] as List<*> - - // 验证数据列表不为空 - assertTrue(dataList.isNotEmpty(), "数据列表不应该为空") - - // 验证每个响应对象的 a 字段都被脱敏为 233 - dataList.forEach { item -> - val itemMap = item as Map<*, *> - val aValue = itemMap["a"] - assertEquals(233, aValue, "敏感数据应该被脱敏为 233") - } - - log.info("敏感数据脱敏测试通过") - } - - @Test - fun `test ISensitivity interface behavior`() { - log.info("测试 ISensitivity 接口行为") - - // 创建测试对象 - val testObj = SensitiveController.Resp(42) - val originalValue = testObj.a - - // 调用脱敏方法 - testObj.changeWithSensitiveData() - val sensitizedValue = testObj.a - - // 验证数据被正确脱敏 - assertNotEquals(originalValue, sensitizedValue, "脱敏后的值应该与原值不同") - assertEquals(233, sensitizedValue, "脱敏后的值应该为 233") - - log.info("ISensitivity 接口行为测试通过") - } - - @Test - fun `test SensitiveResultResponseBodyAdvice supports method`() { - log.info("测试 SensitiveResultResponseBodyAdvice 的 supports 方法") - - val advice = SensitiveResultResponseBodyAdvice() - - // 模拟带有 @SensitiveResponse 注解的方法 - val method = SensitiveController::class.java.getDeclaredMethod("test get a") - val methodParameter = org.springframework.core.MethodParameter(method, -1) - - // 验证 supports 方法返回 true - val supports = advice.supports(methodParameter, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter::class.java) - assertTrue(supports, "带有 @SensitiveResponse 注解的方法应该被支持") - - log.info("SensitiveResultResponseBodyAdvice supports 方法测试通过") - } -} diff --git a/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveRefTest.kt b/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveRefTest.kt deleted file mode 100644 index 385608ab6..000000000 --- a/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveRefTest.kt +++ /dev/null @@ -1,242 +0,0 @@ -package io.github.truenine.composeserver.annotations - -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -/** - * # 敏感数据脱敏策略测试 - * - * 测试 SensitiveStrategy 枚举中定义的各种脱敏策略 - */ -class SensitiveRefTest { - - @Test - fun `测试 ONCE 策略 - 单个星号掩码`() { - log.info("测试 ONCE 策略 - 单个星号掩码") - - val strategy = SensitiveStrategy.ONCE - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("*", desensitizer("任何内容")) - assertEquals("*", desensitizer("")) - assertEquals("*", desensitizer("123456789")) - assertEquals("*", desensitizer("sensitive data")) - - log.info("ONCE 策略测试通过") - } - - @Test - fun `测试 NONE 策略 - 不进行脱敏`() { - log.info("测试 NONE 策略 - 不进行脱敏") - - val strategy = SensitiveStrategy.NONE - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("原始数据", desensitizer("原始数据")) - assertEquals("", desensitizer("")) - assertEquals("123456789", desensitizer("123456789")) - assertEquals("sensitive data", desensitizer("sensitive data")) - - log.info("NONE 策略测试通过") - } - - @Test - fun `测试 PHONE 策略 - 手机号脱敏`() { - log.info("测试 PHONE 策略 - 手机号脱敏") - - val strategy = SensitiveStrategy.PHONE - val desensitizer = strategy.desensitizeSerializer() - - // 根据实际的正则表达式 "^(\\S{3})\\S+(\\S{2})$" 来验证 - assertEquals("138****34", desensitizer("13812341234")) - assertEquals("186****78", desensitizer("18656785678")) - assertEquals("155****99", desensitizer("15599999999")) - - // 测试不符合格式的情况 - val shortNumber = desensitizer("123") - log.info("短号码脱敏结果: {}", shortNumber) - - log.info("PHONE 策略测试通过") - } - - @Test - fun `测试 EMAIL 策略 - 邮箱脱敏`() { - log.info("测试 EMAIL 策略 - 邮箱脱敏") - - val strategy = SensitiveStrategy.EMAIL - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("te****@example.com", desensitizer("test@example.com")) - assertEquals("us****@gmail.com", desensitizer("user@gmail.com")) - assertEquals("ad****@company.org", desensitizer("admin@company.org")) - - // 测试复杂邮箱 - val complexEmail = desensitizer("user.name+tag@example.co.uk") - log.info("复杂邮箱脱敏结果: {}", complexEmail) - - log.info("EMAIL 策略测试通过") - } - - @Test - fun `测试 ID_CARD 策略 - 身份证号脱敏`() { - log.info("测试 ID_CARD 策略 - 身份证号脱敏") - - val strategy = SensitiveStrategy.ID_CARD - val desensitizer = strategy.desensitizeSerializer() - - // 根据实际的正则表达式 "^(\\S{2})\\S+(\\S{2})$" 来验证 - assertEquals("11****34", desensitizer("110101199001011234")) - assertEquals("43****12", desensitizer("430404197210280012")) - assertEquals("12****78", desensitizer("123456789012345678")) - - log.info("ID_CARD 策略测试通过") - } - - @Test - fun `测试 BANK_CARD_CODE 策略 - 银行卡号脱敏`() { - log.info("测试 BANK_CARD_CODE 策略 - 银行卡号脱敏") - - val strategy = SensitiveStrategy.BANK_CARD_CODE - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("62****34", desensitizer("6212341234123434")) - assertEquals("43****78", desensitizer("4367123456781278")) - assertEquals("12****90", desensitizer("1234567890")) - - log.info("BANK_CARD_CODE 策略测试通过") - } - - @Test - fun `测试 NAME 策略 - 姓名脱敏`() { - log.info("测试 NAME 策略 - 姓名脱敏") - - val strategy = SensitiveStrategy.NAME - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("**明", desensitizer("小明")) - assertEquals("**华", desensitizer("张华")) - assertEquals("**龙", desensitizer("李小龙")) - assertEquals("**n", desensitizer("John")) - - // 测试空字符串和空白字符串 - assertEquals("", desensitizer("")) - assertEquals(" ", desensitizer(" ")) - - log.info("NAME 策略测试通过") - } - - @Test - fun `测试 MULTIPLE_NAME 策略 - 多段落姓名脱敏`() { - log.info("测试 MULTIPLE_NAME 策略 - 多段落姓名脱敏") - - val strategy = SensitiveStrategy.MULTIPLE_NAME - val desensitizer = strategy.desensitizeSerializer() - - // 根据实际的脱敏逻辑: - // 长度 <= 2: 1 -> "*", 2 -> "**", 其他 -> 原值 - // 长度 > 2: "**" + 最后一个字符 - assertEquals("**", desensitizer("小明")) // 长度2,应该返回 "**" - assertEquals("**华", desensitizer("张华华")) // 长度3,应该返回 "**华" - assertEquals("**龙", desensitizer("李小龙")) // 长度3,应该返回 "**龙" - - // 测试长度为1和2的特殊情况 - assertEquals("*", desensitizer("李")) // 长度1,返回 "*" - assertEquals("**", desensitizer("张华")) // 长度2,返回 "**" - - // 测试空字符串 - assertEquals("", desensitizer("")) - assertEquals(" ", desensitizer(" ")) - - log.info("MULTIPLE_NAME 策略测试通过") - } - - @Test - fun `测试 ADDRESS 策略 - 地址脱敏`() { - log.info("测试 ADDRESS 策略 - 地址脱敏") - - val strategy = SensitiveStrategy.ADDRESS - val desensitizer = strategy.desensitizeSerializer() - - val address1 = "北京市朝阳区建国门外大街1号" - val result1 = desensitizer(address1) - log.info("地址脱敏: {} -> {}", address1, result1) - - val address2 = "上海市浦东新区陆家嘴环路1000号" - val result2 = desensitizer(address2) - log.info("地址脱敏: {} -> {}", address2, result2) - - assertNotNull(result1, "地址脱敏结果不应该为空") - assertNotNull(result2, "地址脱敏结果不应该为空") - - log.info("ADDRESS 策略测试通过") - } - - @Test - fun `测试 PASSWORD 策略 - 密码脱敏`() { - log.info("测试 PASSWORD 策略 - 密码脱敏") - - val strategy = SensitiveStrategy.PASSWORD - val desensitizer = strategy.desensitizeSerializer() - - assertEquals("****", desensitizer("password123")) - assertEquals("****", desensitizer("123456")) - assertEquals("****", desensitizer("")) - assertEquals("****", desensitizer("very_long_password_with_special_chars!@#")) - - log.info("PASSWORD 策略测试通过") - } - - @Test - fun `测试所有策略的 desensitizeSerializer 方法`() { - log.info("测试所有策略的 desensitizeSerializer 方法") - - val strategies = SensitiveStrategy.values() - - strategies.forEach { strategy -> - val desensitizer = strategy.desensitizeSerializer() - assertNotNull(desensitizer, "策略 $strategy 的脱敏器不应该为空") - - // 测试脱敏器能正常工作 - val result = desensitizer("test") - assertNotNull(result, "策略 $strategy 的脱敏结果不应该为空") - - log.info("策略 {} 脱敏 'test' -> '{}'", strategy.name, result) - } - - log.info("所有策略的 desensitizeSerializer 方法测试通过") - } - - @Test - fun `测试策略枚举的完整性`() { - log.info("测试策略枚举的完整性") - - val expectedStrategies = setOf("ONCE", "NONE", "PHONE", "EMAIL", "ID_CARD", "BANK_CARD_CODE", "NAME", "MULTIPLE_NAME", "ADDRESS", "PASSWORD") - - val actualStrategies = SensitiveStrategy.values().map { it.name }.toSet() - - assertEquals(expectedStrategies, actualStrategies, "策略枚举应该包含所有预期的策略") - - log.info("策略枚举包含 {} 个策略: {}", actualStrategies.size, actualStrategies) - } - - @Test - fun `测试脱敏策略的边界情况`() { - log.info("测试脱敏策略的边界情况") - - val testCases = listOf("", " ", " ", "\n", "\t", "\r\n") - - SensitiveStrategy.values().forEach { strategy -> - testCases.forEach { testCase -> - val desensitizer = strategy.desensitizeSerializer() - val result = desensitizer(testCase) - - log.info("策略 {} 处理 '{}' -> '{}'", strategy.name, testCase.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r"), result) - assertNotNull(result, "策略 $strategy 处理边界情况应该返回非空结果") - } - } - - log.info("边界情况测试通过") - } -} diff --git a/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponseTest.kt b/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponseTest.kt deleted file mode 100644 index 8787ad343..000000000 --- a/shared/src/test/kotlin/io/github/truenine/composeserver/annotations/SensitiveResponseTest.kt +++ /dev/null @@ -1,175 +0,0 @@ -package io.github.truenine.composeserver.annotations - -import io.github.truenine.composeserver.testtoolkit.log -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * # 敏感响应注解测试 - * - * 测试 SensitiveResponse 注解的属性和行为 - */ -class SensitiveResponseTest { - - @SensitiveResponse - fun testMethodWithSensitiveResponse(): String { - return "sensitive data" - } - - fun testMethodWithoutSensitiveResponse(): String { - return "normal data" - } - - @Test - fun `测试 SensitiveResponse 注解的存在性`() { - log.info("测试 SensitiveResponse 注解的存在性") - - val method = this::class.java.getDeclaredMethod("testMethodWithSensitiveResponse") - val annotation = method.getAnnotation(SensitiveResponse::class.java) - - assertNotNull(annotation, "方法应该有 SensitiveResponse 注解") - log.info("找到 SensitiveResponse 注解: {}", annotation) - } - - @Test - fun `测试没有 SensitiveResponse 注解的方法`() { - log.info("测试没有 SensitiveResponse 注解的方法") - - val method = this::class.java.getDeclaredMethod("testMethodWithoutSensitiveResponse") - val annotation = method.getAnnotation(SensitiveResponse::class.java) - - assertEquals(null, annotation, "方法不应该有 SensitiveResponse 注解") - log.info("确认方法没有 SensitiveResponse 注解") - } - - @Test - fun `测试 SensitiveResponse 注解的元注解`() { - log.info("测试 SensitiveResponse 注解的元注解") - - val annotationClass = SensitiveResponse::class.java - val annotations = annotationClass.annotations - - log.info("SensitiveResponse 注解的元注解数量: {}", annotations.size) - - annotations.forEach { annotation -> log.info("元注解: {}", annotation.annotationClass.simpleName) } - - // 验证包含预期的元注解 - val annotationNames = annotations.map { it.annotationClass.simpleName }.toSet() - assertTrue(annotationNames.contains("Inherited"), "应该包含 @Inherited 注解") - assertTrue(annotationNames.contains("Retention"), "应该包含 @Retention 注解") - assertTrue(annotationNames.contains("MustBeDocumented"), "应该包含 @MustBeDocumented 注解") - assertTrue(annotationNames.contains("Target"), "应该包含 @Target 注解") - } - - @Test - fun `测试 SensitiveResponse 注解的 Target 设置`() { - log.info("测试 SensitiveResponse 注解的 Target 设置") - - val annotationClass = SensitiveResponse::class.java - val targetAnnotation = annotationClass.getAnnotation(Target::class.java) - - assertNotNull(targetAnnotation, "应该有 @Target 注解") - - val allowedTargets = targetAnnotation.allowedTargets - log.info("允许的目标数量: {}", allowedTargets.size) - - allowedTargets.forEach { target -> log.info("允许的目标: {}", target.name) } - - assertTrue(allowedTargets.contains(AnnotationTarget.FUNCTION), "应该允许用于函数") - } - - @Test - fun `测试 SensitiveResponse 注解的 Retention 设置`() { - log.info("测试 SensitiveResponse 注解的 Retention 设置") - - val annotationClass = SensitiveResponse::class.java - val retentionAnnotation = annotationClass.getAnnotation(Retention::class.java) - - assertNotNull(retentionAnnotation, "应该有 @Retention 注解") - - val retentionPolicy = retentionAnnotation.value - log.info("保留策略: {}", retentionPolicy.name) - - // 通常应该是 RUNTIME,以便在运行时可以访问 - assertEquals(AnnotationRetention.RUNTIME, retentionPolicy, "应该使用 RUNTIME 保留策略") - } - - @Test - fun `测试 SensitiveResponse 注解的继承性`() { - log.info("测试 SensitiveResponse 注解的继承性") - - val annotationClass = SensitiveResponse::class.java - val inheritedAnnotation = annotationClass.getAnnotation(java.lang.annotation.Inherited::class.java) - - assertNotNull(inheritedAnnotation, "应该有 @Inherited 注解") - log.info("确认 SensitiveResponse 注解支持继承") - } - - @Test - fun `测试 SensitiveResponse 注解的文档化`() { - log.info("测试 SensitiveResponse 注解的文档化") - - val annotationClass = SensitiveResponse::class.java - val documentedAnnotation = annotationClass.getAnnotation(MustBeDocumented::class.java) - - assertNotNull(documentedAnnotation, "应该有 @MustBeDocumented 注解") - log.info("确认 SensitiveResponse 注解会被文档化") - } - - @Test - fun `测试 SensitiveResponse 注解在反射中的可见性`() { - log.info("测试 SensitiveResponse 注解在反射中的可见性") - - val method = this::class.java.getDeclaredMethod("testMethodWithSensitiveResponse") - val annotations = method.annotations - - log.info("方法上的注解数量: {}", annotations.size) - - val sensitiveResponseAnnotations = annotations.filterIsInstance() - assertEquals(1, sensitiveResponseAnnotations.size, "应该找到一个 SensitiveResponse 注解") - - log.info("在反射中成功找到 SensitiveResponse 注解") - } - - @Test - fun `测试 SensitiveResponse 注解的类型信息`() { - log.info("测试 SensitiveResponse 注解的类型信息") - - val annotationClass = SensitiveResponse::class.java - - assertTrue(annotationClass.isAnnotation, "SensitiveResponse 应该是注解类型") - assertEquals("SensitiveResponse", annotationClass.simpleName, "注解名称应该正确") - assertEquals("io.github.truenine.composeserver.annotations", annotationClass.packageName, "包名应该正确") - - log.info("注解类型信息验证通过") - } - - // 测试继承场景 - open class BaseClass { - @SensitiveResponse open fun sensitiveMethod(): String = "base sensitive" - } - - class DerivedClass : BaseClass() { - override fun sensitiveMethod(): String = "derived sensitive" - } - - @Test - fun `测试 SensitiveResponse 注解的继承行为`() { - log.info("测试 SensitiveResponse 注解的继承行为") - - val baseMethod = BaseClass::class.java.getDeclaredMethod("sensitiveMethod") - val derivedMethod = DerivedClass::class.java.getDeclaredMethod("sensitiveMethod") - - val baseAnnotation = baseMethod.getAnnotation(SensitiveResponse::class.java) - val derivedAnnotation = derivedMethod.getAnnotation(SensitiveResponse::class.java) - - assertNotNull(baseAnnotation, "基类方法应该有 SensitiveResponse 注解") - - // 注意:Java 的 @Inherited 只对类级别的注解有效,对方法级别的注解无效 - // 所以派生类的重写方法不会自动继承注解 - log.info("基类方法注解: {}", baseAnnotation) - log.info("派生类方法注解: {}", derivedAnnotation) - } -} From f0e4ec8f4e6059a9ff80abb2ca7a2eb3ed7888fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 03:30:34 +0800 Subject: [PATCH 09/32] =?UTF-8?q?=F0=9F=93=9D=20[docs]=20=E9=87=8D?= =?UTF-8?q?=E5=86=99=20CLAUDE.md=20=E4=BB=A5=E7=AE=80=E5=8C=96=E5=92=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ 更新框架概述与技术栈描述 - 💄 优化模块结构和导航说明 - 💄 精简构建命令与开发标准部分 - 🔐 重新组织提交规范及表情符号系统 - 🔥 删除冗余内容以提升文档清晰度和可读性 --- CLAUDE.md | 540 +++++++++++------------------------------------------- 1 file changed, 106 insertions(+), 434 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 69fc4c617..77e13891c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,441 +1,113 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## 框架定位 - -Compose Server 是一个现代化、模块化的 Kotlin 企业级服务端开发**框架**,而非脚手架。它通过 Gradle 多模块方式,提供安全、数据库、缓存、对象存储、支付、AI 等企业级能力,支持按需集成到任意 -Spring Boot 项目中。 - -## 构建和测试命令 - -这是一个基于 Gradle 的 Kotlin 多模块项目。 - -- `./gradlew build` - 构建整个项目 -- `./gradlew clean` - 清理构建输出 -- `./gradlew publishToMavenLocal` - 发布到本地 Maven 仓库 -- `./gradlew versionCatalogUpdate` - 更新版本目录中的依赖版本 +# CLAUDE.md - AI 助手系统提示 + +## 角色与身份 +你是一个通过上下文引擎访问 Compose Server 代码库的智能编程助手。 + +**框架概述:** Compose Server 是现代化、模块化的 Kotlin 企业级开发框架(非脚手架),通过 Gradle 多模块提供企业级能力。所有模块可独立集成到任意 Spring Boot 项目中。 + +**技术栈:** Kotlin 2.2.x, Spring Boot 3.5.x, Jimmer 0.9.x, Gradle 9.x, PostgreSQL, Redis, Caffeine, MinIO, 云服务。 + +## Token 效率指南 +- **优先简洁直接的回复** - 避免冗长解释 +- **批量相关工具调用** - 将多个信息请求合并为单次调用 +- **限制代码片段** 为关键行(`` 标签内最多 10 行) +- **使用高效工具序列** - 最小化冗余调用 + +## 模块结构与导航 +**包格式:** `io.github.truenine.composeserver.{模块名}` + +**核心模块:** +- `shared/` - 核心组件、工具类、异常处理、统一响应、分页 +- `meta/` - 元数据/注解处理 +- `rds/` - 数据库(Jimmer ORM、CRUD、PostgreSQL、Flyway) +- `surveillance/` - 监控 +- `security/` - Spring Security、OAuth2、加密 +- `oss/` - 对象存储(MinIO、云服务商) +- `pay/` - 支付(微信支付 V3) +- `cacheable/` - 多级缓存(Redis、Caffeine) + +**扩展模块:** +- `data/` - 数据处理(Excel、爬虫、行政区划) +- `depend/` - 依赖处理 +- `testtoolkit/` - 测试工具 +- `gradle-plugin/` - Gradle 插件 +- `ksp/` - Kotlin 符号处理 +- `sms/` - 短信服务 +- `mcp/` - AI 能力(LangChain4j、Ollama) + +**常用路径:** +- 构建文件:`{模块}/build.gradle.kts` +- 源码:`{模块}/src/main/kotlin/io/github/truenine/composeserver/{模块}/` +- 测试:`{模块}/src/test/kotlin/` +- 资源:`{模块}/src/main/resources/` + +## 构建命令 +- `./gradlew build` - 构建项目 +- `./gradlew clean` - 清理输出 +- `./gradlew publishToMavenLocal` - 本地发布 - `./gradlew test` - 运行所有测试 -- `./gradlew :模块名:test` - 运行特定模块的测试 -- `./gradlew spotlessCheck` - 检查代码格式 -- `./gradlew spotlessApply` - 自动修复代码格式 - -## 项目架构 - -### 模块化结构 - -本框架采用多模块设计,主要模块包括: - -- **shared** - 核心基础组件,包含通用工具类、异常处理、类型定义、统一响应、分页等 -- **meta** - 元数据和注解处理器 -- **rds** - 数据库相关(Jimmer ORM、CRUD、PostgreSQL 扩展、Flyway 迁移) -- **surveillance** - 监控组件 -- **security** - 安全相关(Spring Security、OAuth2、加密解密) -- **oss** - 对象存储(MinIO、阿里云 OSS、华为云 OBS) -- **pay** - 支付模块(微信支付 V3) -- **cacheable** - 多级缓存(Redis、Caffeine) -- **data** - 数据处理(EasyExcel、爬虫、行政区划等) -- **depend** - 特定依赖处理 -- **testtoolkit** - 测试工具包 -- **gradle-plugin** - Gradle 插件 -- **ksp** - Kotlin Symbol Processing -- **sms** - 短信服务(腾讯云短信,短信抽象层) -- **mcp** - AI 能力(LangChain4j、Ollama、智谱 AI) - -> 所有模块均可独立集成,推荐组合见下表。 - -### 推荐模块组合 - -| 使用场景 | 推荐模块组合 | -|------------|---------------------------------| -| 基础 Web API | shared + security-spring | -| 数据库操作 | shared + rds-shared + rds-crud | -| 文件存储 | shared + oss-shared + oss-minio | -| 微信支付 | shared + pay | -| 数据导入导出 | shared + data-extract | -| AI 能力 | shared + mcp | - -## 技术栈 - -- **Kotlin** 2.2.x -- **Spring Boot** 3.5.x -- **Jimmer** 0.9.x -- **Gradle** 9.x -- **PostgreSQL**、**Redis**、**Caffeine**、**MinIO**、**阿里云 OSS**、**华为云 OBS** 等 - -## 依赖管理 - -- 统一使用 Gradle Version Catalog(`gradle/libs.versions.toml`)管理依赖版本 -- 所有模块版本、groupId 通过根项目统一管理 -- 推荐通过 `publishToMavenLocal` 集成本地开发版本 +- `./gradlew :{模块}:test` - 模块特定测试 +- `./gradlew spotlessApply` - 修复格式(提交前运行) +- `./gradlew versionCatalogUpdate` - 更新依赖 -## 代码约定 - -- 所有模块使用 `kotlinspring-convention` 插件,集成 Spring Boot 与 Kotlin 规范 -- 包名格式:`io.github.truenine.composeserver.模块名` - -- 代码格式化:使用 Spotless,提交前请运行 `./gradlew spotlessApply` -- 数据库迁移:使用 Flyway,脚本位于 `rds/flyway-migration-数据库类型/src/main/resources/db/migration/`,命名规则 `V版本号__描述.sql` - -## 集成与最佳实践 - -1. **依赖引入** - 在业务项目的 `build.gradle.kts` 中按需添加依赖,例如: - ```kotlin - implementation("io.github.truenine:composeserver-shared:latest") - implementation("io.github.truenine:composeserver-rds-shared:latest") - implementation("io.github.truenine:composeserver-security-spring:latest") - ``` -2. **自动配置** - 启用自动配置注解(如有): - ```kotlin - @SpringBootApplication - @EnableComposeServer - class YourApplication - ``` -3. **统一响应、异常、分页等** - 推荐使用框架内置的统一响应、异常处理、分页等能力,详见 `shared` 模块。 - -4. **测试与发布** - -- 修改代码后先格式化,再运行测试,最后构建或发布到本地 Maven 仓库 -- 推荐使用 `./gradlew test`、`./gradlew build`、`./gradlew publishToMavenLocal` - -## 其他说明 - -- 本项目为**框架库**,不包含脚手架或项目初始化功能 -- 所有模块均已发布至 Maven Central,详见 [README.md] 或 [Maven Central](https://central.sonatype.com/search?q=g:io.github.truenine) -- 详细 API、集成示例、变更日志等请参考 [README.md] 和官方文档 +## 开发标准 +- **依赖管理:** Gradle Version Catalog (`gradle/libs.versions.toml`) +- **插件约定:** 所有模块使用 `kotlinspring-convention` +- **代码格式:** Spotless(提交前必须) +- **测试命名:** 与被测试类同名,移除 `@DisplayName` 注解 +- **集成:** 在 build.gradle.kts 中添加 `implementation("io.github.truenine:composeserver-{模块}:latest")` ## Git 提交规范 -### 提交消息格式 - -本项目采用表情符号 + 英文 Conventional Commits 格式,支持详细功能列表: - -### 基础格式 - -``` -emoji [scope] description -``` - -### 详细列表格式(可选) - -``` -emoji [scope] main feature description - -- emoji specific change 1 -- emoji specific change 2 -- emoji specific change 3 -``` - -**格式说明:** - -- `emoji`:从下表中选择合适的表情符号 -- `[scope]`:模块或功能范围,用方括号包围 -- `description`:简洁的功能描述(支持中英文) -- **详细列表**:当改动较多时,可添加表情符号格式的具体变更列表 - -**使用原则:** - -- 🎯 **简单改动**:仅使用基础格式 -- 📋 **复杂改动**:使用详细列表格式,每项变更前加对应表情符号 -- 🌐 **语言选择**:描述支持中英文,根据团队习惯选择 - -**基础格式示例:** - -``` -✨ [shared] add unified exception handling -🐛 [rds] fix connection pool issue -♻️ [security] refactor JWT validation -``` - -**详细列表格式示例:** - -``` -✨ [shared] 日志功能 - -- 🚑 紧急修复日志级别 -- 🐛 依赖倒转缺失问题 -- 💄 日志框架进行 inline 处理 - -🔧 [buildlogic] 构建系统优化 - -- ⚡ 提升编译性能 -- 🔧 更新 Gradle 配置 -- 📦 优化依赖管理 -- 🚨 修复 Spotless 检查 - -♻️ [security] 安全模块重构 - -- 🏗️ 重构认证流程 -- 🔐 增强权限控制 -- 🛡️ 修复安全漏洞 -- 📈 添加安全监控 -``` - -### 表情符号参考表 - -| 表情符号 | 类型 | 描述 | 使用场景 | -|------|-----------|----------|----------------------| -| 🎉 | feat | 新功能/重大更新 | 添加新功能、重大特性、项目初始化 | -| ✨ | feat | 新功能/增强 | 添加新特性、功能增强、文档更新 | -| 🐛 | fix | Bug 修复 | 修复错误、解决问题 | -| 🔧 | config | 配置修改 | 修改配置文件、CI/CD 配置、构建配置 | -| 📝 | docs | 文档更新 | 更新文档、README、注释 | -| 🎨 | style | 代码风格/格式化 | 代码格式化、样式调整、结构优化 | -| ♻️ | refactor | 重构 | 代码重构、包结构调整、命名空间变更 | -| ⚡ | perf | 性能优化 | 提升性能、优化算法 | -| 🔥 | remove | 删除代码/文件 | 删除无用代码、移除功能、清理文件 | -| 🧪 | test | 测试相关 | 添加测试、修复测试、测试配置 | -| 👷 | ci | CI/CD 相关 | 持续集成、构建脚本、部署配置 | -| 📦 | build | 构建系统 | 依赖管理、构建配置、打包相关 | -| ⬆️ | upgrade | 升级依赖版本 | 升级依赖库版本、更新第三方库 | -| ⬇️ | downgrade | 降级依赖版本 | 降级依赖库版本、回退版本 | -| 🚀 | release | 发布版本 | 版本发布、标签创建 | -| 🔀 | merge | 合并分支 | 分支合并、冲突解决 | -| 🤖 | ai | AI 工具配置 | AI 助手配置、自动化工具 | -| 💄 | optimize | 优化/性能优化 | 性能优化、代码优化、算法改进 | -| 🌐 | network | 网络相关 | 网络配置、API 调用、远程服务 | -| 🔐 | security | 安全/防御编程 | 安全修复、权限控制、格式校验、防御编程 | -| 🚑 | hotfix | 紧急修复 | 紧急修复、补救措施、临时解决方案 | -| 📈 | analytics | 分析/监控 | 性能监控、数据分析、日志记录 | -| 🍱 | assets | 资源文件 | 图片、字体、静态资源 | -| 🚨 | lint | 代码检查 | 修复 linting 警告、代码质量 | -| 💡 | comment | 注释 | 添加或更新注释、文档字符串 | -| 🔊 | log | 日志 | 添加日志、调试信息 | -| 🔇 | log | 移除日志 | 删除日志、静默输出 | - -### 功能类型详细说明 - -#### 🎉 重大功能 (Major Features) - -- 🚀 项目初始化和重大里程碑 -- 📦 新模块或子系统的引入 -- 🏗️ 架构重大变更 - -#### ✨ 新功能 (Features) - -- 🔌 新增 API 接口 -- 🛠️ 新增工具类或组件 -- ⚡ 功能增强和改进 - -#### 🐛 Bug 修复 (Bug Fixes) - -- 🔨 修复功能错误 -- 🔄 解决兼容性问题 -- 🛡️ 修复安全漏洞 - -#### ♻️ 重构 (Refactoring) - -- 🏗️ 代码结构优化 -- 📁 包名或命名空间调整 -- 🎯 设计模式应用 - -#### 🔧 配置 (Configuration) - -- ⚙️ Gradle 构建配置 -- 🔄 CI/CD 流水线配置 -- 📋 环境配置文件 - -#### 📝 文档 (Documentation) - -- 📖 README 更新 -- 📚 API 文档 -- 💬 代码注释完善 - -#### 💄 优化 (Optimization) - -- ⚡ 性能优化 -- 🔧 代码优化 -- 📈 算法改进 - -#### 🌐 网络 (Network) - -- 🔌 API 接口调用 -- 🌍 远程服务集成 -- 📡 网络配置 - -#### 🔐 安全/防御编程 (Security & Defensive Programming) - -- 🛡️ 安全修复 -- 🔒 权限控制 -- ✅ 格式校验 -- 🛠️ 防御编程 - -#### 🚑 紧急修复 (Hotfix) - -- 🔥 紧急修复 -- 🩹 补救措施 -- ⚡ 临时解决方案 - -### Scope 范围说明 - -使用方括号 `[]` 包围 scope,从以下类别中选择合适的范围: - -#### 🏗️ 模块名称 (Module Names) - -``` -[shared] [rds] [security] [oss] -[pay] [cacheable] [data] [testtoolkit] -[surveillance][meta] [depend] [gradle-plugin] -[ksp] [sms] [mcp] -``` - -#### 🎯 功能领域 (Functional Areas) - -``` -[auth] [api] [db] [cache] -[config] [test] [build] [ci] -[docs] [core] [utils] [validation] -[logging] [monitoring] [migration] [integration] -``` - -#### 🔧 特定组件 (Specific Components) - -``` -[gradle] [buildlogic] [deps] [publish] -[release] [claude] [cursor] [spotless] -[flyway] [jimmer] [spring] [kotlin] -[docker] [k8s] [helm] [terraform] -``` - -#### 🌐 技术栈 (Technology Stack) - -``` -[postgresql] [redis] [minio] [oauth2] -[jwt] [wechat] [aliyun] [huawei] -[tencent] [ollama] [langchain] [ai] -``` - -#### 📁 文件类型 (File Types) - -``` -[readme] [changelog] [license] [gitignore] -[dockerfile] [compose] [makefile] [scripts] -``` - -### 提交规范要求 - -1. **表情符号使用**:每个提交必须以合适的表情符号开头,详细列表中每项也需要对应表情符号 -2. **消息长度**:标题行不超过 72 个字符,详细列表每行不超过 50 个字符 -3. **语言规范**:描述支持中英文,保持团队一致性 -4. **列表使用**: - -- 简单改动(1-2个变更):使用基础格式 -- 复杂改动(3个以上变更):推荐使用详细列表格式 -- 列表项按重要性或逻辑顺序排列 - -5. **原子性**:每个提交应该是一个逻辑上完整的变更单元 -6. **测试验证**:提交前确保代码通过测试和格式检查 - -### 提交示例 - -#### 基础格式示例 - -```bash -# 简单新功能 -git commit -m "✨ [shared] add unified exception handling" -git commit -m "🐛 [rds] fix connection pool issue" -git commit -m "♻️ [security] refactor JWT validation" -git commit -m "💄 [cache] optimize Redis performance" -git commit -m "🌐 [api] integrate third-party service" -git commit -m "🔐 [validation] add input format validation" -git commit -m "🚑 [auth] emergency fix for login issue" -git commit -m "📝 [docs] update API documentation" -``` - -#### 详细列表格式示例 - -```bash -# 复杂功能开发 -git commit -m "✨ [shared] 日志功能 - -- 🚑 紧急修复日志级别 -- 🐛 依赖倒转缺失问题 -- 💄 日志框架进行 inline 处理" - -# 性能优化 -git commit -m "💄 [cache] 缓存系统优化 - -- ⚡ 提升 Redis 连接性能 -- 🔧 优化缓存策略 -- 📈 改进缓存命中率 -- 🚨 修复内存泄漏问题" - -# 网络集成 -git commit -m "🌐 [api] 第三方服务集成 - -- 🔌 集成微信支付 API -- 📡 配置网络代理 -- 🛠️ 添加重试机制 -- 📊 网络监控埋点" - -# 安全防御编程 -git commit -m "🔐 [security] 安全防护增强 - -- 🛡️ 修复 SQL 注入漏洞 -- ✅ 添加输入格式校验 -- 🔒 增强权限控制 -- 🛠️ 防御性编程实践" - -# 多模块测试更新 -git commit -m "🧪 [testtoolkit] 测试框架升级 - -- 🔧 更新 JUnit 配置 -- 📦 集成 Testcontainers -- ⚡ 优化测试性能 -- 📊 添加覆盖率报告" - -# 版本发布和依赖管理 -git commit -m "🚀 [release] version 0.0.5" -git commit -m "⬆️ [deps] upgrade Spring Boot to 3.5.0" -git commit -m "⬇️ [deps] downgrade Kotlin for compatibility" - -# 清理和删除 -git commit -m "🔥 [buildlogic] remove unused imports" -git commit -m "🔥 [EmptyDefault] remove related definitions" - -# AI 工具配置 -git commit -m "🤖 [claude] add AI configuration" -git commit -m "🤖 [lingma] add Tongyi Lingma support" -``` - -#### 混合格式示例 - +**格式:** `emoji [scope] description`(简单)或详细列表格式(3+ 变更) + +**完整表情符号系统:** +| 表情符号 | 类型 | 描述 | 使用场景 | +|---------|------|------|----------| +| 🎉 | feat | 重大功能/初始化 | 新功能、重大更新、项目初始化 | +| ✨ | feat | 新功能/增强 | 添加功能、增强、文档更新 | +| 🐛 | fix | Bug 修复 | 修复错误、解决问题 | +| 🔧 | config | 配置修改 | 配置文件、CI/CD、构建配置 | +| 📝 | docs | 文档更新 | 更新文档、README、注释 | +| 🎨 | style | 代码风格/格式化 | 代码格式化、样式、结构优化 | +| ♻️ | refactor | 重构 | 代码重构、包结构调整 | +| ⚡ | perf | 性能优化 | 性能优化、算法改进 | +| 🔥 | remove | 删除代码/文件 | 删除无用代码、移除功能 | +| 🧪 | test | 测试相关 | 添加测试、修复测试、测试配置 | +| 👷 | ci | CI/CD | 持续集成、构建脚本 | +| 📦 | build | 构建系统 | 依赖管理、构建配置 | +| ⬆️ | upgrade | 升级依赖 | 升级库版本 | +| ⬇️ | downgrade | 降级依赖 | 降级库版本 | +| 🚀 | release | 发布版本 | 版本发布、标签创建 | +| 🔀 | merge | 合并分支 | 分支合并、冲突解决 | +| 🤖 | ai | AI 工具配置 | AI 助手配置、自动化 | +| 💄 | optimize | 优化 | 性能优化、代码改进 | +| 🌐 | network | 网络相关 | 网络配置、API 调用、远程服务 | +| 🔐 | security | 安全/验证 | 安全修复、权限控制、验证 | +| 🚑 | hotfix | 紧急修复 | 紧急修复、临时解决方案 | +| 📈 | analytics | 分析/监控 | 性能监控、数据分析 | +| 🍱 | assets | 资源文件 | 图片、字体、静态资源 | +| 🚨 | lint | 代码检查 | 修复 linting 警告、代码质量 | +| 💡 | comment | 注释 | 添加/更新注释、文档字符串 | +| 🔊 | log | 日志 | 添加日志、调试信息 | +| 🔇 | log | 移除日志 | 删除日志、静默输出 | + +**作用域:** `[shared]` `[rds]` `[security]` `[oss]` `[pay]` `[cacheable]` `[data]` `[surveillance]` `[meta]` `[depend]` `[testtoolkit]` `[gradle-plugin]` `[ksp]` `[sms]` `[mcp]` `[auth]` `[api]` `[db]` `[cache]` `[config]` `[test]` `[build]` `[ci]` `[docs]` `[core]` `[utils]` `[validation]` `[gradle]` `[buildlogic]` `[deps]` `[spotless]` `[flyway]` `[jimmer]` `[spring]` `[kotlin]` + +**规则:** +- 简单变更(1-2个):基础格式 +- 复杂变更(3+个):详细列表格式 +- 原子性提交,提交前测试 +- 提交前运行 `./gradlew spotlessApply` + +**示例:** ```bash -# 大版本发布(包含多项重要变更) -git commit -m "🚀 [release] version 1.0.0 - -- ✨ 新增统一异常处理 -- 🔐 增强安全防护机制 -- 💄 优化数据库连接池性能 -- 🌐 集成第三方支付服务 -- 📝 完善 API 文档 -- 🧪 添加集成测试套件" - -# 紧急修复(包含多个相关问题) -git commit -m "🚑 [security] 紧急安全修复 - -- 🛡️ 修复 JWT 验证漏洞 -- 🔐 增强输入格式校验 -- ✅ 添加防御性编程检查 -- 📈 添加安全审计日志 -- 🚨 更新安全检查规则" - -# 性能优化专项(多个优化措施) -git commit -m "💄 [performance] 系统性能优化 - -- ⚡ 优化数据库查询性能 -- 🔧 改进缓存算法 -- 📈 提升 API 响应速度 -- 🛠️ 优化内存使用" - -# 网络功能增强 -git commit -m "🌐 [network] 网络功能增强 - -- 🔌 集成新的 API 服务 -- 📡 优化网络连接配置 -- 🛠️ 添加网络异常处理 -- 📊 网络性能监控" +✨ [shared] 添加统一异常处理 +🐛 [rds] 修复连接池问题 +♻️ [security] 重构 JWT 验证 + +✨ [shared] 日志系统 +- 🚑 修复日志级别紧急问题 +- 🐛 解决依赖倒转问题 +- 💄 优化日志框架内联处理 ``` From c04715f710be59ea008200b79f97daac9da35ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 03:49:18 +0800 Subject: [PATCH 10/32] =?UTF-8?q?=F0=9F=93=9D=20[docs]=20=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E4=B8=8E=E9=87=8D=E6=9E=84=20GLOBAL=5FCLAUDE.md=20?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🔥 删除冗余内容,提升文档清晰度 - 💄 更新规范与操作步骤,优化技术指引 - ♻️ 重新组织章节结构以增强可读性 --- docs/GLOBAL_CLAUDE.md | 100 +++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 65 deletions(-) diff --git a/docs/GLOBAL_CLAUDE.md b/docs/GLOBAL_CLAUDE.md index a6becf34f..b50226bda 100644 --- a/docs/GLOBAL_CLAUDE.md +++ b/docs/GLOBAL_CLAUDE.md @@ -1,77 +1,47 @@ -# claude code 核心工作规则 +# GLOBAL_CLAUDE.md -## 沟通效率优化 -- 批量处理相关任务以减少交互轮次 -- 将多个相关修改合并为单次操作 +## 个性化开发约定 -# 通用代码标准 +### 执行优先级 +1. 安全检查:扫描敏感信息暴露 +2. 代码质量:函数参数≤5个,使用数据类封装 +3. 格式要求:所有代码块必须使用大括号 -## 编程原则 -- 函数参数限制在5个以内 -- 禁止硬编码常量,使用配置或常量定义 -- 避免静默失败 +### 注释规范 +- API文档:英文注释 +- 代码注释:解释"为什么"而非"做什么" -## 代码风格一致性 -- 强制使用大括号包围代码块,即使只有一行代码(防止类似苹果 goto fail 的安全漏洞) +### 强制要求 +- 严禁代码中暴露API密钥、密码、token -## 注释标准 -- API 文档注释使用英文 -- 注释解释"为什么"而非"是什么" +## 特定语言约定 -## 安全编程指南 -- 严格禁止在代码中暴露敏感信息(密钥、密码等) -- 使用参数化查询防止注入攻击 +### SQL操作步骤 +1. 检查现有查询是否使用参数化 +2. 统一使用snake_case命名 +3. 验证无字符串拼接风险 -# 语言特定标准 +### Kotlin开发步骤 +1. 优先使用val声明不可变变量 +2. 避免!!操作符,使用?.或let{} +3. 数据类替代多参数函数 -## SQL -- 必须使用参数化查询,严格禁止字符串拼接 -- 使用 snake_case 命名约定 +### 前端代码步骤 +- TypeScript: 启用strict模式,避免any类型 +- Vue: 使用Composition API + -----\nSGVsbG8=\n-----END -----" + + assertThrows { PemFormat.parse(maliciousInput) } + } + + @Test + fun boundsCheckingPreventsBufferOverflow() { + val veryLongKeyType = "A".repeat(10000) + + assertThrows { PemFormat[SIMPLE_BASE64, veryLongKeyType] } + } } - @ParameterizedTest(name = "测试换行符「{0}」") - @ValueSource(strings = ["\n", "\r", "\r\n"]) - fun `测试不同换行符处理`(lineEnding: String) { - val pemWithDifferentLineEndings = - """ - -----BEGIN TEST-----${lineEnding} - SGVsbG8gV29ybGQ=${lineEnding} - -----END TEST----- + @Nested + inner class EdgeCaseTests { + + @Test + fun handlesMinimalValidPem() { + val minimalPem = "-----BEGIN A-----\nB\n-----END A-----" + val pemFormat = PemFormat.parse(minimalPem) + + assertEquals("A", pemFormat.schema) + assertEquals("B", pemFormat.content) + } + + @Test + fun handlesEmptyKeyTypeCorrectly() { + val pemWithEmptyType = "-----BEGIN -----\nSGVsbG8=\n-----END -----" + val pemFormat = PemFormat.parse(pemWithEmptyType) + + assertEquals("", pemFormat.schema) + assertEquals("SGVsbG8=", pemFormat.content) + } + + @Test + fun handlesMultipleConsecutiveLineBreaks() { + val pemWithExtraBreaks = """ - .trimIndent() + -----BEGIN TEST----- - val pemFormat = PemFormat.parse(pemWithDifferentLineEndings) - assertEquals("TEST", pemFormat.schema) - assertEquals("SGVsbG8gV29ybGQ=", pemFormat.content) + + SGVsbG8gV29ybGQ= + + + -----END TEST----- + """ + .trimIndent() + + val pemFormat = PemFormat.parse(pemWithExtraBreaks) + assertEquals("TEST", pemFormat.schema) + assertEquals("SGVsbG8gV29ybGQ=", pemFormat.content) + } } } diff --git a/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringFnsTest.kt b/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringExtensionsTest.kt similarity index 99% rename from security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringFnsTest.kt rename to security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringExtensionsTest.kt index 1b039ab54..604d23154 100644 --- a/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringFnsTest.kt +++ b/security/crypto/src/test/kotlin/io/github/truenine/composeserver/security/crypto/StringExtensionsTest.kt @@ -6,7 +6,7 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.Test /** Test cases for String extension functions */ -class StringFnsTest { +class StringExtensionsTest { @Test fun `test uuid generation`() { diff --git a/security/oauth2/src/main/kotlin/io/github/truenine/composeserver/security/oauth2/property/WxpaProperty.kt b/security/oauth2/src/main/kotlin/io/github/truenine/composeserver/security/oauth2/property/WxpaProperty.kt index 5e8a12825..6f675b1cb 100644 --- a/security/oauth2/src/main/kotlin/io/github/truenine/composeserver/security/oauth2/property/WxpaProperty.kt +++ b/security/oauth2/src/main/kotlin/io/github/truenine/composeserver/security/oauth2/property/WxpaProperty.kt @@ -2,7 +2,7 @@ package io.github.truenine.composeserver.security.oauth2.property import io.github.truenine.composeserver.datetime import io.github.truenine.composeserver.iso8601LongUtc -import io.github.truenine.composeserver.security.crypto.Keys +import io.github.truenine.composeserver.security.crypto.CryptographicKeyManager import io.github.truenine.composeserver.security.crypto.sha1 /** @@ -29,7 +29,11 @@ class WxpaProperty { var jsapiTicket: String? = null @JvmOverloads - fun signature(url: String, nonceString: String = Keys.generateRandomAsciiString(), timestamp: Long = datetime.now().iso8601LongUtc): WxpaSignatureResp { + fun signature( + url: String, + nonceString: String = CryptographicKeyManager.generateRandomAsciiString(), + timestamp: Long = datetime.now().iso8601LongUtc, + ): WxpaSignatureResp { val splitUrl = url.split("#")[0] val b = mutableMapOf("noncestr" to nonceString, "jsapi_ticket" to jsapiTicket, "timestamp" to timestamp.toString(), "url" to splitUrl) diff --git a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuer.kt b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuer.kt index 7a4fcf387..467291b13 100644 --- a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuer.kt +++ b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuer.kt @@ -4,7 +4,7 @@ import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import com.fasterxml.jackson.databind.ObjectMapper import io.github.truenine.composeserver.DateTimeConverter -import io.github.truenine.composeserver.security.crypto.Encryptors +import io.github.truenine.composeserver.security.crypto.CryptographicOperations import io.github.truenine.composeserver.security.jwt.consts.IssuerParam import io.github.truenine.composeserver.slf4j import java.security.PrivateKey @@ -43,7 +43,7 @@ class JwtIssuer private constructor() : JwtVerifier() { internal fun createContent(content: Any): String = runCatching { objectMapper.writeValueAsString(content) }.onFailure { log.warn("jwt json 签发异常,或许没有配置序列化器", it) }.getOrElse { "{}" } - internal fun encryptData(encData: String, eccPublicKey: PublicKey): String? = Encryptors.encryptByEccPublicKey(eccPublicKey, encData) + internal fun encryptData(encData: String, eccPublicKey: PublicKey): String? = CryptographicOperations.encryptByEccPublicKey(eccPublicKey, encData) inner class Builder { fun build(): JwtIssuer = this@JwtIssuer diff --git a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtVerifier.kt b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtVerifier.kt index b8f1e27ac..2f642d915 100644 --- a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtVerifier.kt +++ b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/JwtVerifier.kt @@ -5,7 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm import com.fasterxml.jackson.databind.ObjectMapper import io.github.truenine.composeserver.DateTimeConverter import io.github.truenine.composeserver.security.JwtException -import io.github.truenine.composeserver.security.crypto.Encryptors +import io.github.truenine.composeserver.security.crypto.CryptographicOperations import io.github.truenine.composeserver.security.jwt.consts.JwtToken import io.github.truenine.composeserver.security.jwt.consts.VerifierParam import io.github.truenine.composeserver.slf4j @@ -75,7 +75,7 @@ open class JwtVerifier internal constructor() { } private fun decryptData(encData: String, targetType: KClass, eccPrivateKey: PrivateKey? = this.contentEccPrivateKey): T? { - val content = eccPrivateKey.let { priKey -> Encryptors.decryptByEccPrivateKey(priKey!!, encData) } ?: encData + val content = eccPrivateKey.let { priKey -> CryptographicOperations.decryptByEccPrivateKey(priKey!!, encData) } ?: encData return parseContent(content, targetType) } diff --git a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/IssuerParam.kt b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/IssuerParam.kt index 1e76d4f29..0b61b3dcb 100644 --- a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/IssuerParam.kt +++ b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/IssuerParam.kt @@ -1,6 +1,6 @@ package io.github.truenine.composeserver.security.jwt.consts -import io.github.truenine.composeserver.security.crypto.Keys +import io.github.truenine.composeserver.security.crypto.CryptographicKeyManager import java.security.PublicKey import java.security.interfaces.RSAPrivateKey import java.time.Duration @@ -19,10 +19,10 @@ data class IssuerParam( fun containEncryptContent(): Boolean = null != this.encryptedDataObj fun contentEncryptEccKeyFromBase64(base64Key: String) { - this.contentEncryptEccKey = Keys.readRsaPublicKeyByBase64(base64Key) + this.contentEncryptEccKey = CryptographicKeyManager.readRsaPublicKeyByBase64(base64Key) } fun signatureKeyFromBase64(base64Key: String) { - this.signatureKey = Keys.readRsaPrivateKeyByBase64(base64Key)!! + this.signatureKey = CryptographicKeyManager.readRsaPrivateKeyByBase64(base64Key)!! } } diff --git a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/VerifierParam.kt b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/VerifierParam.kt index 908ac3415..7ecf02545 100644 --- a/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/VerifierParam.kt +++ b/security/spring/src/main/kotlin/io/github/truenine/composeserver/security/jwt/consts/VerifierParam.kt @@ -1,6 +1,6 @@ package io.github.truenine.composeserver.security.jwt.consts -import io.github.truenine.composeserver.security.crypto.Keys +import io.github.truenine.composeserver.security.crypto.CryptographicKeyManager import java.security.PrivateKey import java.security.interfaces.RSAPublicKey @@ -16,10 +16,10 @@ data class VerifierParam( fun isRequireDecrypted(): Boolean = this.encryptDataTargetType != null fun contentEncryptEccKeyFromBase64(base64Key: String) { - this.contentEncryptEccKey = Keys.readEccPrivateKeyByBase64(base64Key) + this.contentEncryptEccKey = CryptographicKeyManager.readEccPrivateKeyByBase64(base64Key) } fun signatureKeyFromBase64(base64Key: String) { - this.signatureKey = Keys.readRsaPublicKeyByBase64(base64Key)!! + this.signatureKey = CryptographicKeyManager.readRsaPublicKeyByBase64(base64Key)!! } } diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/FileKeyRepoTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/FileKeyRepoTest.kt deleted file mode 100644 index 763ea0831..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/FileKeyRepoTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.github.truenine.composeserver.security - -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.security.crypto.FileKeyRepo -import io.github.truenine.composeserver.security.crypto.Keys -import io.github.truenine.composeserver.security.crypto.PemFormat -import io.github.truenine.composeserver.security.jwt.JwtIssuer -import io.github.truenine.composeserver.security.jwt.JwtVerifier -import io.github.truenine.composeserver.security.jwt.consts.IssuerParam -import io.github.truenine.composeserver.security.jwt.consts.VerifierParam -import java.time.Duration -import java.time.temporal.ChronoUnit -import kotlin.test.assertNotNull -import org.junit.jupiter.api.Test - -class FileKeyRepoTest { - - @Test - fun findKeyByName() { - val e = Keys.generateEccKeyPair() - - println(Keys.writeKeyToPem(e!!.privateKey)) - println("=========") - println(Keys.writeKeyToPem(e.publicKey)) - - val f = FileKeyRepo() - val b = f.findEccPrivateKeyByName("acv.pem") - val c = f.findEccPublicKeyByName("acc.pem") - - val eccPair = f.findEccKeyPairByName("acc.pem", "acv.pem") - - println(b) - println(c) - println(eccPair) - } - - @Test - fun findRsa() { - val r = Keys.generateRsaKeyPair()!! - println(PemFormat[r.publicKey]) - println(PemFormat[r.privateKey]) - - val f = FileKeyRepo() - val k = f.findRsaKeyPairByName("rcc.pem", "rcv.pem")!! - println(k.publicKey) - println(k.privateKey) - } - - @Test - fun testReadJwt() { - val f = FileKeyRepo() - val e = f.jwtEncryptDataIssuerEccKeyPair() - val s = f.jwtSignatureIssuerRsaKeyPair() - assertNotNull(e) - assertNotNull(e.publicKey) - assertNotNull(e.privateKey) - assertNotNull(s) - assertNotNull(s.publicKey) - assertNotNull(s.privateKey) - - val iss = - JwtIssuer.createIssuer() - .serializer(ObjectMapper()) - .expireFromDuration(Duration.of(100, ChronoUnit.SECONDS)) - .signatureIssuerKey(s.privateKey) - .signatureVerifyKey(s.publicKey) - .contentEncryptKey(e.publicKey) - .contentDecryptKey(e.privateKey) - .build() - - val ver = JwtVerifier.createVerifier().serializer(ObjectMapper()).contentDecryptKey(e.privateKey).signatureVerifyKey(s.publicKey).build() - - val issToken = - iss.issued( - IssuerParam().apply { - encryptedDataObj = "1" to "2" - subjectObj = "3" to "4" - } - ) - val res = ver.verify(VerifierParam(issToken, subjectTargetType = Any::class.java, encryptDataTargetType = Any::class.java)) - println(res?.subject) - println(res?.decryptedData) - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/BootLaunchTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/BootLaunchTest.kt deleted file mode 100644 index 0e3b378e2..000000000 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/BootLaunchTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.truenine.composeserver.security.jwt - -import io.github.truenine.composeserver.testtoolkit.info -import io.github.truenine.composeserver.testtoolkit.log -import jakarta.annotation.Resource -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.ApplicationContext - -@SpringBootTest -class BootLaunchTest { - lateinit var ctx: ApplicationContext - @Resource set - - @Test - fun `test launch`() { - log.info(::ctx) - } -} diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuerVerifierTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuerVerifierTest.kt index 7e7cf6b4a..ec124cfff 100644 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuerVerifierTest.kt +++ b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtIssuerVerifierTest.kt @@ -1,140 +1,531 @@ package io.github.truenine.composeserver.security.jwt import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.security.crypto.Keys +import io.github.truenine.composeserver.security.crypto.CryptographicKeyManager +import io.github.truenine.composeserver.security.crypto.domain.IEccExtKeyPair +import io.github.truenine.composeserver.security.crypto.domain.IRsaExtKeyPair import io.github.truenine.composeserver.security.jwt.consts.IssuerParam import io.github.truenine.composeserver.security.jwt.consts.VerifierParam import java.time.Duration import java.time.temporal.ChronoUnit -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* +import kotlin.time.measureTime import org.junit.jupiter.api.Test -/** Test JWT issuer and verifier functionality */ +/** + * Comprehensive test suite for JWT issuer and verifier functionality. + * + * This test class validates JWT token generation, verification, encryption/decryption, error handling, and performance characteristics. Tests follow TDD + * principles with independent, atomic tests that provide complete coverage of JWT operations. + * + * @author TrueNine + * @since 2024-07-14 + */ class JwtIssuerVerifierTest { + companion object { + private const val TEST_ISSUER = "test-issuer" + private const val TEST_ID = "test-id" + private const val SIMPLE_TEST_ISSUER = "simple-test" + private const val SIMPLE_TEST_ID = "simple-id" + private const val TEST_EXPIRE_SECONDS = 100L + private const val LARGE_DATA_SIZE = 10000 + private const val CONCURRENT_THREADS = 10 + private const val CONCURRENT_ITERATIONS = 100 + private const val PERFORMANCE_THRESHOLD_MS = 5000L + } + private val objectMapper = ObjectMapper() - @Test - fun `test JWT issuer and verifier integration`() { - val eccPair = Keys.generateEccKeyPair()!! - val rsaPair = Keys.generateRsaKeyPair()!! + /** Test data container for JWT testing scenarios. */ + data class TestKeyPairs(val rsaKeyPair: IRsaExtKeyPair, val eccKeyPair: IEccExtKeyPair) - val issuer = - JwtIssuer.createIssuer() - .issuer("test-issuer") - .id("test-id") - .signatureIssuerKey(rsaPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) - .contentEncryptKey(eccPair.publicKey) - .serializer(objectMapper) - .expireFromDuration(Duration.of(100, ChronoUnit.SECONDS)) - .build() + /** Test data container for various data types. */ + data class TestTokenData( + val simpleString: String = "simple string data", + val complexMap: Map = mapOf("key1" to "value1", "key2" to 123, "key3" to true), + val listData: List = listOf("user1", "admin", "moderator"), + val specialChars: String = "特殊字符测试!@#$%^&*()_+-=[]{}|;':\",./<>?", + val emptyString: String = "", + val nullValue: String? = null, + ) - val verifier = - JwtVerifier.createVerifier() - .issuer("test-issuer") - .id("test-id") - .contentDecryptKey(eccPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) - .serializer(objectMapper) - .build() + /** Creates test key pairs for JWT operations. */ + private fun createTestKeyPairs(): TestKeyPairs { + val rsaKeyPair = CryptographicKeyManager.generateRsaKeyPair() ?: fail("Failed to generate RSA key pair") + val eccKeyPair = CryptographicKeyManager.generateEccKeyPair(CryptographicKeyManager.generateRandomAsciiString()) ?: fail("Failed to generate ECC key pair") + return TestKeyPairs(rsaKeyPair, eccKeyPair) + } - val testData = mapOf("key1" to "value1", "key2" to 123) - val testSubject = listOf("user1", "admin") + /** Creates a JWT issuer with specified configuration. */ + private fun createJwtIssuer( + keyPairs: TestKeyPairs, + issuer: String = TEST_ISSUER, + id: String = TEST_ID, + expireSeconds: Long = TEST_EXPIRE_SECONDS, + ): JwtIssuer { + return JwtIssuer.createIssuer() + .issuer(issuer) + .id(id) + .signatureIssuerKey(keyPairs.rsaKeyPair.privateKey) + .signatureVerifyKey(keyPairs.rsaKeyPair.publicKey) + .contentEncryptKey(keyPairs.eccKeyPair.publicKey) + .serializer(objectMapper) + .expireFromDuration(Duration.of(expireSeconds, ChronoUnit.SECONDS)) + .build() + } - val issuerParam = IssuerParam(signatureKey = rsaPair.privateKey) - issuerParam.encryptedDataObj = testData - issuerParam.subjectObj = testSubject + /** Creates a JWT verifier with specified configuration. */ + private fun createJwtVerifier(keyPairs: TestKeyPairs, issuer: String = TEST_ISSUER, id: String = TEST_ID): JwtVerifier { + return JwtVerifier.createVerifier() + .issuer(issuer) + .id(id) + .contentDecryptKey(keyPairs.eccKeyPair.privateKey) + .signatureVerifyKey(keyPairs.rsaKeyPair.publicKey) + .serializer(objectMapper) + .build() + } + + /** Validates that a token string is properly formatted. */ + private fun assertTokenValid(token: String) { + assertNotNull(token, "Token should not be null") + assertTrue(token.isNotEmpty(), "Token should not be empty") + assertTrue(token.contains("."), "Token should contain JWT separators") + val parts = token.split(".") + assertEquals(3, parts.size, "JWT should have 3 parts (header.payload.signature)") + parts.forEach { part -> assertTrue(part.isNotEmpty(), "JWT part should not be empty") } + } + + @Test + fun testJwtIssuerAndVerifierIntegration() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + val testData = TestTokenData() + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData.complexMap + issuerParam.subjectObj = testData.listData val token = issuer.issued(issuerParam) - assertNotNull(token) - assertTrue(token.isNotEmpty()) + assertTokenValid(token) val verifierParam = VerifierParam(token = token, subjectTargetType = Any::class.java, encryptDataTargetType = Any::class.java) val result = verifier.verify(verifierParam) - assertNotNull(result) - assertNotNull(result.subject) - assertNotNull(result.decryptedData) + assertNotNull(result, "Verification result should not be null") + assertNotNull(result.subject, "Subject should not be null") + assertNotNull(result.decryptedData, "Decrypted data should not be null") + assertFalse(result.isExpired, "Token should not be expired") + assertEquals(TEST_ID, result.id, "Token ID should match") } @Test - fun `test JWT issuer builder pattern`() { - val eccPair = Keys.generateEccKeyPair()!! - val rsaPair = Keys.generateRsaKeyPair()!! + fun testJwtIssuerBuilderPattern() { + val keyPairs = createTestKeyPairs() val issuer = JwtIssuer.createIssuer() .serializer(objectMapper) .issuer("test") .id("1") - .signatureIssuerKey(rsaPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) - .contentEncryptKey(eccPair.publicKey) - .contentDecryptKey(eccPair.privateKey) + .signatureIssuerKey(keyPairs.rsaKeyPair.privateKey) + .signatureVerifyKey(keyPairs.rsaKeyPair.publicKey) + .contentEncryptKey(keyPairs.eccKeyPair.publicKey) + .contentDecryptKey(keyPairs.eccKeyPair.privateKey) .build() - assertNotNull(issuer) + assertNotNull(issuer, "JWT issuer should be created successfully") } @Test - fun `test JWT verifier builder pattern`() { - val eccPair = Keys.generateEccKeyPair()!! - val rsaPair = Keys.generateRsaKeyPair()!! + fun testJwtVerifierBuilderPattern() { + val keyPairs = createTestKeyPairs() val verifier = JwtVerifier.createVerifier() .serializer(objectMapper) .issuer("test") .id("1") - .contentDecryptKey(eccPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) + .contentDecryptKey(keyPairs.eccKeyPair.privateKey) + .signatureVerifyKey(keyPairs.rsaKeyPair.publicKey) .build() - assertNotNull(verifier) + assertNotNull(verifier, "JWT verifier should be created successfully") } @Test - fun `test JWT with simple data types`() { - val eccPair = Keys.generateEccKeyPair()!! - val rsaPair = Keys.generateRsaKeyPair()!! - - val issuer = - JwtIssuer.createIssuer() - .issuer("simple-test") - .id("simple-id") - .signatureIssuerKey(rsaPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) - .contentEncryptKey(eccPair.publicKey) - .serializer(objectMapper) - .build() - - val verifier = - JwtVerifier.createVerifier() - .issuer("simple-test") - .id("simple-id") - .contentDecryptKey(eccPair.privateKey) - .signatureVerifyKey(rsaPair.publicKey) - .serializer(objectMapper) - .build() + fun testJwtWithSimpleDataTypes() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs, SIMPLE_TEST_ISSUER, SIMPLE_TEST_ID) + val verifier = createJwtVerifier(keyPairs, SIMPLE_TEST_ISSUER, SIMPLE_TEST_ID) val simpleData = "simple string data" val simpleSubject = "user123" - val issuerParam = IssuerParam(signatureKey = rsaPair.privateKey) + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) issuerParam.encryptedDataObj = simpleData issuerParam.subjectObj = simpleSubject val token = issuer.issued(issuerParam) - assertNotNull(token) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification result should not be null") + assertEquals(simpleSubject, result.subject, "Subject should match") + assertEquals(simpleData, result.decryptedData, "Decrypted data should match") + assertFalse(result.isExpired, "Token should not be expired") + } + + @Test + fun testInvalidTokenVerification() { + val keyPairs = createTestKeyPairs() + val verifier = createJwtVerifier(keyPairs) + + val invalidTokens = + listOf("invalid.token.format", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature", "", "not.a.jwt", "too.many.parts.in.this.token") + + invalidTokens.forEach { invalidToken -> + val verifierParam = + VerifierParam(token = invalidToken, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNull(result, "Invalid token should return null result") + } + } + + @Test + fun testWrongKeyVerification() { + val keyPairs1 = createTestKeyPairs() + val keyPairs2 = createTestKeyPairs() + + val issuer = createJwtIssuer(keyPairs1) + val verifierWithWrongKey = createJwtVerifier(keyPairs2) // Different keys + + val issuerParam = IssuerParam(signatureKey = keyPairs1.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = "test data" + issuerParam.subjectObj = "test subject" + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifierWithWrongKey.verify(verifierParam) + // When using wrong keys, the verification should either return null or a token with null decryptedData + // due to decryption failure, but the signature verification will fail + if (result != null) { + // If result is not null, the encrypted data should be null due to decryption failure + assertNull(result.decryptedData, "Decrypted data should be null when using wrong decryption key") + } + // Either way is acceptable - null result or result with null decryptedData + assertTrue(result == null || result.decryptedData == null, "Token verified with wrong key should either return null or have null decrypted data") + } + + @Test + fun testNullParameterHandling() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + + // Test with null encrypted data + val issuerParamWithNullData = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParamWithNullData.encryptedDataObj = null + issuerParamWithNullData.subjectObj = "test subject" + + val tokenWithNullData = issuer.issued(issuerParamWithNullData as IssuerParam) + assertTokenValid(tokenWithNullData) + + // Test with null subject + val issuerParamWithNullSubject = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParamWithNullSubject.encryptedDataObj = "test data" + issuerParamWithNullSubject.subjectObj = null + + val tokenWithNullSubject = issuer.issued(issuerParamWithNullSubject as IssuerParam) + assertTokenValid(tokenWithNullSubject) + } + + @Test + fun testEmptyDataHandling() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = "" + issuerParam.subjectObj = "" + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification result should not be null") + assertEquals("", result.subject, "Empty subject should be preserved") + assertEquals("", result.decryptedData, "Empty data should be preserved") + } + + @Test + fun testSpecialCharacterHandling() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + val testData = TestTokenData() + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData.specialChars + issuerParam.subjectObj = testData.specialChars + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification result should not be null") + assertEquals(testData.specialChars, result.subject, "Special characters in subject should be preserved") + assertEquals(testData.specialChars, result.decryptedData, "Special characters in data should be preserved") + } + + @Test + fun testLargeDataHandling() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val largeData = "x".repeat(LARGE_DATA_SIZE) + val largeSubject = "subject_${"y".repeat(1000)}" + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = largeData + issuerParam.subjectObj = largeSubject + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification result should not be null") + assertEquals(largeSubject, result.subject, "Large subject should be preserved") + assertEquals(largeData, result.decryptedData, "Large data should be preserved") + } + + @Test + fun testTokenExpirationValidation() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs, expireSeconds = 1) // 1 second expiration + val verifier = createJwtVerifier(keyPairs) + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = "test data" + issuerParam.subjectObj = "test subject" + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + // Verify immediately (should work) + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val immediateResult = verifier.verify(verifierParam) + assertNotNull(immediateResult, "Immediate verification should succeed") + assertFalse(immediateResult.isExpired, "Token should not be expired immediately") + + // Wait for expiration and verify again + Thread.sleep(2000) // Wait 2 seconds + + val expiredResult = verifier.verify(verifierParam) + // The token should be marked as expired but still decoded + if (expiredResult != null) { + assertTrue(expiredResult.isExpired, "Token should be marked as expired") + } + } + + @Test + fun testEncryptionDecryptionConsistency() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val testCases = listOf("Simple string", mapOf("key" to "value", "number" to 42), listOf("item1", "item2", "item3"), 123456789, true, null) + + testCases.forEach { testData -> + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData + issuerParam.subjectObj = "test_subject_${testData?.hashCode()}" + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = Any::class.java, encryptDataTargetType = Any::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification result should not be null for data: $testData") + + if (testData != null) { + assertNotNull(result.decryptedData, "Decrypted data should not be null for: $testData") + } + } + } + + @Test + fun testPerformanceBenchmark() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val iterations = 100 + val testData = "performance test data" + val testSubject = "performance test subject" + + val issueTime = measureTime { + repeat(iterations) { + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData + issuerParam.subjectObj = testSubject + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + } + } + + val tokens = mutableListOf() + repeat(iterations) { + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData + issuerParam.subjectObj = testSubject + tokens.add(issuer.issued(issuerParam)) + } + + val verifyTime = measureTime { + tokens.forEach { token -> + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + val result = verifier.verify(verifierParam) + assertNotNull(result, "Verification should succeed") + } + } + + println("Performance benchmark results:") + println(" Token generation: $iterations tokens in ${issueTime.inWholeMilliseconds}ms") + println(" Token verification: $iterations tokens in ${verifyTime.inWholeMilliseconds}ms") + + assertTrue(issueTime.inWholeMilliseconds < PERFORMANCE_THRESHOLD_MS, "Token generation should complete within ${PERFORMANCE_THRESHOLD_MS}ms") + assertTrue(verifyTime.inWholeMilliseconds < PERFORMANCE_THRESHOLD_MS, "Token verification should complete within ${PERFORMANCE_THRESHOLD_MS}ms") + } + + @Test + fun testConcurrentTokenGeneration() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val tokens = mutableListOf() + val threads = mutableListOf() + + repeat(CONCURRENT_THREADS) { threadId -> + val thread = Thread { + repeat(CONCURRENT_ITERATIONS) { iteration -> + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = "data_${threadId}_$iteration" + issuerParam.subjectObj = "subject_${threadId}_$iteration" + + val token = issuer.issued(issuerParam) + synchronized(tokens) { tokens.add(token) } + } + } + threads.add(thread) + } + + val executionTime = measureTime { + threads.forEach { it.start() } + threads.forEach { it.join() } + } + + assertEquals(CONCURRENT_THREADS * CONCURRENT_ITERATIONS, tokens.size, "All tokens should be generated") + + // Verify all tokens are unique + val uniqueTokens = tokens.toSet() + assertEquals(tokens.size, uniqueTokens.size, "All generated tokens should be unique") + + // Verify all tokens are valid + tokens.forEach { token -> + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + + val result = verifier.verify(verifierParam) + assertNotNull(result, "All tokens should verify successfully") + } + + println("Concurrent generation: ${tokens.size} tokens in ${executionTime.inWholeMilliseconds}ms") + assertTrue(executionTime.inWholeMilliseconds < PERFORMANCE_THRESHOLD_MS * 2, "Concurrent generation should complete within reasonable time") + } + + @Test + fun testTokenDecodeWithoutVerification() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + val testData = "decode test data" + val testSubject = "decode test subject" + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = testData + issuerParam.subjectObj = testSubject + + val token = issuer.issued(issuerParam) + assertTokenValid(token) val verifierParam = VerifierParam(token = token, subjectTargetType = String::class.java, encryptDataTargetType = String::class.java) + // Test decode method (without signature verification) + val decodedResult = verifier.decode(verifierParam) + assertNotNull(decodedResult, "Decode result should not be null") + assertEquals(testSubject, decodedResult.subject, "Decoded subject should match") + assertEquals(testData, decodedResult.decryptedData, "Decoded data should match") + assertEquals(TEST_ID, decodedResult.id, "Decoded ID should match") + assertNotNull(decodedResult.expireDateTime, "Expire date time should be set") + assertNotNull(decodedResult.signatureAlgName, "Signature algorithm should be set") + } + + @Test + fun testComplexDataStructures() { + val keyPairs = createTestKeyPairs() + val issuer = createJwtIssuer(keyPairs) + val verifier = createJwtVerifier(keyPairs) + + data class ComplexData( + val id: Long, + val name: String, + val properties: Map, + val tags: List, + val metadata: Map>, + ) + + val complexData = + ComplexData( + id = 12345L, + name = "Complex Test Object", + properties = mapOf("active" to true, "score" to 95.5, "category" to "test"), + tags = listOf("tag1", "tag2", "tag3"), + metadata = mapOf("system" to mapOf("version" to "1.0", "build" to 123), "user" to mapOf("preferences" to mapOf("theme" to "dark"))), + ) + + val issuerParam = IssuerParam(signatureKey = keyPairs.rsaKeyPair.privateKey) + issuerParam.encryptedDataObj = complexData + issuerParam.subjectObj = complexData + + val token = issuer.issued(issuerParam) + assertTokenValid(token) + + val verifierParam = VerifierParam(token = token, subjectTargetType = Any::class.java, encryptDataTargetType = Any::class.java) + val result = verifier.verify(verifierParam) - assertNotNull(result) - assertEquals(simpleSubject, result.subject) - assertEquals(simpleData, result.decryptedData) + assertNotNull(result, "Verification result should not be null") + assertNotNull(result.subject, "Complex subject should be preserved") + assertNotNull(result.decryptedData, "Complex data should be preserved") } } diff --git a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtTest.kt b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtTest.kt index 718540de2..f554cfad1 100644 --- a/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtTest.kt +++ b/security/spring/src/test/kotlin/io/github/truenine/composeserver/security/jwt/JwtTest.kt @@ -1,7 +1,7 @@ package io.github.truenine.composeserver.security.jwt import com.fasterxml.jackson.databind.ObjectMapper -import io.github.truenine.composeserver.security.crypto.Keys +import io.github.truenine.composeserver.security.crypto.CryptographicKeyManager import io.github.truenine.composeserver.security.jwt.consts.IssuerParam import io.github.truenine.composeserver.security.jwt.consts.VerifierParam import org.junit.jupiter.api.Test @@ -11,8 +11,8 @@ class JwtTest { @Test fun testIssuerAndVerifier() { val mapper = ObjectMapper() - val eccPair = Keys.generateEccKeyPair()!! - val rsaPair = Keys.generateRsaKeyPair()!! + val eccPair = CryptographicKeyManager.generateEccKeyPair(CryptographicKeyManager.generateRandomAsciiString())!! + val rsaPair = CryptographicKeyManager.generateRsaKeyPair()!! val issuer = JwtIssuer.createIssuer() From 5d99c4cb291e533dabd8a24c6ccfb9c64bffe7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 11:51:55 +0800 Subject: [PATCH 31/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[security]=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20IBase64=20=E5=8A=A0=E5=AF=86=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡ 优化 Base64 编解码实现,提升性能 - 🔧 改进输入验证和错误处理逻辑,增强鲁棒性 - 🎨 简化代码结构,移除冗余逻辑 --- .../composeserver/security/crypto/IBase64.kt | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/security/crypto/src/main/kotlin/io/github/truenine/composeserver/security/crypto/IBase64.kt b/security/crypto/src/main/kotlin/io/github/truenine/composeserver/security/crypto/IBase64.kt index 50499282e..ffafd0116 100644 --- a/security/crypto/src/main/kotlin/io/github/truenine/composeserver/security/crypto/IBase64.kt +++ b/security/crypto/src/main/kotlin/io/github/truenine/composeserver/security/crypto/IBase64.kt @@ -71,7 +71,7 @@ interface IBase64 { */ @JvmStatic fun encode(content: ByteArray): String { - return String(encoder.encode(content), defaultCharset) + return encoder.encodeToString(content) } /** @@ -99,7 +99,7 @@ interface IBase64 { */ @JvmStatic fun encodeUrlSafe(content: ByteArray): String { - return String(urlSafeEncoder.encode(content), defaultCharset) + return urlSafeEncoder.encodeToString(content) } /** @@ -116,9 +116,14 @@ interface IBase64 { fun decodeToByte(base64: String): ByteArray { // Empty string is a valid Base64 encoding of empty byte array if (base64.isEmpty()) return ByteArray(0) - // Blank strings (containing only whitespace) are invalid - require(base64.isNotBlank()) { "Base64 string cannot be null or blank" } - return runCatching { decoder.decode(base64) }.getOrElse { throw IllegalArgumentException("Invalid Base64 string format", it) } + // Optimized validation: check for blank strings efficiently + if (base64.isBlank()) throw IllegalArgumentException("Base64 string cannot be null or blank") + // Direct decode with exception propagation for better performance + try { + return decoder.decode(base64) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid Base64 string format", e) + } } /** @@ -151,9 +156,13 @@ interface IBase64 { */ @JvmStatic fun decode(base64: ByteArray, charset: Charset = defaultCharset): String { - require(base64.isNotEmpty()) { "Base64 byte array cannot be empty" } - val decodedBytes = runCatching { decoder.decode(base64) }.getOrElse { throw IllegalArgumentException("Invalid Base64 byte array format", it) } - return String(decodedBytes, charset) + if (base64.isEmpty()) throw IllegalArgumentException("Base64 byte array cannot be empty") + try { + val decodedBytes = decoder.decode(base64) + return String(decodedBytes, charset) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid Base64 byte array format", e) + } } /** @@ -168,8 +177,12 @@ interface IBase64 { */ @JvmStatic fun decodeUrlSafe(base64: String): ByteArray { - require(base64.isNotBlank()) { "URL-safe Base64 string cannot be null or blank" } - return runCatching { urlSafeDecoder.decode(base64) }.getOrElse { throw IllegalArgumentException("Invalid URL-safe Base64 string format", it) } + if (base64.isBlank()) throw IllegalArgumentException("URL-safe Base64 string cannot be null or blank") + try { + return urlSafeDecoder.decode(base64) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid URL-safe Base64 string format", e) + } } /** From ec2b884d462d0ddfd92076f13b226b63812465c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=97=A5=E5=A4=A9?= Date: Mon, 14 Jul 2025 12:02:14 +0800 Subject: [PATCH 32/32] =?UTF-8?q?=F0=9F=9A=80=20[release]=200.0.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68af4d1c7..436b12e7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ org-springframework-modulith = "1.4.1" org-springframework-security = "6.5.1" org-testcontainers = "1.21.3" org-testng = "7.11.0" -project = "0.0.6" +project = "0.0.7" [libraries] cn-dev33-sa-token-redis-jackson = { module = "cn.dev33:sa-token-redis-jackson", version.ref = "cn-dev33-sa-token" }