diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61bd050e..3bbe570c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,10 @@ name: Java CI with Maven on: push: branches: + - main - master - dev + - develop pull_request: types: [opened, synchronize, reopened] jobs: @@ -14,14 +16,14 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Cache local Maven repository @@ -43,11 +45,8 @@ jobs: - name: Test with Maven run: mvn -B test -DskipTests=false - name: Upload to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - # files: ./coverage1.xml,./coverage2.xml # optional - flags: unittests # optional - name: codecov-umbrella # optional - fail_ci_if_error: true # optional (default = false) - verbose: true # optional (default = false) + # possibly other stuff + token: ${{ secrets.CODECOV_ORG_TOKEN }} + fail_ci_if_error: false # or true if you want CI to fail when Codecov fails diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index e4cb75d7..69a16d2d 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -22,7 +22,7 @@ jobs: steps: # Checkout the repository to the GitHub Actions runner - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - name: Run Codacy Analysis CLI diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index aa28fad9..c942cd4c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [ "master","dev" ] + branches: [ "master","main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "dev" ] @@ -27,7 +27,25 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache local Maven repository + uses: actions/cache@v3.3.1 + env: + cache-name: cache-mvn + with: + path: ~/.m2/repository + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/oss-deploy.yml b/.github/workflows/oss-deploy.yml index def4c05b..ab22fa9b 100644 --- a/.github/workflows/oss-deploy.yml +++ b/.github/workflows/oss-deploy.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Cache local Maven repository @@ -39,8 +39,9 @@ jobs: # run: mvn deploy # env: # GITHUB_TOKEN: ${{ github.token }} # GITHUB_TOKEN is the default env for the password + - name: Import GPG - uses: crazy-max/ghaction-import-gpg@v5.3.0 + uses: crazy-max/ghaction-import-gpg@v5 with: gpg_private_key: ${{ secrets.MAVEN_GPG_KEY }} passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }} @@ -49,5 +50,5 @@ jobs: run: ./mvnw --settings ./ossrh-settings.xml clean deploy -Dgpg.passphrase=${MAVEN_GPG_PASSPHRASE} -DskipTests=true -P 'oss-release' env: MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + OSSRH_USERNAME: ${{ secrets.OSSRH_TOKEN_USER }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_TOKEN_PWD }} diff --git a/.github/workflows/qodana-analysis.yml b/.github/workflows/qodana-analysis.yml index 12016fd1..07392451 100644 --- a/.github/workflows/qodana-analysis.yml +++ b/.github/workflows/qodana-analysis.yml @@ -7,7 +7,8 @@ on: - main - master - dev - - 'releases/*' + - develop + #- 'releases/*' jobs: qodana: @@ -17,15 +18,15 @@ jobs: pull-requests: write checks: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit fetch-depth: 0 # a full history is required for pull request analysis - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - name: Cache local Maven repository diff --git a/README.md b/README.md index 81e48537..02407adc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.power4j.fist/fist-kit-dependencies/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.power4j.fist/fist-kit-dependencies) +[![Maven Central Version](https://img.shields.io/maven-central/v/com.power4j.fist/fist-kit-dependencies)](https://central.sonatype.com/artifact/com.power4j.fist/fist-kit-dependencies) ## 技术栈 - JDK: `11` diff --git a/fist-kit-app/fist-security/fist-support-security/pom.xml b/fist-kit-app/fist-security/fist-support-security/pom.xml index c93a5dc7..9c9dfa2f 100644 --- a/fist-kit-app/fist-security/fist-support-security/pom.xml +++ b/fist-kit-app/fist-security/fist-support-security/pom.xml @@ -61,6 +61,10 @@ org.springframework spring-core + + com.github.seancfoley + ipaddress + io.projectreactor @@ -92,7 +96,7 @@ org.bouncycastle bcprov-jdk15to18 - 1.77 + 1.78.1 test diff --git a/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/boot/security/inner/TrustedUserFilter.java b/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/boot/security/inner/TrustedUserFilter.java index 04a05b12..eb1df580 100644 --- a/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/boot/security/inner/TrustedUserFilter.java +++ b/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/boot/security/inner/TrustedUserFilter.java @@ -21,11 +21,14 @@ import com.power4j.fist.boot.security.context.UserContextHolder; import com.power4j.fist.boot.security.core.SecurityConstant; import com.power4j.fist.boot.security.core.UserInfo; +import inet.ipaddr.AddressStringException; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.springframework.lang.Nullable; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -34,9 +37,8 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.InetAddress; +import java.util.ArrayList; import java.util.Collection; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; /** * @author CJ (power4j@outlook.com) @@ -54,15 +56,24 @@ public class TrustedUserFilter extends OncePerRequestFilter { @Setter private boolean strictMode = true; - @Override - public void afterPropertiesSet() throws ServletException { - postCheck(); - super.afterPropertiesSet(); - } + private final Collection whitelist = new ArrayList<>(4); - @Nullable - @Setter - private Collection whitelist; + public void setWhitelist(Collection list) { + whitelist.clear(); + if (ObjectUtils.isNotEmpty(list)) { + for (String p : list) { + try { + IPAddressString ip = new IPAddressString(p); + ip.validate(); + whitelist.add(ip.getAddress()); + } + catch (AddressStringException e) { + String msg = "非法IP地址:" + p; + throw new IllegalArgumentException(msg, e); + } + } + } + } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -92,32 +103,19 @@ else if (e.getCause() instanceof IOException) { } } - void postCheck() { - if (whitelist != null) { - for (String p : whitelist) { - try { - Pattern.compile(p); - } - catch (PatternSyntaxException e) { - log.error("表达式非法:{}", p); - throw e; - } - } - } - } - private boolean isTrusted(HttpServletRequest request) { if (strictMode) { String ip = request.getRemoteAddr(); - if (whitelist != null && whitelist.stream().anyMatch(ip::matches)) { - return true; + if (ObjectUtils.isNotEmpty(whitelist)) { + IPAddress reqAddr = new IPAddressString(ip).getAddress(); + return whitelist.stream().anyMatch(addr -> addr.contains(reqAddr)); } else { InetAddress address = NetKit.parse(ip); if (address.isLoopbackAddress() || address.isSiteLocalAddress()) { return true; } - log.warn("认证信息不可信"); + log.warn("认证信息不可信,来源:{}", ip); return false; } } diff --git a/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/security/core/authorization/config/GlobalAuthorizationProperties.java b/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/security/core/authorization/config/GlobalAuthorizationProperties.java index a90927d5..abf49cec 100644 --- a/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/security/core/authorization/config/GlobalAuthorizationProperties.java +++ b/fist-kit-app/fist-security/fist-support-security/src/main/java/com/power4j/fist/security/core/authorization/config/GlobalAuthorizationProperties.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -55,6 +56,8 @@ public class GlobalAuthorizationProperties { private Auth auth = new Auth(); + private AccessIpConfig accessIp = new AccessIpConfig(); + @Data public static class Auth { @@ -81,4 +84,20 @@ public static class SafeModeConfig { } + @Data + public static class AccessIpConfig { + + /** 用户访问IP的最大解析索引,用于防止IP欺骗 */ + private int maxTrustResolves = 64; + + /** 所有用户的IP白名单(CIDR) */ + private List global = Collections.singletonList("0.0.0.0/0"); + + /** + * 针对特定用户的IP白名单(CIDR) + */ + private Map> rules = Collections.emptyMap(); + + } + } diff --git a/fist-kit-app/fist-web/fist-boot-web-app/pom.xml b/fist-kit-app/fist-web/fist-boot-web-app/pom.xml index 69d76d09..3dec1c7f 100644 --- a/fist-kit-app/fist-web/fist-boot-web-app/pom.xml +++ b/fist-kit-app/fist-web/fist-boot-web-app/pom.xml @@ -68,7 +68,7 @@ org.bouncycastle bcprov-jdk15to18 - 1.77 + 1.78.1 test diff --git a/fist-kit-app/fist-web/fist-boot-web-app/src/main/java/com/power4j/fist/boot/common/jackson/JacksonConfig.java b/fist-kit-app/fist-web/fist-boot-web-app/src/main/java/com/power4j/fist/boot/common/jackson/JacksonConfig.java index 43bfce1d..7e2f7114 100644 --- a/fist-kit-app/fist-web/fist-boot-web-app/src/main/java/com/power4j/fist/boot/common/jackson/JacksonConfig.java +++ b/fist-kit-app/fist-web/fist-boot-web-app/src/main/java/com/power4j/fist/boot/common/jackson/JacksonConfig.java @@ -16,10 +16,13 @@ package com.power4j.fist.boot.common.jackson; +import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; import com.power4j.fist.boot.common.jackson.module.DateTimeModule; import com.power4j.fist.boot.common.jackson.module.NumberStrModule; +import com.power4j.fist.jackson.support.obfuscation.ObfuscatedAnnotationIntrospector; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -72,6 +75,7 @@ public Jackson2ObjectMapperBuilderCustomizer customizer() { applyTimeZone(builder); applySimpleDateFormat(builder); applyModules(builder); + applyExtra(builder); }; } @@ -111,4 +115,10 @@ private void applyModules(Jackson2ObjectMapperBuilder builder) { } } + private void applyExtra(Jackson2ObjectMapperBuilder builder) { + log.info("Install extra Serializer/Deserializer"); + builder.annotationIntrospector( + introspector -> AnnotationIntrospectorPair.pair(introspector, new ObfuscatedAnnotationIntrospector())); + } + } diff --git a/fist-kit-app/fist-web/fist-support-web/pom.xml b/fist-kit-app/fist-web/fist-support-web/pom.xml index ae315d11..471589d5 100644 --- a/fist-kit-app/fist-web/fist-support-web/pom.xml +++ b/fist-kit-app/fist-web/fist-support-web/pom.xml @@ -41,6 +41,10 @@ com.power4j.fist fist-support-spring + + com.power4j.fist + fist-jackson + org.apache.commons commons-lang3 diff --git a/fist-kit-build/pom.xml b/fist-kit-build/pom.xml index 28ecd0b5..3cabae70 100644 --- a/fist-kit-build/pom.xml +++ b/fist-kit-build/pom.xml @@ -196,6 +196,11 @@ mapstruct-processor ${mapstruct.version} + + com.github.seancfoley + ipaddress + ${seancfoley.ipaddress.version} + org.springdoc @@ -287,13 +292,36 @@ org.apache.maven.plugins maven-surefire-plugin - ${skipTests} + ${skipTests} -javaagent:${settings.localRepository}/org/jetbrains/intellij/deps/intellij-coverage-agent/${intellij.coverage.agent.version}/intellij-coverage-agent-${intellij.coverage.agent.version}.jar=${intellij.agent.options} + + ${argLine} -Dfile.encoding=UTF-8 + 1 + false + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + org.apache.maven.plugins maven-dependency-plugin diff --git a/fist-kit-cloud/fist-cloud-gateway/fist-cloud-gateway-acl/src/main/java/com/power4j/fist/autoconfigure/gateway/GatewayAuthFilterConfiguration.java b/fist-kit-cloud/fist-cloud-gateway/fist-cloud-gateway-acl/src/main/java/com/power4j/fist/autoconfigure/gateway/GatewayAuthFilterConfiguration.java index 45f9bc11..c81d9d35 100644 --- a/fist-kit-cloud/fist-cloud-gateway/fist-cloud-gateway-acl/src/main/java/com/power4j/fist/autoconfigure/gateway/GatewayAuthFilterConfiguration.java +++ b/fist-kit-cloud/fist-cloud-gateway/fist-cloud-gateway-acl/src/main/java/com/power4j/fist/autoconfigure/gateway/GatewayAuthFilterConfiguration.java @@ -17,11 +17,14 @@ import com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl.SafeModeFilter; import com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl.SkipAuthorizationFilter; import com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl.TenantFilter; +import com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl.UserIpAccessFilter; import com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl.UserPermissionFilter; import com.power4j.fist.cloud.security.oauth2.client.UserIntrospectClient; import com.power4j.fist.security.core.authorization.config.GlobalAuthorizationProperties; import com.power4j.fist.security.core.authorization.domain.PermissionDefinition; import com.power4j.fist.security.core.authorization.service.reactive.ReactivePermissionDefinitionService; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -33,8 +36,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * @author CJ (power4j@outlook.com) @@ -115,6 +121,25 @@ public Oauth2IntrospectFilter oauth2IntrospectFilter(UserIntrospectClient client return new Oauth2IntrospectFilter(client); } + @Bean + @Order(BASE_ORDER + 800) + public UserIpAccessFilter userIpAccessFilter() { + final GlobalAuthorizationProperties.AccessIpConfig config = authorizationProperties.getAccessIp(); + Map> rules = new HashMap<>(8); + List global = config.getGlobal() + .stream() + .map(o -> new IPAddressString(o).getAddress()) + .collect(Collectors.toList()); + rules.put(UserIpAccessFilter.ANY_USER, global); + config.getRules().forEach((k, v) -> { + List rule = v.stream() + .map(o -> new IPAddressString(o).getAddress()) + .collect(Collectors.toList()); + rules.put(k, rule); + }); + return new UserIpAccessFilter(rules, config.getMaxTrustResolves()); + } + @Bean @Order(BASE_ORDER + 1_100) public LoginAccessFilter loginAccessFilter() { diff --git a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/pom.xml b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/pom.xml index e43656ef..8b601f19 100644 --- a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/pom.xml +++ b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/pom.xml @@ -38,6 +38,10 @@ com.power4j.fist fist-support-security + + com.github.seancfoley + ipaddress + org.redisson diff --git a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/domain/AuthProblem.java b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/domain/AuthProblem.java index f49ba5a0..3f083afa 100644 --- a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/domain/AuthProblem.java +++ b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/domain/AuthProblem.java @@ -132,6 +132,11 @@ public String description() { */ public final static AuthProblem USER_ACCESS_DENIED = new AuthProblem(50, "USER_ACCESS_DENIED"); + /** + * 用户IP不在白名单 + */ + public final static AuthProblem USER_IP_DENIED = new AuthProblem(51, "USER_IP_DENIED"); + /** * 无API访问权限 */ diff --git a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilter.java b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilter.java new file mode 100644 index 00000000..987f4c17 --- /dev/null +++ b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/main/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilter.java @@ -0,0 +1,81 @@ +package com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl; + +import com.power4j.fist.cloud.gateway.authorization.domain.AuthContext; +import com.power4j.fist.cloud.gateway.authorization.domain.AuthProblem; +import com.power4j.fist.cloud.gateway.authorization.filter.reactive.GatewayAuthFilter; +import com.power4j.fist.security.core.authorization.domain.AuthenticatedUser; +import com.power4j.fist.security.core.authorization.filter.reactive.ServerAuthFilterChain; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +@Slf4j +public class UserIpAccessFilter implements GatewayAuthFilter { + + public final static String ANY_USER = "*"; + + // key: username, value: list of CIDR rules + private final Map> rules; + + private final XForwardedRemoteAddressResolver resolver; + + public UserIpAccessFilter(Map> rules, int maxTrustResolves) { + this.rules = rules; + this.resolver = XForwardedRemoteAddressResolver.maxTrustedIndex(maxTrustResolves); + } + + @Override + public Mono filter(AuthContext ctx, ServerAuthFilterChain chain) { + if (rules.isEmpty()) { + return doNext(ctx, chain); + } + return applyRules(ctx, chain); + } + + private Mono applyRules(AuthContext ctx, ServerAuthFilterChain chain) { + final AuthenticatedUser userInfo = ctx.getUserInfo(); + if (Objects.isNull(userInfo)) { + return exitChain(ctx, AuthProblem.USER_IP_DENIED.moreInfo("No user info")); + } + final IPAddress accessIp = new IPAddressString(resolver.resolve(ctx.getExchange()).getHostString()) + .getAddress(); + // apply user based rules + final String username = userInfo.getUsername(); + if (rules.containsKey(username)) { + List ipList = rules.get(username); + for (IPAddress rule : ipList) { + if (rule.contains(accessIp)) { + if (log.isDebugEnabled()) { + log.debug("user [{}] allowed access from [{}] with rule [{}]", username, accessIp, rule); + } + return doNext(ctx, chain); + } + } + } + // apply global rules + if (rules.containsKey(ANY_USER)) { + List ipList = rules.get(ANY_USER); + for (IPAddress rule : ipList) { + if (rule.contains(accessIp)) { + if (log.isDebugEnabled()) { + log.debug("user [{}] allowed access from [{}] with global rule [{}]", username, accessIp, rule); + } + return doNext(ctx, chain); + } + } + } + return exitChain(ctx, AuthProblem.USER_IP_DENIED + .moreInfo(String.format("user [%s] not allowed access from [%s]", username, accessIp))); + } + +} diff --git a/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/test/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilterTest.java b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/test/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilterTest.java new file mode 100644 index 00000000..91de5e91 --- /dev/null +++ b/fist-kit-cloud/fist-cloud-gateway/fist-gateway-auth-core/src/test/java/com/power4j/fist/cloud/gateway/authorization/filter/reactive/impl/UserIpAccessFilterTest.java @@ -0,0 +1,112 @@ +package com.power4j.fist.cloud.gateway.authorization.filter.reactive.impl; + +import com.power4j.fist.cloud.gateway.authorization.domain.AuthContext; +import com.power4j.fist.cloud.gateway.authorization.domain.AuthProblem; +import com.power4j.fist.cloud.gateway.authorization.domain.AuthUser; +import com.power4j.fist.security.core.authorization.filter.reactive.ServerAuthFilterChain; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +@ExtendWith(MockitoExtension.class) +class UserIpAccessFilterTest { + + private MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost") + .remoteAddress(InetSocketAddress.createUnresolved("1.2.3.4", 80)) + .build(); + + private ServerWebExchange exchange = MockServerWebExchange.from(request); + + @Spy + private AuthContext authContext = new AuthContext(exchange, null); + + @Mock + private ServerAuthFilterChain authFilterChain; + + @Test + void shouldCallNextIfNoRules() { + AuthContext authContext = Mockito.mock(AuthContext.class); + UserIpAccessFilter filter = new UserIpAccessFilter(Collections.emptyMap(), 1); + when(authFilterChain.filter(any())).thenReturn(Mono.empty()); + Mono result = filter.filter(authContext, authFilterChain); + StepVerifier.create(result).verifyComplete(); + verify(authFilterChain, times(1)).filter(any()); + } + + @Test + void shouldDenyIfNoAuthState() { + Map> rules = Map.of("admin", List.of(new IPAddressString("0.0.0.0/32").getAddress())); + UserIpAccessFilter filter = new UserIpAccessFilter(rules, 1); + when(authContext.getUserInfo()).thenReturn(null); + Mono result = filter.filter(authContext, authFilterChain); + StepVerifier.create(result).verifyComplete(); + assertThat(authContext.getAuthState()).isNotNull(); + assertThat(authContext.getAuthState().getProblem()).isNotNull(); + assertThat(authContext.getAuthState().getProblem().getCode()).isEqualTo(AuthProblem.USER_IP_DENIED.getCode()); + verify(authFilterChain, never()).filter(any()); + } + + @Test + void shouldCallNextWhenUserRuleMatch() { + AuthUser user = AuthUser.builder().username("admin").build(); + Map> rules = Map.of("admin", List.of(new IPAddressString("1.2.0.0/16").getAddress())); + UserIpAccessFilter filter = new UserIpAccessFilter(rules, 1); + when(authContext.getUserInfo()).thenReturn(user); + when(authFilterChain.filter(any())).thenReturn(Mono.empty()); + Mono result = filter.filter(authContext, authFilterChain); + StepVerifier.create(result).verifyComplete(); + verify(authFilterChain, times(1)).filter(any()); + } + + @Test + void shouldCallNextWhenGlobalRuleMatch() { + AuthUser user = AuthUser.builder().username("admin").build(); + Map> rules = Map.of("*", List.of(new IPAddressString("1.2.3.0/24").getAddress())); + UserIpAccessFilter filter = new UserIpAccessFilter(rules, 1); + when(authContext.getUserInfo()).thenReturn(user); + when(authFilterChain.filter(any())).thenReturn(Mono.empty()); + Mono result = filter.filter(authContext, authFilterChain); + StepVerifier.create(result).verifyComplete(); + verify(authFilterChain, times(1)).filter(any()); + } + + @Test + void shouldDenyIfNotMatch() { + Map> rules = Map.of("xx", List.of(new IPAddressString("0.0.0.0/32").getAddress())); + UserIpAccessFilter filter = new UserIpAccessFilter(rules, 1); + when(authContext.getUserInfo()).thenReturn(null); + Mono result = filter.filter(authContext, authFilterChain); + StepVerifier.create(result).verifyComplete(); + assertThat(authContext.getAuthState()).isNotNull(); + assertThat(authContext.getAuthState().getProblem()).isNotNull(); + assertThat(authContext.getAuthState().getProblem().getCode()).isEqualTo(AuthProblem.USER_IP_DENIED.getCode()); + verify(authFilterChain, never()).filter(any()); + } + +} diff --git a/fist-kit-infra/fist-jackson/pom.xml b/fist-kit-infra/fist-jackson/pom.xml index c92c3c7b..3160073f 100644 --- a/fist-kit-infra/fist-jackson/pom.xml +++ b/fist-kit-infra/fist-jackson/pom.xml @@ -30,12 +30,8 @@ - org.apache.commons - commons-lang3 - - - com.google.guava - guava + org.slf4j + slf4j-api org.springframework @@ -43,11 +39,7 @@ com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core + jackson-databind diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Obfuscation.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Obfuscation.java new file mode 100644 index 00000000..35e04a09 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Obfuscation.java @@ -0,0 +1,31 @@ +package com.power4j.fist.jackson.annotation; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.power4j.fist.jackson.support.obfuscation.ObfuscatedStringDeserializer; +import com.power4j.fist.jackson.support.obfuscation.ObfuscatedStringSerializer; +import com.power4j.fist.jackson.support.obfuscation.SimpleStringObfuscate; +import com.power4j.fist.jackson.support.obfuscation.StringObfuscate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Obfuscated value + * + * @author CJ (power4j@outlook.com) + * @since 2022.1 + */ +@JacksonAnnotationsInside +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD }) +@JsonSerialize(using = ObfuscatedStringSerializer.class) +@JsonDeserialize(using = ObfuscatedStringDeserializer.class) +public @interface Obfuscation { + + Class processor() default SimpleStringObfuscate.class; + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Secret.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Secret.java deleted file mode 100644 index 20347853..00000000 --- a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/annotation/Secret.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.power4j.fist.jackson.annotation; - -import com.fasterxml.jackson.annotation.JacksonAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author CJ (power4j@outlook.com) - * @since 2022.1 - */ -@JacksonAnnotation -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.FIELD }) -public @interface Secret { - -} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/package-info.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/package-info.java deleted file mode 100644 index dfffd693..00000000 --- a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/package-info.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2021 ChenJun (power4j@outlook.com & https://github.com/John-Chan) - * - * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @author CJ (power4j@outlook.com) - * @date 2021/6/2 - * @since 1.0 - */ -@NonNullApi -@NonNullFields -package com.power4j.fist.jackson; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; \ No newline at end of file diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedAnnotationIntrospector.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedAnnotationIntrospector.java new file mode 100644 index 00000000..9c6eee0b --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedAnnotationIntrospector.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; +import com.power4j.fist.jackson.annotation.Obfuscation; + +import java.util.Objects; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +public class ObfuscatedAnnotationIntrospector extends NopAnnotationIntrospector { + + private final StringObfuscate defaultProcessor = SimpleStringObfuscate.ofDefault(); + + @Override + public Object findSerializer(Annotated am) { + Obfuscation obfuscation = am.getAnnotation(Obfuscation.class); + if (Objects.nonNull(obfuscation)) { + return new ObfuscatedStringSerializer(defaultProcessor); + } + return super.findSerializer(am); + } + + @Override + public Object findDeserializer(Annotated am) { + Obfuscation obfuscation = am.getAnnotation(Obfuscation.class); + if (Objects.nonNull(obfuscation)) { + return new ObfuscatedStringDeserializer(defaultProcessor); + } + return super.findContentDeserializer(am); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringDeserializer.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringDeserializer.java new file mode 100644 index 00000000..84690160 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringDeserializer.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; +import com.power4j.fist.jackson.annotation.Obfuscation; + +import java.io.IOException; +import java.util.Optional; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + * @see StringObfuscateRegistry + */ +public class ObfuscatedStringDeserializer extends StdDeserializer implements ContextualDeserializer { + + private final StringObfuscate obfuscate; + + public ObfuscatedStringDeserializer() { + super(String.class); + this.obfuscate = SimpleStringObfuscate.ofDefault(); + } + + protected ObfuscatedStringDeserializer(StringObfuscate obfuscate) { + super(String.class); + this.obfuscate = obfuscate; + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException, JacksonException { + String value = p.getValueAsString(); + if (value == null) { + return null; + } + else if (!value.startsWith(obfuscate.algorithm() + ".")) { + return value; + } + value = value.substring(obfuscate.algorithm().length() + 1); + try { + return obfuscate.deobfuscate(value); + } + catch (Exception e) { + throw new JsonParseException(p, e.getMessage(), e); + } + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctx, BeanProperty property) + throws JsonMappingException { + if (property == null) { + return StringDeserializer.instance; + } + Obfuscation annotation = property.getAnnotation(Obfuscation.class); + if (annotation == null) { + return StringDeserializer.instance; + } + Class obfuscate = annotation.processor(); + Optional processor = StringObfuscateRegistry.getObfuscateInstance(obfuscate); + if (processor.isEmpty()) { + throw new IllegalStateException( + String.format("Obfuscation processor not registered: %s", annotation.processor().getName())); + } + return new ObfuscatedStringDeserializer(processor.get()); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringSerializer.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringSerializer.java new file mode 100644 index 00000000..0a64fd1e --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/ObfuscatedStringSerializer.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.power4j.fist.jackson.annotation.Obfuscation; + +import java.io.IOException; +import java.util.Optional; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + * @see StringObfuscateRegistry + */ +public class ObfuscatedStringSerializer extends StdSerializer implements ContextualSerializer { + + private final StringObfuscate obfuscate; + + public ObfuscatedStringSerializer() { + super(String.class); + this.obfuscate = SimpleStringObfuscate.ofDefault(); + } + + public ObfuscatedStringSerializer(StringObfuscate obfuscate) { + super(String.class); + this.obfuscate = obfuscate; + } + + @Override + public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + if (value == null) { + jsonGenerator.writeNull(); + } + else if (value.isEmpty()) { + jsonGenerator.writeString(value); + } + else { + String obfuscated; + try { + obfuscated = obfuscate.algorithm() + "." + obfuscate.obfuscate(value); + } + catch (Exception e) { + throw new JsonGenerationException(e, jsonGenerator); + } + + jsonGenerator.writeString(obfuscated); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { + if (property == null) { + return prov.findNullValueSerializer(property); + } + Obfuscation annotation = property.getAnnotation(Obfuscation.class); + if (annotation == null) { + return prov.findContentValueSerializer(property.getType(), property); + } + Class obfuscate = annotation.processor(); + Optional processor = StringObfuscateRegistry.getObfuscateInstance(obfuscate); + if (processor.isEmpty()) { + throw new IllegalStateException( + String.format("Obfuscation processor not registered: %s", annotation.processor().getName())); + } + return new ObfuscatedStringSerializer(processor.get()); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscate.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscate.java new file mode 100644 index 00000000..3b57bb01 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscate.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +public class SimpleStringObfuscate implements StringObfuscate { + + public final static String ALGORITHM = "OBF.XOR_V1"; + + private final Base64.Encoder ENCODER = Base64.getEncoder(); + + private final Base64.Decoder DECODER = Base64.getDecoder(); + + private final byte[] key; + + public static SimpleStringObfuscate ofDefault() { + return new SimpleStringObfuscate(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + } + + public SimpleStringObfuscate(byte[] key) { + this.key = key; + } + + @Override + public String algorithm() { + return ALGORITHM; + } + + @Override + public String obfuscate(String value) { + if (value.isEmpty()) { + return value; + } + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + byte[] result = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + result[i] = (byte) (bytes[i] ^ key[i % key.length]); + } + return ENCODER.encodeToString(result); + } + + @Override + public String deobfuscate(String value) throws Exception { + if (value.isEmpty()) { + return value; + } + byte[] bytes; + try { + bytes = DECODER.decode(value); + } + catch (IllegalArgumentException e) { + throw new IllegalStateException("Base64 decode error", e); + } + byte[] result = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + result[i] = (byte) (bytes[i] ^ key[i % key.length]); + } + return new String(result, StandardCharsets.UTF_8); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscate.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscate.java new file mode 100644 index 00000000..de218adc --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscate.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +public interface StringObfuscate { + + /** + * The algorithm id + * @return String + */ + String algorithm(); + + /** + * Obfuscate string value + * @param value the value to encode,must not be null,may be empty + * @return Obfuscated string + */ + String obfuscate(String value) throws Exception; + + /** + * Deobfuscate + * @param value the value to deobfuscate,must not be null,may be empty + * @return original string + */ + String deobfuscate(String value) throws Exception; + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscateRegistry.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscateRegistry.java new file mode 100644 index 00000000..456ae43c --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/StringObfuscateRegistry.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +@Slf4j +public class StringObfuscateRegistry { + + private final static Map, Supplier> OBFUSCATE_MAP = new ConcurrentHashMap<>(); + + static { + registerObfuscate(SimpleStringObfuscate.class, SimpleStringObfuscate::ofDefault); + } + + public static Optional getObfuscateInstance(Class obfuscate) { + return Optional.ofNullable(OBFUSCATE_MAP.get(obfuscate)).map(Supplier::get); + } + + public static void registerObfuscate(Class obfuscate, + Supplier supplier) { + OBFUSCATE_MAP.put(obfuscate, supplier); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/package-info.java b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/package-info.java new file mode 100644 index 00000000..661dad65 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/main/java/com/power4j/fist/jackson/support/obfuscation/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author CJ (power4j@outlook.com) + * @date 2021/6/2 + * @since 1.0 + */ +@NonNullApi +@NonNullFields +package com.power4j.fist.jackson.support.obfuscation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/JacksonSimpleStringObfuscateTest.java b/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/JacksonSimpleStringObfuscateTest.java new file mode 100644 index 00000000..67adda25 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/JacksonSimpleStringObfuscateTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; +import com.power4j.fist.jackson.annotation.Obfuscation; +import lombok.Data; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +public class JacksonSimpleStringObfuscateTest { + + @Data + public static class Foo { + + @Obfuscation + private String name; + + } + + private static ObjectMapper objectMapper; + + @BeforeAll + public static void init() { + ObjectMapper mapper = new ObjectMapper(); + AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector(); + AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new ObfuscatedAnnotationIntrospector()); + mapper.setAnnotationIntrospector(is1); + objectMapper = mapper; + } + + @Test + public void testSerialize() throws IOException { + Foo foo = new Foo(); + foo.setName("hello world"); + String value = objectMapper.writeValueAsString(foo); + System.out.println("Serialized: " + value); + Assertions.assertEquals("{\"name\":\"OBF.XOR_V1.aGRub2slcWh6ZW4=\"}", value); + } + + @Test + public void testSerializeNull() throws IOException { + Foo foo = new Foo(); + foo.setName(null); + String value = objectMapper.writeValueAsString(foo); + System.out.println("Serialized: " + value); + Assertions.assertEquals("{\"name\":null}", value); + } + + @Test + public void testSerializeEmpty() throws IOException { + Foo foo = new Foo(); + foo.setName(""); + String value = objectMapper.writeValueAsString(foo); + System.out.println("Serialized: " + value); + Assertions.assertEquals("{\"name\":\"\"}", value); + } + + @Test + public void testDeserialize() throws IOException { + String json = "{\"name\":\"OBF.XOR_V1.aGRub2slcWh6ZW4=\"}"; + Foo foo = objectMapper.readValue(json, Foo.class); + System.out.println("Deserialized: " + foo.getName()); + Assertions.assertEquals("hello world", foo.getName()); + } + + @Test + public void testDeserializeNull() throws IOException { + String json = "{\"name\":null}"; + Foo foo = objectMapper.readValue(json, Foo.class); + System.out.println("Deserialized: " + foo.getName()); + Assertions.assertNull(foo.getName()); + } + + @Test + public void testDeserializeEmpty() throws IOException { + String json = "{\"name\":\"\"}"; + Foo foo = objectMapper.readValue(json, Foo.class); + System.out.println("Deserialized: " + foo.getName()); + Assertions.assertTrue(foo.getName().isEmpty()); + } + +} diff --git a/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscateTest.java b/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscateTest.java new file mode 100644 index 00000000..52fefac1 --- /dev/null +++ b/fist-kit-infra/fist-jackson/src/test/java/com/power4j/fist/jackson/support/obfuscation/SimpleStringObfuscateTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024. ChenJun (power4j@outlook.com & https://github.com/John-Chan) + * + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.power4j.fist.jackson.support.obfuscation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author CJ (power4j@outlook.com) + * @since 1.0 + */ +class SimpleStringObfuscateTest { + + @Test + void handleEmptyString() throws Exception { + StringObfuscate obfuscate = SimpleStringObfuscate.ofDefault(); + String original = ""; + String obfuscated = obfuscate.obfuscate(original); + assertEquals("", obfuscated); + String deobfuscated = obfuscate.deobfuscate(obfuscated); + assertEquals(original, deobfuscated); + } + + @Test + void obfuscate() throws Exception { + StringObfuscate obfuscate = SimpleStringObfuscate.ofDefault(); + String original = "hello world"; + String obfuscated = obfuscate.obfuscate(original); + String deobfuscated = obfuscate.deobfuscate(obfuscated); + assertEquals(original, deobfuscated); + + original = "你好!世界"; + obfuscated = obfuscate.obfuscate(original); + deobfuscated = obfuscate.deobfuscate(obfuscated); + assertEquals(original, deobfuscated); + } + +} diff --git a/pom.xml b/pom.xml index 44d6d59b..60e64069 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ false - 2022.1.11 + 2022.2.1 UTF-8 UTF-8 11 @@ -64,20 +64,21 @@ 1.5.5.Final 0.2.0 - 0.0.40 + 0.0.42 1.6.0 1.13.1 - 3.3.0 + 0.8.12 + 3.3.1 4.9.10 3.2.2 3.11.0 - 3.6.1 - 1.0.748 + 3.7.1 + 1.0.759 ${project.basedir}/.qodana/code-coverage/output.ic,true,true,true,false 2.15.1 0.1.9 - 33.0.0-jre + 33.2.1-jre 5.8.28 3.31.0 0.1.1 @@ -95,6 +96,7 @@ 2.3.6.RELEASE 2.14.0 1.2.7 + 5.5.0 2.3-groovy-4.0 @@ -122,10 +124,6 @@ - - org.jacoco - jacoco-maven-plugin - io.spring.javaformat spring-javaformat-maven-plugin @@ -217,7 +215,7 @@ org.codehaus.groovy groovy - 3.0.19 + 3.0.21 runtime @@ -226,21 +224,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 - - - - prepare-agent - - - - report - test - - report - - - + ${jacoco-maven-plugin.version} pl.project13.maven