├── .gitignore ├── README.MD ├── examples └── data-masking-test │ ├── .gitignore │ ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── stableforever │ │ │ └── security │ │ │ └── masking │ │ │ └── test │ │ │ ├── DataMaskingTestApplication.java │ │ │ ├── SimpleModel.java │ │ │ └── TestController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── com │ └── stableforever │ └── security │ └── masking │ └── test │ └── DataMaskingTestApplicationTests.java ├── pom.xml ├── sensitive-data-masking-core ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── stableforever │ │ └── security │ │ └── masking │ │ ├── Desensitizer.java │ │ ├── DesensitizerImpl.java │ │ ├── GenericMaskMode.java │ │ ├── Sensitive.java │ │ ├── SensitiveType.java │ │ └── config │ │ └── DesensitizerConfigProperties.java │ └── test │ └── java │ └── com │ └── stableforever │ └── security │ └── masking │ └── DesensitizerImplTest.java ├── sensitive-data-masking-jackson-spring-boot-starter ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── stableforever │ │ └── security │ │ └── masking │ │ └── jackson │ │ ├── DataMaskingSpringBootConfiguration.java │ │ └── JacksonDataMaskConfigProperties.java │ └── resources │ └── META-INF │ ├── spring-configuration-metadata.json │ └── spring.factories └── sensitive-data-masking-jackson ├── pom.xml └── src └── main └── java └── com └── stableforever └── security └── masking └── jackson ├── CustomBeanPropertyWriter.java ├── DesensitizerModule.java ├── DesensitizerRegistry.java ├── DesensitizerRegistryImpl.java ├── JsonStringDesensitizer.java └── JsonStringDesensitizerImpl.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Idea 11 | .idea/ 12 | 13 | # Mobile Tools for Java (J2ME) 14 | .mtj.tmp/ 15 | 16 | # Mac 17 | .Ds_Store 18 | 19 | # Package Files # 20 | *.jar 21 | *.war 22 | *.nar 23 | *.ear 24 | *.zip 25 | *.tar.gz 26 | *.rar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | **/target/ 31 | **/.idea/ 32 | *.iml 33 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 数据脱敏 2 | 3 | ## 一、介绍 4 | 5 | 用于处理敏感数据输出时数据脱敏。如人名“张山山”,输出时显示为“张*山”。数据脱敏的应用场景比较多,目前也不有少的人实现了代码。暂时没有看到完整的可以直接拿来就用的封装。 6 | 7 | ## 二、使用方法 8 | 9 | ### 1. 下载本代码并执行mvn install 10 | 11 | ```bash 12 | git clone https://github.com/ColinZou/data-masking.git 13 | cd data-masking && mvn install 14 | ``` 15 | 16 | 稍后再花些时间把代码上传到maven中央仓库。 17 | 18 | ### 2. 引入starter 19 | 20 | ```xml 21 | 22 | com.stableforever 23 | sensitive-data-masking-jackson-spring-boot-starter 24 | 0.0.1-SNAPSHOT 25 | 26 | ``` 27 | 28 | ### 3. 启用 29 | 30 | #### a). 在配置当中添加以下配置项: 31 | 32 | ```properties 33 | web.desensitizer.enabled=true 34 | web.desensitizer.classNamePrefix=com. 35 | ``` 36 | 37 | web.desensitizer.classNamePrefix需要设置为你的数据模型包路径。 38 | 39 | #### b). 在需要做脱敏处理的字段上添加注解 40 | 41 | ```java 42 | @Data 43 | public class SimpleModel { 44 | @Sensitive(value = SensitiveType.CHINESE_NAME) 45 | private String fullName; 46 | } 47 | ``` 48 | 49 | 详见examples/data-masking-test -------------------------------------------------------------------------------- /examples/data-masking-test/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /build/ 27 | 28 | ### VS Code ### 29 | .vscode/ 30 | -------------------------------------------------------------------------------- /examples/data-masking-test/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if (mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if (mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if (!outputFile.getParentFile().exists()) { 87 | if (!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /examples/data-masking-test/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinZou/data-masking/e6a5f51a33b22d63cca53a4e8d30a817f80c47eb/examples/data-masking-test/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /examples/data-masking-test/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /examples/data-masking-test/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /examples/data-masking-test/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /examples/data-masking-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.5.RELEASE 9 | 10 | 11 | com.stableforever 12 | data-masking-test 13 | 0.0.1-SNAPSHOT 14 | data-masking-test 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | true 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | com.stableforever 39 | sensitive-data-masking-jackson-spring-boot-starter 40 | 0.0.1-SNAPSHOT 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/data-masking-test/src/main/java/com/stableforever/security/masking/test/DataMaskingTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.test; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DataMaskingTestApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DataMaskingTestApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/data-masking-test/src/main/java/com/stableforever/security/masking/test/SimpleModel.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.test; 2 | 3 | import com.stableforever.security.masking.Sensitive; 4 | import com.stableforever.security.masking.SensitiveType; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class SimpleModel { 9 | @Sensitive(value = SensitiveType.CHINESE_NAME) 10 | private String fullName; 11 | } 12 | -------------------------------------------------------------------------------- /examples/data-masking-test/src/main/java/com/stableforever/security/masking/test/TestController.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.test; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @RequestMapping(path = "/api/v1/test") 9 | public class TestController { 10 | @RequestMapping(produces = "application/json", method = RequestMethod.GET) 11 | public SimpleModel getSimpleModel() { 12 | SimpleModel model = new SimpleModel(); 13 | model.setFullName("张小三"); 14 | return model; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/data-masking-test/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | web.desensitizer.enabled=true 2 | web.desensitizer.classNamePrefix=com. 3 | -------------------------------------------------------------------------------- /examples/data-masking-test/src/test/java/com/stableforever/security/masking/test/DataMaskingTestApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.test; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class DataMaskingTestApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | pom 6 | 7 | sensitive-data-masking-core 8 | sensitive-data-masking-jackson 9 | sensitive-data-masking-jackson-spring-boot-starter 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.1.5.RELEASE 15 | 16 | 17 | com.stableforever 18 | sensitive-data-masking-parent 19 | 0.0.1-SNAPSHOT 20 | sensitive-data-masking-parent 21 | Sensitive Data Masking 22 | 23 | 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.projectlombok 30 | lombok 31 | true 32 | 33 | 34 | ch.qos.logback 35 | logback-classic 36 | 37 | 38 | junit 39 | junit 40 | test 41 | 42 | 43 | org.springframework.boot 44 | spring-boot 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.codehaus.mojo 66 | versions-maven-plugin 67 | 2.7 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sensitive-data-masking-parent 7 | com.stableforever 8 | 0.0.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | sensitive-data-masking-core 13 | 14 | 15 | com.google.guava 16 | guava 17 | 29.0-jre 18 | 19 | 20 | javax.validation 21 | validation-api 22 | 23 | 24 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/Desensitizer.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | /** 6 | * 字符串脱敏工具 7 | * 8 | * @author colin 9 | * @version 0.1 10 | */ 11 | public interface Desensitizer { 12 | /** 13 | * 默认的mask字符 14 | */ 15 | String DEFAULT_MASK_CHAR = "*"; 16 | 17 | @Slf4j 18 | class Builder { 19 | private DesensitizerImpl item = new DesensitizerImpl(); 20 | private static final int INVALID_ARGUMENT_ERROR_CODE = 101010101; 21 | 22 | private Builder() { 23 | } 24 | 25 | /** 26 | * 生成脱敏工具 27 | * 28 | * @return 29 | */ 30 | public Desensitizer build() { 31 | MaskMode mode = item.getMode(); 32 | if (mode == MaskMode.HEAD && item.getFixedHeaderSize() > 0) { 33 | log.warn("Fixed header bigger than 0 when mode is {}", mode); 34 | } 35 | if (mode == MaskMode.TAIL && item.getFixedTailorSize() > 0) { 36 | log.warn("Fixed tailor bigger than 0 when mode is {}", mode); 37 | } 38 | return item; 39 | } 40 | 41 | /** 42 | * 设置工作模式 43 | * 44 | * @param maskMode 45 | * @return 46 | */ 47 | public Builder sethMode(MaskMode maskMode) { 48 | if (null == maskMode) { 49 | throw new IllegalArgumentException("maskMode cannot be null"); 50 | } 51 | item.setMode(maskMode); 52 | return this; 53 | } 54 | 55 | /** 56 | * 设置遮挡字符 57 | * 58 | * @param maskChar 59 | * @return 60 | */ 61 | public Builder setMaskChar(char maskChar) { 62 | if (maskChar == 0) { 63 | throw new IllegalArgumentException("Invalid mask char"); 64 | } 65 | item.setMaskChar(new String(new char[]{maskChar})); 66 | return this; 67 | } 68 | 69 | /** 70 | * 设置不被mask的开始字符长度 71 | * 72 | * @param size 73 | * @return 74 | */ 75 | public Builder setFixedHeaderSize(int size) { 76 | if (size <= 0) { 77 | throw new IllegalArgumentException("header must be larger than 0"); 78 | } 79 | item.setFixedHeaderSize(size); 80 | return this; 81 | } 82 | 83 | /** 84 | * 设置不被mask的结束字符长度 85 | * 86 | * @param size 87 | * @return 88 | */ 89 | public Builder setFixedTailorSize(int size) { 90 | if (size <= 0) { 91 | throw new IllegalArgumentException("header must be larger than 0"); 92 | } 93 | item.setFixedTailorSize(size); 94 | return this; 95 | } 96 | 97 | /** 98 | * 设置是否自动计算固定部分 99 | * 100 | * @param autoFixedPart 101 | * @return 102 | */ 103 | public Builder setAutoFixedPart(boolean autoFixedPart) { 104 | item.setAuto(autoFixedPart); 105 | return this; 106 | } 107 | } 108 | 109 | /** 110 | * Builder方法 111 | * 112 | * @return 113 | */ 114 | static Builder builder() { 115 | return new Builder(); 116 | } 117 | 118 | /** 119 | * 遮挡模式 120 | */ 121 | enum MaskMode { 122 | /** 123 | * 尾部 124 | */ 125 | TAIL, 126 | /** 127 | * 头部 128 | */ 129 | HEAD, 130 | /** 131 | * 中央 132 | */ 133 | MIDDLE, 134 | } 135 | 136 | /** 137 | * 脱敏 138 | * 139 | * @param rawString 140 | * @return 141 | */ 142 | String desensitize(final String rawString); 143 | } -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/DesensitizerImpl.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | import com.google.common.base.Strings; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | /** 8 | * 脱敏工具实现类 9 | * 10 | * @author colin 11 | * @version 0.1 12 | */ 13 | public class DesensitizerImpl implements Desensitizer { 14 | /** 15 | * 工作模式 16 | */ 17 | @Getter 18 | private MaskMode mode = MaskMode.HEAD; 19 | /** 20 | * 固定的头部字符数量 21 | */ 22 | @Getter 23 | private int fixedHeaderSize = 0; 24 | /** 25 | * 固定的尾部字符数量 26 | */ 27 | @Getter 28 | private int fixedTailorSize = 3; 29 | /** 30 | * mask字符 31 | */ 32 | @Getter 33 | @Setter 34 | private String maskChar = Desensitizer.DEFAULT_MASK_CHAR; 35 | /** 36 | * 自动模型 37 | * 即根据mode来自动决定如何mask 38 | */ 39 | @Getter 40 | @Setter 41 | private boolean auto; 42 | 43 | protected void setMode(MaskMode mode) { 44 | this.mode = mode; 45 | // 设置工作模式时,自动把特定fixed part归零 46 | // 如设置mask头部,则把头部的固定数量归零 47 | switch (mode) { 48 | case TAIL: 49 | this.fixedTailorSize = 0; 50 | break; 51 | case HEAD: 52 | this.fixedHeaderSize = 0; 53 | break; 54 | case MIDDLE: 55 | default: 56 | // do nothing 57 | break; 58 | } 59 | } 60 | 61 | protected void setFixedHeaderSize(int fixedHeaderSize) { 62 | if (mode != MaskMode.HEAD) { 63 | this.fixedHeaderSize = fixedHeaderSize; 64 | } 65 | } 66 | 67 | protected void setFixedTailorSize(int fixedTailorSize) { 68 | if (mode != MaskMode.TAIL) { 69 | this.fixedTailorSize = fixedTailorSize; 70 | } 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return "DesensitizerImpl{" + 76 | "mode=" + mode + 77 | ", fixedHeaderSize=" + fixedHeaderSize + 78 | ", fixedTailorSize=" + fixedTailorSize + 79 | ", maskChar='" + maskChar + '\'' + 80 | '}'; 81 | } 82 | 83 | /** 84 | * 脱敏 85 | * 86 | * @param rawString 87 | * @return 88 | */ 89 | @Override 90 | public String desensitize(String rawString) { 91 | if (Strings.isNullOrEmpty(rawString) || rawString.length() == 1) { 92 | return rawString; 93 | } 94 | if (this.auto) { 95 | return this.desensitizeAuto(rawString); 96 | } 97 | return this.desensitizeManual(rawString); 98 | } 99 | 100 | /** 101 | * 自动模式 102 | * 103 | * @param rawString 104 | * @return 105 | */ 106 | private String desensitizeAuto(String rawString) { 107 | StringBuilder resultBuilder = new StringBuilder(); 108 | int length = rawString.length(); 109 | if (mode == MaskMode.TAIL || mode == MaskMode.HEAD) { 110 | // 以1/2作为遮挡范围 111 | int half = (int) Math.ceil(length / 2.0); 112 | boolean head = mode == MaskMode.HEAD; 113 | if (head) { 114 | resultBuilder.append(Strings.repeat(maskChar, half)) 115 | .append(rawString, half, length); 116 | } else { 117 | resultBuilder.append(rawString, 0, length - half) 118 | .append(Strings.repeat(maskChar, half)); 119 | } 120 | return resultBuilder.toString(); 121 | } 122 | // 仅有两个字符,不能采用遮挡中间的做法 123 | if (length == 2) { 124 | return resultBuilder.append(rawString, 0, 1) 125 | .append(maskChar).toString(); 126 | } 127 | // 以一半字符被mask作为目标 128 | int middle = Math.max((int) Math.ceil(length / 2.0), 1); 129 | // 计算首尾字符长度 130 | int side = Math.max((int) Math.floor((length - middle) / 2.0), 1); 131 | // 修正中间被mask的长度 132 | middle = length - side * 2; 133 | resultBuilder.append(rawString, 0, side) 134 | .append(Strings.repeat(maskChar, middle)) 135 | .append(rawString, side + middle, length); 136 | return resultBuilder.toString(); 137 | } 138 | 139 | /** 140 | * 手动模式 141 | * 142 | * @param rawString 143 | * @return 144 | */ 145 | private String desensitizeManual(String rawString) { 146 | StringBuilder resultBuilder = new StringBuilder(); 147 | int length = rawString.length(); 148 | int maskLength; 149 | switch (mode) { 150 | case TAIL: 151 | if (length <= fixedHeaderSize) { 152 | return rawString; 153 | } 154 | maskLength = length - fixedHeaderSize; 155 | resultBuilder.append(rawString, 0, fixedHeaderSize) 156 | .append(Strings.repeat(maskChar, maskLength)); 157 | break; 158 | default: 159 | case HEAD: 160 | if (length <= fixedTailorSize) { 161 | return rawString; 162 | } 163 | maskLength = length - fixedTailorSize; 164 | resultBuilder.append(Strings.repeat(maskChar, maskLength)) 165 | .append(rawString.substring(maskLength)); 166 | break; 167 | case MIDDLE: 168 | int unmaskLength = fixedTailorSize + fixedHeaderSize; 169 | if (length <= unmaskLength) { 170 | return rawString; 171 | } 172 | maskLength = length - unmaskLength; 173 | resultBuilder.append(rawString, 0, fixedHeaderSize) 174 | .append(Strings.repeat(maskChar, maskLength)) 175 | .append(rawString, fixedHeaderSize + maskLength, length); 176 | break; 177 | } 178 | return resultBuilder.toString(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/GenericMaskMode.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | /** 4 | * 普通脱敏处理的数据遮挡模式 5 | * @author colin 6 | * @version 0.1 7 | */ 8 | public enum GenericMaskMode { 9 | /** 10 | * 开始部分mask 11 | */ 12 | HEAD, 13 | /** 14 | * 结束部分mask 15 | */ 16 | TAIL, 17 | /** 18 | * 中间mask 19 | */ 20 | MIDDLE 21 | } 22 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/Sensitive.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 敏感数据的注解 7 | * 8 | * @author colin 9 | * @version 0.1 10 | */ 11 | @Target(ElementType.FIELD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @Documented 15 | public @interface Sensitive { 16 | /** 17 | * 类型 18 | * 19 | * @return 类型 20 | */ 21 | SensitiveType value(); 22 | 23 | /** 24 | * 一般的数字的mask模式 25 | * 26 | * @return 27 | */ 28 | GenericMaskMode numberMaskMode() 29 | default GenericMaskMode.MIDDLE; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/SensitiveType.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | /** 4 | * 敏感类型 5 | * 6 | * @author colin 7 | * @version 0.1 8 | */ 9 | public enum SensitiveType { 10 | /** 11 | * 中文人名 12 | */ 13 | CHINESE_NAME, 14 | /** 15 | * 身份证号 16 | */ 17 | ID_CARD, 18 | /** 19 | * 电话号码 20 | */ 21 | PHONE_NUMBER, 22 | /** 23 | * 地址 24 | */ 25 | ADDRESS, 26 | /** 27 | * 电子邮件 28 | */ 29 | EMAIL, 30 | /** 31 | * 银行卡 32 | */ 33 | BANK_CARD, 34 | /** 35 | * 密码 36 | */ 37 | PASSWORD, 38 | /** 39 | * 普通号码 40 | */ 41 | GENERIC, 42 | ; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/main/java/com/stableforever/security/masking/config/DesensitizerConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.config; 2 | 3 | import com.stableforever.security.masking.Desensitizer; 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.Min; 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * 配置属性 11 | * 12 | * @author colin 13 | * @version 0.1 14 | */ 15 | @Data 16 | public class DesensitizerConfigProperties { 17 | /** 18 | * 是否启用 19 | */ 20 | private boolean enabled = true; 21 | /** 22 | * 需要处理类名前缀 23 | */ 24 | @NotNull 25 | private String classNamePrefix = ""; 26 | /** 27 | * 中文人名的脱敏方式 28 | */ 29 | @NotNull 30 | private Desensitizer.MaskMode chineseNameMaskMode = Desensitizer.MaskMode.MIDDLE; 31 | /** 32 | * 中文人名固定不变部分自动化处理 33 | */ 34 | private boolean chineseNameFixedPartAutoDecide = true; 35 | /** 36 | * 中文人名脱敏时,固定不变部分的长度 37 | */ 38 | @Min(1) 39 | private int chineseNameHeadSize = 1; 40 | 41 | /** 42 | * 中文人名的脱敏方式 43 | */ 44 | @NotNull 45 | private Desensitizer.MaskMode idCardMaskMode = Desensitizer.MaskMode.MIDDLE; 46 | /** 47 | * 身份证号脱敏时,固定不变的首尾部份是否自动计算 48 | */ 49 | private boolean idCardFixedPartAutoDecide = true; 50 | /** 51 | * 身份证号脱敏时,固定的头部长度 52 | */ 53 | @Min(1) 54 | private int idCardFixedHeadSize = 3; 55 | /** 56 | * 身份证号脱敏时,固定的尾部长度 57 | */ 58 | @Min(1) 59 | private int idCardFixedTailSize = 4; 60 | /** 61 | * 电话号码的脱敏方式 62 | */ 63 | @NotNull 64 | private Desensitizer.MaskMode phoneNumberMaskMode = Desensitizer.MaskMode.MIDDLE; 65 | /** 66 | * 电话号码脱敏时,固定不变的首尾部份是否自动计算 67 | */ 68 | private boolean phoneNumberFixedPartAutoDecide = true; 69 | /** 70 | * 电话号码脱敏时,固定的头部长度 71 | */ 72 | @Min(1) 73 | private int phoneNumberFixedHeadSize = 3; 74 | /** 75 | * 电话号码脱敏时,固定的尾部长度 76 | */ 77 | @Min(1) 78 | private int phoneNumberFixedTailSize = 4; 79 | /** 80 | * 地址脱敏方式 81 | */ 82 | @NotNull 83 | private Desensitizer.MaskMode addressMaskMode = Desensitizer.MaskMode.MIDDLE; 84 | /** 85 | * 地址脱敏时,固定不变的首尾部份是否自动计算 86 | */ 87 | private boolean addressFixedPartAutoDecide = true; 88 | /** 89 | * 地址脱敏时,固定的头部长度 90 | */ 91 | @Min(1) 92 | private int addressFixedHeadSize = 6; 93 | /** 94 | * 地址脱敏时,固定的尾部长度 95 | */ 96 | @Min(1) 97 | private int addressFixedTailSize = 6; 98 | /** 99 | * 地址脱敏方式 100 | */ 101 | @NotNull 102 | private Desensitizer.MaskMode emailMaskMode = Desensitizer.MaskMode.MIDDLE; 103 | /** 104 | * 地址脱敏时,固定不变的首尾部份是否自动计算 105 | */ 106 | private boolean emailFixedPartAutoDecide = true; 107 | /** 108 | * 地址脱敏时,固定的头部长度 109 | */ 110 | @Min(1) 111 | private int emailFixedHeadSize = 2; 112 | /** 113 | * 地址脱敏时,固定的尾部长度 114 | */ 115 | @Min(1) 116 | private int emailFixedTailSize = 2; 117 | /** 118 | * 银行卡脱敏方式 119 | */ 120 | @NotNull 121 | private Desensitizer.MaskMode bankCardMaskMode = Desensitizer.MaskMode.MIDDLE; 122 | /** 123 | * 银行卡脱敏时,固定不变的首尾部份是否自动计算 124 | */ 125 | private boolean bankCardFixedPartAutoDecide = true; 126 | /** 127 | * 银行卡脱敏时,固定的头部长度 128 | */ 129 | @Min(1) 130 | private int bankCardFixedHeadSize = 4; 131 | /** 132 | * 银行卡脱敏时,固定的尾部长度 133 | */ 134 | @Min(1) 135 | private int bankCardFixedTailSize = 4; 136 | /** 137 | * 普通数据脱敏方式 138 | */ 139 | @NotNull 140 | private Desensitizer.MaskMode genericMaskMode = Desensitizer.MaskMode.MIDDLE; 141 | /** 142 | * 普通数据脱敏时,固定不变的首尾部份是否自动计算 143 | */ 144 | private boolean genericFixedPartAutoDecide = true; 145 | /** 146 | * 普通数据脱敏时,固定的头部长度 147 | */ 148 | @Min(1) 149 | private int genericFixedHeadSize = 4; 150 | /** 151 | * 普通数据脱敏时,固定的尾部长度 152 | */ 153 | @Min(1) 154 | private int genericFixedTailSize = 4; 155 | } 156 | -------------------------------------------------------------------------------- /sensitive-data-masking-core/src/test/java/com/stableforever/security/masking/DesensitizerImplTest.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * DesensitizerImpl的测试类 8 | * @author colin 9 | * @version 0.1 10 | */ 11 | public class DesensitizerImplTest { 12 | @Test 13 | public void testDesensitizeHeadModeManual() { 14 | Desensitizer headMode = Desensitizer.builder() 15 | .sethMode(Desensitizer.MaskMode.HEAD) 16 | .setFixedTailorSize(3).build(); 17 | String raw = "123"; 18 | String result = headMode.desensitize(raw); 19 | Assert.assertEquals(result, raw); 20 | 21 | raw = "1234"; 22 | result = headMode.desensitize(raw); 23 | Assert.assertEquals("*234", result); 24 | 25 | raw = "13541355678"; 26 | result = headMode.desensitize(raw); 27 | Assert.assertEquals("********678", result); 28 | } 29 | 30 | @Test 31 | public void testDesensitizeTailModeManual() { 32 | Desensitizer headMode = Desensitizer.builder() 33 | .sethMode(Desensitizer.MaskMode.TAIL) 34 | .setFixedHeaderSize(3).build(); 35 | String raw = "123"; 36 | String result = headMode.desensitize(raw); 37 | Assert.assertEquals(result, raw); 38 | 39 | raw = "1234"; 40 | result = headMode.desensitize(raw); 41 | Assert.assertEquals("123*", result); 42 | 43 | raw = "13541355678"; 44 | result = headMode.desensitize(raw); 45 | Assert.assertEquals("135********", result); 46 | } 47 | 48 | @Test 49 | public void testDesensitizeMiddleModeManual() { 50 | Desensitizer headMode = Desensitizer.builder() 51 | .sethMode(Desensitizer.MaskMode.MIDDLE) 52 | .setFixedHeaderSize(3).setFixedTailorSize(4).build(); 53 | String raw = "123"; 54 | String result = headMode.desensitize(raw); 55 | Assert.assertEquals(result, raw); 56 | 57 | raw = "1234"; 58 | result = headMode.desensitize(raw); 59 | Assert.assertEquals("1234", result); 60 | 61 | raw = "12345678"; 62 | result = headMode.desensitize(raw); 63 | Assert.assertEquals("123*5678", result); 64 | 65 | 66 | raw = "13541325678"; 67 | result = headMode.desensitize(raw); 68 | Assert.assertEquals("135****5678", result); 69 | } 70 | 71 | @Test 72 | public void testDesensitizeHeadModeAuto() { 73 | Desensitizer headMode = Desensitizer.builder() 74 | .sethMode(Desensitizer.MaskMode.HEAD) 75 | .setAutoFixedPart(true).build(); 76 | String raw = "1"; 77 | String result = headMode.desensitize(raw); 78 | Assert.assertEquals(result, raw); 79 | 80 | raw = "12"; 81 | result = headMode.desensitize(raw); 82 | Assert.assertEquals("*2", result); 83 | 84 | raw = "123"; 85 | result = headMode.desensitize(raw); 86 | Assert.assertEquals("**3", result); 87 | 88 | raw = "1234"; 89 | result = headMode.desensitize(raw); 90 | Assert.assertEquals("**34", result); 91 | 92 | raw = "12345"; 93 | result = headMode.desensitize(raw); 94 | Assert.assertEquals("***45", result); 95 | } 96 | 97 | @Test 98 | public void testDesensitizeTailModeAuto() { 99 | Desensitizer headMode = Desensitizer.builder() 100 | .sethMode(Desensitizer.MaskMode.TAIL) 101 | .setAutoFixedPart(true).build(); 102 | String raw = "123"; 103 | String result = headMode.desensitize(raw); 104 | Assert.assertEquals("1**", result); 105 | 106 | raw = "1234"; 107 | result = headMode.desensitize(raw); 108 | Assert.assertEquals("12**", result); 109 | 110 | raw = "12345"; 111 | result = headMode.desensitize(raw); 112 | Assert.assertEquals("12***", result); 113 | } 114 | @Test 115 | public void testDesensitizeMiddleModeAuto() { 116 | Desensitizer headMode = Desensitizer.builder() 117 | .sethMode(Desensitizer.MaskMode.MIDDLE) 118 | .setAutoFixedPart(true).build(); 119 | String raw = "123"; 120 | String result = headMode.desensitize(raw); 121 | Assert.assertEquals("1*3", result); 122 | 123 | raw = "1234"; 124 | result = headMode.desensitize(raw); 125 | Assert.assertEquals("1**4", result); 126 | 127 | raw = "12345678"; 128 | result = headMode.desensitize(raw); 129 | Assert.assertEquals("12****78", result); 130 | 131 | 132 | raw = "13541325678"; 133 | result = headMode.desensitize(raw); 134 | Assert.assertEquals("13*******78", result); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sensitive-data-masking-parent 7 | com.stableforever 8 | 0.0.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | sensitive-data-masking-jackson-spring-boot-starter 13 | 14 | 15 | com.stableforever 16 | sensitive-data-masking-jackson 17 | ${project.parent.version} 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-configuration-processor 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-autoconfigure 26 | 27 | 28 | org.springframework 29 | spring-web 30 | 31 | 32 | org.jetbrains 33 | annotations 34 | 16.0.3 35 | compile 36 | 37 | 38 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson-spring-boot-starter/src/main/java/com/stableforever/security/masking/jackson/DataMaskingSpringBootConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.*; 10 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 11 | 12 | /** 13 | * 自动配置 14 | * 15 | * @author colin 16 | * @version 0.1 17 | */ 18 | 19 | @Configuration 20 | @Slf4j 21 | @EnableConfigurationProperties({JacksonDataMaskConfigProperties.class}) 22 | @ComponentScan("com.stableforever.security.masking") 23 | public class DataMaskingSpringBootConfiguration { 24 | 25 | @Bean 26 | @Autowired 27 | public DesensitizerModule desensitizerModule(@Qualifier("jsonStringDesensitizer") JsonStringDesensitizer jsonStringDesensitizer, 28 | ObjectMapper objectMapper, MappingJackson2HttpMessageConverter converter) { 29 | converter.setObjectMapper(objectMapper); 30 | log.info("INIT {} for desensitizing", DesensitizerModule.class); 31 | return new DesensitizerModule(objectMapper, jsonStringDesensitizer); 32 | } 33 | 34 | @Bean 35 | @Autowired 36 | public DesensitizerRegistry desensitizerRegistry(JacksonDataMaskConfigProperties properties) { 37 | return new DesensitizerRegistryImpl(properties); 38 | } 39 | 40 | @Bean 41 | @Autowired 42 | public JsonStringDesensitizer jsonStringDesensitizer(DesensitizerRegistry registry, 43 | JacksonDataMaskConfigProperties properties) { 44 | return new JsonStringDesensitizerImpl(registry, properties.isEnabled(), properties.getClassNamePrefix()); 45 | } 46 | 47 | @ConditionalOnMissingBean(type = {"org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"}) 48 | @Bean 49 | public MappingJackson2HttpMessageConverter converter() { 50 | MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); 51 | return converter; 52 | } 53 | 54 | @ConditionalOnMissingBean(type = {"com.fasterxml.jackson.databind.ObjectMapper"}) 55 | @Bean 56 | public ObjectMapper jsonMapper() { 57 | return new ObjectMapper(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson-spring-boot-starter/src/main/java/com/stableforever/security/masking/jackson/JacksonDataMaskConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.stableforever.security.masking.config.DesensitizerConfigProperties; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @ConfigurationProperties(prefix = "web.desensitizer") 7 | public class JacksonDataMaskConfigProperties extends DesensitizerConfigProperties { 8 | } 9 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "web.desensitizer.enabled", 5 | "type": "java.lang.Boolean", 6 | "description": "Enabled status for the desensitizer", 7 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 8 | "defaultValue": true 9 | }, 10 | { 11 | "name": "web.desensitizer.classNamePrefix", 12 | "type": "java.lang.String", 13 | "description": "Class name prefix for desensitizing", 14 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 15 | "defaultValue": "" 16 | }, 17 | { 18 | "name": "web.desensitizer.chineseNameMaskMode", 19 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 20 | "description": "Mask mode for Chinese name", 21 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 22 | "defaultValue": "MIDDLE", 23 | "values": [ 24 | { 25 | "value": "MIDDLE" 26 | }, 27 | { 28 | "value": "HEAD" 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "web.desensitizer.chineseNameFixedPartAutoDecide", 34 | "type": "java.lang.Boolean", 35 | "description": "Should auto decide the fixed part", 36 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 37 | "defaultValue": true 38 | }, 39 | { 40 | "name": "web.desensitizer.chineseNameHeadSize", 41 | "type": "java.lang.Integer", 42 | "description": "Fixed head string size", 43 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 44 | "defaultValue": 1 45 | }, 46 | { 47 | "name": "web.desensitizer.idCardMaskMode", 48 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 49 | "description": "Mask mode for idCard", 50 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 51 | "defaultValue": "MIDDLE" 52 | }, 53 | { 54 | "name": "web.desensitizer.idCardFixedPartAutoDecide", 55 | "type": "java.lang.Boolean", 56 | "description": "Should auto decide the fixed part", 57 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 58 | "defaultValue": true 59 | }, 60 | { 61 | "name": "web.desensitizer.idCardFixedHeadSize", 62 | "type": "java.lang.Integer", 63 | "description": "Fixed head string size", 64 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 65 | "defaultValue": 3 66 | }, 67 | { 68 | "name": "web.desensitizer.idCardFixedTailSize", 69 | "type": "java.lang.Integer", 70 | "description": "Fixed tail string size", 71 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 72 | "defaultValue": 4 73 | }, 74 | { 75 | "name": "web.desensitizer.phoneNumberMaskMode", 76 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 77 | "description": "Mask mode", 78 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 79 | "defaultValue": "MIDDLE" 80 | }, 81 | { 82 | "name": "web.desensitizer.phoneNumberFixedPartAutoDecide", 83 | "type": "java.lang.Boolean", 84 | "description": "Should auto decide the fixed part", 85 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 86 | "defaultValue": true 87 | }, 88 | { 89 | "name": "web.desensitizer.phoneNumberFixedHeadSize", 90 | "type": "java.lang.Integer", 91 | "description": "Fixed head string size", 92 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 93 | "defaultValue": 3 94 | }, 95 | { 96 | "name": "web.desensitizer.phoneNumberFixedTailSize", 97 | "type": "java.lang.Integer", 98 | "description": "Fixed tail string size", 99 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 100 | "defaultValue": 4 101 | }, 102 | { 103 | "name": "web.desensitizer.addressMaskMode", 104 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 105 | "description": "Mask mode", 106 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 107 | "defaultValue": "MIDDLE" 108 | }, 109 | { 110 | "name": "web.desensitizer.addressFixedPartAutoDecide", 111 | "type": "java.lang.Boolean", 112 | "description": "Should auto decide the fixed part", 113 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 114 | "defaultValue": true 115 | }, 116 | { 117 | "name": "web.desensitizer.addressFixedHeadSize", 118 | "type": "java.lang.Integer", 119 | "description": "Fixed head string size", 120 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 121 | "defaultValue": 3 122 | }, 123 | { 124 | "name": "web.desensitizer.addressFixedTailSize", 125 | "type": "java.lang.Integer", 126 | "description": "Fixed tail string size", 127 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 128 | "defaultValue": 4 129 | }, 130 | { 131 | "name": "web.desensitizer.emailMaskMode", 132 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 133 | "description": "Mask mode", 134 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 135 | "defaultValue": "MIDDLE" 136 | }, 137 | { 138 | "name": "web.desensitizer.emailFixedPartAutoDecide", 139 | "type": "java.lang.Boolean", 140 | "description": "Should auto decide the fixed part", 141 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 142 | "defaultValue": true 143 | }, 144 | { 145 | "name": "web.desensitizer.emailFixedHeadSize", 146 | "type": "java.lang.Integer", 147 | "description": "Fixed head string size", 148 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 149 | "defaultValue": 3 150 | }, 151 | { 152 | "name": "web.desensitizer.emailFixedTailSize", 153 | "type": "java.lang.Integer", 154 | "description": "Fixed tail string size", 155 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 156 | "defaultValue": 4 157 | }, 158 | { 159 | "name": "web.desensitizer.bankCardMaskMode", 160 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 161 | "description": "Mask mode", 162 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 163 | "defaultValue": "MIDDLE" 164 | }, 165 | { 166 | "name": "web.desensitizer.bankCardFixedPartAutoDecide", 167 | "type": "java.lang.Boolean", 168 | "description": "Should auto decide the fixed part", 169 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 170 | "defaultValue": true 171 | }, 172 | { 173 | "name": "web.desensitizer.bankCardFixedHeadSize", 174 | "type": "java.lang.Integer", 175 | "description": "Fixed head string size", 176 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 177 | "defaultValue": 3 178 | }, 179 | { 180 | "name": "web.desensitizer.bankCardFixedTailSize", 181 | "type": "java.lang.Integer", 182 | "description": "Fixed tail string size", 183 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 184 | "defaultValue": 4 185 | }, 186 | { 187 | "name": "web.desensitizer.genericMaskMode", 188 | "type": "com.stableforever.security.masking.Desensitizer.MaskMode", 189 | "description": "Mask mode", 190 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 191 | "defaultValue": "MIDDLE" 192 | }, 193 | { 194 | "name": "web.desensitizer.genericFixedPartAutoDecide", 195 | "type": "java.lang.Boolean", 196 | "description": "Should auto decide the fixed part", 197 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 198 | "defaultValue": true 199 | }, 200 | { 201 | "name": "web.desensitizer.genericFixedHeadSize", 202 | "type": "java.lang.Integer", 203 | "description": "Fixed head string size", 204 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 205 | "defaultValue": 3 206 | }, 207 | { 208 | "name": "web.desensitizer.genericFixedTailSize", 209 | "type": "java.lang.Integer", 210 | "description": "Fixed tail string size", 211 | "sourceType": "com.stableforever.security.masking.config.DesensitizerConfigProperties", 212 | "defaultValue": 4 213 | } 214 | ] 215 | } -------------------------------------------------------------------------------- /sensitive-data-masking-jackson-spring-boot-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.stableforever.security.masking.jackson.DataMaskingSpringBootConfiguration -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sensitive-data-masking-parent 7 | com.stableforever 8 | 0.0.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | sensitive-data-masking-jackson 13 | 14 | 15 | com.fasterxml.jackson.core 16 | jackson-databind 17 | 18 | 19 | com.stableforever 20 | sensitive-data-masking-core 21 | ${project.parent.version} 22 | 23 | 24 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/CustomBeanPropertyWriter.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.JsonSerializer; 5 | import com.fasterxml.jackson.databind.SerializerProvider; 6 | import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; 7 | import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | /** 11 | * 定制化的属性writer 12 | * 13 | * @author colin 14 | * @version 0.1 15 | */ 16 | @Slf4j 17 | public class CustomBeanPropertyWriter extends BeanPropertyWriter { 18 | /** 19 | * GETTER方法的前缀 20 | */ 21 | private static final String GETTER_PREFIX = "get"; 22 | private JsonStringDesensitizer jsonStringDesensitizer; 23 | 24 | CustomBeanPropertyWriter(BeanPropertyWriter base, String name, JsonStringDesensitizer jsonStringDesensitizer) { 25 | super(base, base.getFullName().withSimpleName(name)); 26 | this.jsonStringDesensitizer = jsonStringDesensitizer; 27 | } 28 | 29 | @Override 30 | public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception { 31 | Class type = _accessorMethod == null ? _field.getDeclaringClass() : _accessorMethod.getDeclaringClass(); 32 | String fieldName = null; 33 | if (_accessorMethod == null) { 34 | fieldName = _field.getName(); 35 | } else if (_accessorMethod.getName().startsWith(GETTER_PREFIX)) { 36 | fieldName = _accessorMethod.getName().substring(GETTER_PREFIX.length()); 37 | //首字母小写 38 | if (fieldName.length() >= 2) { 39 | fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); 40 | } else { 41 | fieldName = fieldName.toLowerCase(); 42 | } 43 | } 44 | final Object value = jsonStringDesensitizer.desensitive( 45 | (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean, (Object[]) null), 46 | type, 47 | fieldName); 48 | // Null handling is bit different, check that first 49 | if (value == null) { 50 | if (_nullSerializer != null) { 51 | gen.writeFieldName(_name); 52 | _nullSerializer.serialize(null, gen, prov); 53 | } 54 | return; 55 | } 56 | // then find serializer to use 57 | JsonSerializer ser = _serializer; 58 | if (ser == null) { 59 | Class cls = value.getClass(); 60 | PropertySerializerMap m = _dynamicSerializers; 61 | ser = m.serializerFor(cls); 62 | if (ser == null) { 63 | ser = _findAndAddDynamic(m, cls, prov); 64 | } 65 | } 66 | // and then see if we must suppress certain values (default, empty) 67 | if (_suppressableValue != null) { 68 | if (MARKER_FOR_EMPTY == _suppressableValue) { 69 | if (ser.isEmpty(prov, value)) { 70 | return; 71 | } 72 | } else if (_suppressableValue.equals(value)) { 73 | return; 74 | } 75 | } 76 | // For non-nulls: simple check for direct cycles 77 | if (value == bean) { 78 | // three choices: exception; handled by call; or pass-through 79 | if (_handleSelfReference(bean, gen, prov, ser)) { 80 | return; 81 | } 82 | } 83 | gen.writeFieldName(_name); 84 | if (_typeSerializer == null) { 85 | ser.serialize(value, gen, prov); 86 | } else { 87 | ser.serializeWithType(value, gen, prov, _typeSerializer); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/DesensitizerModule.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.fasterxml.jackson.databind.BeanDescription; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationConfig; 6 | import com.fasterxml.jackson.databind.module.SimpleModule; 7 | import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; 8 | import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Jackson数据脱敏的模块 16 | * 17 | * @author colin 18 | * @version 0.1 19 | */ 20 | @Slf4j 21 | public class DesensitizerModule extends SimpleModule { 22 | @Autowired 23 | public DesensitizerModule(ObjectMapper objectMapper, JsonStringDesensitizer jsonStringDesensitizer) { 24 | //添加modifier,并向objectmapper注册 25 | this.setSerializerModifier(new BeanSerializerModifierImpl(jsonStringDesensitizer)); 26 | objectMapper.registerModule(this); 27 | log.info("Registering {} to object mapper", this.getClass()); 28 | } 29 | 30 | private static class BeanSerializerModifierImpl extends BeanSerializerModifier { 31 | private JsonStringDesensitizer jsonStringDesensitizer; 32 | 33 | private BeanSerializerModifierImpl(JsonStringDesensitizer jsonStringDesensitizer) { 34 | this.jsonStringDesensitizer = jsonStringDesensitizer; 35 | } 36 | 37 | @Override 38 | public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { 39 | int length = beanProperties.size(); 40 | // 修改BeanPropertyWriter 41 | for (int i = 0; i < length; i++) { 42 | BeanPropertyWriter oldWriter = beanProperties.get(i); 43 | beanProperties.set(i, new CustomBeanPropertyWriter(oldWriter, oldWriter.getName(), jsonStringDesensitizer)); 44 | } 45 | return super.changeProperties(config, beanDesc, beanProperties); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/DesensitizerRegistry.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.stableforever.security.masking.Desensitizer; 4 | import com.stableforever.security.masking.Sensitive; 5 | 6 | /** 7 | * 脱敏工具注册表 8 | * @author colin 9 | * @version 0.1 10 | */ 11 | public interface DesensitizerRegistry { 12 | /** 13 | * 寻找实现 14 | * @param sensitive 15 | * @return 16 | */ 17 | Desensitizer lookup(Sensitive sensitive); 18 | } 19 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/DesensitizerRegistryImpl.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.google.common.base.Strings; 4 | import com.stableforever.security.masking.*; 5 | import com.stableforever.security.masking.config.DesensitizerConfigProperties; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * 脱敏工具集 13 | * 14 | * @author colin 15 | * @version 0.1 16 | */ 17 | public class DesensitizerRegistryImpl implements DesensitizerRegistry { 18 | /** 19 | * 脱敏工具 20 | * map的key通过以下方法进行计算 21 | * 22 | * @link com.jiujin.json.desensitizer.service.DesensitizerRegistryImpl.SensitiveInstance#calcSensitiveHash() 23 | */ 24 | private Map desensitizerMap = new HashMap<>(); 25 | 26 | /** 27 | * 敏感对象实例,用于记录具体的脱敏配置 28 | * 29 | * @author colin 30 | * @version 0.1 31 | */ 32 | @SuppressWarnings(value = "ALL") 33 | public static class SensitiveInstance implements Sensitive { 34 | private int hashCode; 35 | private final SensitiveType type; 36 | private final GenericMaskMode genericMaskMode; 37 | 38 | /** 39 | * 构造函数 40 | * 41 | * @param sensitive 42 | */ 43 | SensitiveInstance(Sensitive sensitive) { 44 | this.type = sensitive.value(); 45 | this.genericMaskMode = sensitive.numberMaskMode(); 46 | this.hashCode = calcSensitiveHash(this.type, this.genericMaskMode); 47 | } 48 | 49 | /** 50 | * 构建函数 51 | * 52 | * @param type 53 | * @param genericMaskMode 54 | */ 55 | SensitiveInstance(SensitiveType type, GenericMaskMode genericMaskMode) { 56 | this.type = type; 57 | this.genericMaskMode = genericMaskMode; 58 | this.hashCode = calcSensitiveHash(type, genericMaskMode); 59 | } 60 | 61 | SensitiveInstance(SensitiveType type) { 62 | this(type, GenericMaskMode.MIDDLE); 63 | } 64 | 65 | /** 66 | * 计算sensitive的hash值 67 | * 68 | * @param type 69 | * @param numberMaskMode 70 | * @return 71 | */ 72 | static int calcSensitiveHash(SensitiveType type, GenericMaskMode numberMaskMode) { 73 | return type.hashCode() + numberMaskMode.hashCode(); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return this.hashCode; 79 | } 80 | 81 | @Override 82 | public boolean equals(Object obj) { 83 | if (null == obj || !Sensitive.class.isAssignableFrom(obj.getClass())) { 84 | return false; 85 | } 86 | Sensitive target = (Sensitive) obj; 87 | return target.value().equals(this.value()) && 88 | target.numberMaskMode().equals(this.numberMaskMode()); 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return "SensitiveInstance{strValue='" + this.value().name() + 94 | "' genericMaskMode='" + this.numberMaskMode().name() + "'}"; 95 | } 96 | 97 | /** 98 | * 类型 99 | * 100 | * @return 类型 101 | */ 102 | @Override 103 | public SensitiveType value() { 104 | return this.type; 105 | } 106 | 107 | /** 108 | * 一般的数字的mask模式 109 | * 110 | * @return 111 | */ 112 | @Override 113 | public GenericMaskMode numberMaskMode() { 114 | return this.genericMaskMode; 115 | } 116 | 117 | /** 118 | * Returns the annotation type of this annotation. 119 | * 120 | * @return the annotation type of this annotation 121 | */ 122 | @Override 123 | public Class annotationType() { 124 | return Sensitive.class; 125 | } 126 | } 127 | 128 | /** 129 | * 电子邮件脱敏 130 | * 131 | * @author colin 132 | * @version 0.1 133 | */ 134 | private static class EmailDesensitizer extends DesensitizerImpl { 135 | private static final String AT_SIGN = "@"; 136 | 137 | EmailDesensitizer(final DesensitizerConfigProperties properties) { 138 | this.setMode(properties.getEmailMaskMode()); 139 | this.setFixedHeaderSize(properties.getEmailFixedHeadSize()); 140 | this.setFixedTailorSize(properties.getEmailFixedTailSize()); 141 | this.setAuto(properties.isEmailFixedPartAutoDecide()); 142 | } 143 | 144 | /** 145 | * 脱敏 146 | * 147 | * @param rawString 148 | * @return 149 | */ 150 | @Override 151 | public String desensitize(final String rawString) { 152 | int atSignIndex = rawString.indexOf(AT_SIGN); 153 | if (atSignIndex <= 0) { 154 | return rawString; 155 | } 156 | return super.desensitize(rawString.substring(0, atSignIndex)) + 157 | rawString.substring(atSignIndex); 158 | } 159 | } 160 | 161 | public DesensitizerRegistryImpl(final DesensitizerConfigProperties properties) { 162 | // 中文人名 163 | this.reg(new SensitiveInstance(SensitiveType.CHINESE_NAME), 164 | Desensitizer.builder() 165 | .sethMode(properties.getChineseNameMaskMode()) 166 | .setFixedHeaderSize(properties.getChineseNameHeadSize()) 167 | .setAutoFixedPart(properties.isChineseNameFixedPartAutoDecide()) 168 | .build() 169 | ); 170 | // 身份证号 171 | this.reg(new SensitiveInstance(SensitiveType.ID_CARD), 172 | Desensitizer.builder() 173 | .sethMode(properties.getIdCardMaskMode()) 174 | .setFixedHeaderSize(properties.getIdCardFixedHeadSize()) 175 | .setFixedTailorSize(properties.getIdCardFixedTailSize()) 176 | .setAutoFixedPart(properties.isIdCardFixedPartAutoDecide()) 177 | .build() 178 | ); 179 | //电话号码 180 | this.reg(new SensitiveInstance(SensitiveType.PHONE_NUMBER), 181 | Desensitizer.builder() 182 | .sethMode(properties.getPhoneNumberMaskMode()) 183 | .setFixedHeaderSize(properties.getPhoneNumberFixedHeadSize()) 184 | .setFixedTailorSize(properties.getPhoneNumberFixedTailSize()) 185 | .setAutoFixedPart(properties.isPhoneNumberFixedPartAutoDecide()) 186 | .build() 187 | ); 188 | // 地址 189 | this.reg(new SensitiveInstance(SensitiveType.ADDRESS), 190 | Desensitizer.builder() 191 | .sethMode(properties.getAddressMaskMode()) 192 | .setFixedHeaderSize(properties.getAddressFixedHeadSize()) 193 | .setFixedTailorSize(properties.getAddressFixedTailSize()) 194 | .setAutoFixedPart(properties.isAddressFixedPartAutoDecide()) 195 | .build() 196 | ); 197 | // 电子邮件 198 | this.reg(new SensitiveInstance(SensitiveType.EMAIL), 199 | new EmailDesensitizer(properties) 200 | ); 201 | // 银行卡 202 | this.reg(new SensitiveInstance(SensitiveType.BANK_CARD), 203 | Desensitizer.builder() 204 | .sethMode(properties.getBankCardMaskMode()) 205 | .setFixedHeaderSize(properties.getBankCardFixedHeadSize()) 206 | .setFixedTailorSize(properties.getBankCardFixedTailSize()) 207 | .setAutoFixedPart(properties.isBankCardFixedPartAutoDecide()) 208 | .build() 209 | ); 210 | // 密码 211 | this.reg(new SensitiveInstance(SensitiveType.PASSWORD), rawString -> { 212 | if (Strings.isNullOrEmpty(rawString)) { 213 | return rawString; 214 | } 215 | return Strings.repeat(Desensitizer.DEFAULT_MASK_CHAR, rawString.length()); 216 | }); 217 | // 普通号码:头、中、后 218 | this.reg(new SensitiveInstance(SensitiveType.GENERIC, GenericMaskMode.HEAD), 219 | Desensitizer.builder() 220 | .sethMode(Desensitizer.MaskMode.HEAD) 221 | .setFixedHeaderSize(properties.getGenericFixedHeadSize()) 222 | .setFixedTailorSize(properties.getGenericFixedTailSize()) 223 | .setAutoFixedPart(properties.isGenericFixedPartAutoDecide()) 224 | .build() 225 | ); 226 | this.reg(new SensitiveInstance(SensitiveType.GENERIC, GenericMaskMode.MIDDLE), 227 | Desensitizer.builder() 228 | .sethMode(Desensitizer.MaskMode.MIDDLE) 229 | .setFixedHeaderSize(properties.getGenericFixedHeadSize()) 230 | .setFixedTailorSize(properties.getGenericFixedTailSize()) 231 | .setAutoFixedPart(properties.isGenericFixedPartAutoDecide()) 232 | .build() 233 | ); 234 | this.reg(new SensitiveInstance(SensitiveType.GENERIC, GenericMaskMode.TAIL), 235 | Desensitizer.builder() 236 | .sethMode(Desensitizer.MaskMode.TAIL) 237 | .setFixedHeaderSize(properties.getGenericFixedHeadSize()) 238 | .setFixedTailorSize(properties.getGenericFixedTailSize()) 239 | .setAutoFixedPart(properties.isGenericFixedPartAutoDecide()) 240 | .build() 241 | ); 242 | } 243 | 244 | /** 245 | * 注册 246 | * 247 | * @param sensitive 248 | * @param item 249 | * @param item 250 | */ 251 | void reg(Sensitive sensitive, Desensitizer item) { 252 | this.desensitizerMap.put(sensitive.hashCode(), item); 253 | } 254 | 255 | /** 256 | * 寻找实现 257 | * 258 | * @param sensitive 259 | * @return 260 | */ 261 | @Override 262 | public Desensitizer lookup(Sensitive sensitive) { 263 | return desensitizerMap.get(SensitiveInstance.calcSensitiveHash(sensitive.value(), sensitive.numberMaskMode())); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/JsonStringDesensitizer.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | /** 4 | * JSON字符串的脱敏接口 5 | * @author colin 6 | * @version 0.1 7 | */ 8 | public interface JsonStringDesensitizer { 9 | /** 10 | * 脱敏方法 11 | * @param rawValue 12 | * @param modelType 13 | * @param fieldName 14 | * @return 15 | */ 16 | Object desensitive(final Object rawValue, Class modelType, String fieldName); 17 | } 18 | -------------------------------------------------------------------------------- /sensitive-data-masking-jackson/src/main/java/com/stableforever/security/masking/jackson/JsonStringDesensitizerImpl.java: -------------------------------------------------------------------------------- 1 | package com.stableforever.security.masking.jackson; 2 | 3 | import com.google.common.base.Strings; 4 | import com.stableforever.security.masking.Sensitive; 5 | 6 | import java.lang.reflect.Field; 7 | import java.util.HashMap; 8 | import java.util.HashSet; 9 | import java.util.Map; 10 | import java.util.Set; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.locks.ReentrantLock; 13 | 14 | /** 15 | * JSON字符串脱敏工具实现类 16 | * 17 | * @author colin 18 | * @version 0.1 19 | */ 20 | public class JsonStringDesensitizerImpl implements JsonStringDesensitizer { 21 | /** 22 | * 敏感字段缓存 23 | */ 24 | private final Map> SENSITIVE_FIELD_CACHE_MAP = new ConcurrentHashMap<>(); 25 | /** 26 | * 第三敏感字段更新时用到的锁 27 | */ 28 | private final ReentrantLock SENSITIVE_FIELD_CACHE_MAP_LOCK = new ReentrantLock(false); 29 | private final DesensitizerRegistry registry; 30 | private final boolean enabled; 31 | private final String classPrefix; 32 | 33 | public JsonStringDesensitizerImpl(DesensitizerRegistry registry, boolean enabled, String classPrefix) { 34 | this.registry = registry; 35 | this.enabled = enabled; 36 | this.classPrefix = classPrefix; 37 | } 38 | 39 | /** 40 | * 脱敏方法 41 | * 42 | * @param rawValue 43 | * @param modelType 44 | * @param fieldName 45 | * @return 46 | */ 47 | @Override 48 | public Object desensitive(Object rawValue, Class modelType, String fieldName) { 49 | // 检查包名 50 | if (null == fieldName || null == rawValue || !enabled || !modelType.getPackage().getName().startsWith(this.classPrefix)) { 51 | return rawValue; 52 | } 53 | Map fields = getSensitiveFields(modelType); 54 | // 只有字段在敏感字段列表当中出现,然后rawValue是字符串是才做处理 55 | if (!fields.containsKey(fieldName) || !(rawValue instanceof CharSequence) || Strings.isNullOrEmpty((String) rawValue)) { 56 | return rawValue; 57 | } 58 | Sensitive sensitive = fields.get(fieldName); 59 | return registry.lookup(sensitive).desensitize((String) rawValue); 60 | } 61 | 62 | /** 63 | * 读取特定类型的敏感字段 64 | * 65 | * @param type 66 | * @return type当中包含的敏感字段 67 | */ 68 | private Map getSensitiveFields(Class type) { 69 | if (SENSITIVE_FIELD_CACHE_MAP.containsKey(type)) { 70 | return SENSITIVE_FIELD_CACHE_MAP.get(type); 71 | } 72 | final Map result = new HashMap<>(); 73 | SENSITIVE_FIELD_CACHE_MAP_LOCK.lock(); 74 | try { 75 | Set fieldSet = new HashSet<>(); 76 | Field[] fields = type.getDeclaredFields(); 77 | for (Field f : fields) { 78 | if (f.isAnnotationPresent(Sensitive.class)) { 79 | fieldSet.add(f); 80 | } 81 | } 82 | fieldSet.forEach(item -> { 83 | result.put(item.getName(), item.getAnnotation(Sensitive.class)); 84 | }); 85 | // 父类当中可能也存在相关的字段需要处理 86 | Class superClass = type.getSuperclass(); 87 | if (superClass != null && superClass != Object.class) { 88 | result.putAll(getSensitiveFields(superClass)); 89 | } 90 | // 添加到缓存 91 | SENSITIVE_FIELD_CACHE_MAP.put(type, result); 92 | } finally { 93 | SENSITIVE_FIELD_CACHE_MAP_LOCK.unlock(); 94 | } 95 | return result; 96 | } 97 | } 98 | --------------------------------------------------------------------------------