配置文件及日志文件脱敏

配置文件脱敏

使用原因:在项目中,经常需要在配置文件里配置一些敏感信息,比如数据库用户名和密码,redis、mq的连接信息等。如果直接写明文,很容易造成密码泄露等安全问题。

jasypt简介

Jasypt是一个Java库,它允许开发者以最小的改动为项目添加基本的加密功能,而且不需要对密码学的工作原理有深刻的了解。

Jasypt特点
  1. 高安全性、基于标准的加密技术,既可用于单向加密也可用于双向加密。加密密码、文本、数字、二进制文件
  2. 与Hibernate的透明集成
  3. 适合集成到基本Spring的应用程序中,也可与Spring Security透明的集成
  4. 对应用程序的配置(即数据源)进行加密的综合能力
  5. 在多处理器/多核系统中具有高性能加密的特殊功能
  6. 开放的API,可与任何JCE供应商一起使用
  7. 配置相关的加密信息,就能够实现在项目运行的时候,自动把配置文件已经加密的信息解密成明文,供程序使用

可以加密所有的Spring环境配置信息,比如系统变量、环境变量、命令行变量、applicationproperties,yaml配置等

jar包
 <!--配置文件加密-->
 <!--方式一: 直接引入jasypt-spring-boot-starter-->
 <dependency>
        <groupId>com.github.ulisesbocchio</groupId>
        <artifactId>jasypt-spring-boot-starter</artifactId>
        <version>2.0.0</version>
 </dependency>

<!-- 方式二: 需在启动类添加@EnableEncryptableProperties -->
 <dependency>
      <groupId>com.github.ulisesbocchio</groupId>
      <artifactId>jasypt-spring-boot</artifactId>
      <version>3.0.3</version>
 </dependency>

需要注意版本对应

jasypt-spring-boot-starter依赖的 spring-boot-starter
2.1.02.0.3.RELEASE 2.2.6.RELEASE
2.0.02.0.0.RELEASE 2.2.6.RELEASE
1.181.5.10.RELEASE 2.2.6.RELEASE
1.121.5.1.RELEASE 2.2.6.RELEASE

需要注加解密的类型一致,如:

2.0.0;2.1.0;2.1.1;2.1.2版本默认加密方式为:PBEWithMD5AndDES
3.0.3版本默认加密方式为:PBEWITHHMACSHA512ANDAES_256
当引入3.0.3依赖,却没有添加相关jasypt加解密配置,而密文通过【PBEWithMD5AndDES】来加密,启动会报错。
需要切换为【PBEWITHHMACSHA512ANDAES_256】方式进行。
配置文件中密码加解密工具类
import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.EnvironmentStringPBEConfig;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.jasypt.util.text.BasicTextEncryptor;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 配置文件中密码加解密工具类
 */
public class JasypUtil {
     public static void main(String[] args) {
//        /*加密方式为: PBEWithMD5AndDES*/
//        BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
//        //加密所需的salt(盐)
//        textEncryptor.setPassword("XVo2eH1oYD+Mc5hwZUdp6w==");
//        //要加密的数据(数据库的用户名或密码)
//        String passwordEncrypt = textEncryptor.encrypt("admin_123");
//        //解密
//        String passwordDencrypt = textEncryptor.decrypt(passwordEncrypt);
//        System.out.println("解密后的password明文" + passwordDencrypt);
//        System.out.println("password密文:" + passwordEncrypt);

        // 加密方式:PBEWithMD5AndDES
        String factor = encryptWithMD5("123456", "123456");
        System.out.println("加密后的密钥: "+factor);
        String password = encryptWithMD5("admin_123","1x6rd4Yu7IVQ1/O64gggqw==");
        System.out.println("加密后的密码: "+password);
 
        //加密方式:PBEWITHHMACSHA512ANDAES_256
//        String factor = encryptWithSHA512("123456", "123456");
//        System.out.println("加密后的密钥: "+factor);
//        String password = encryptWithSHA512("admin_123","cSkz30kwdEEKkThXtpxGhGse9HeNtofAzvzAFgVK58cXPOVeOX7Dm2tZ9IqRTyFL");
//        System.out.println("加密后的密码: "+password);
    }
    private static final String PBEWITHMD5ANDDES = "PBEWithMD5AndDES";
    private static final String PBEWITHHMACSHA512ANDAES_256 = "PBEWITHHMACSHA512ANDAES_256";
    /**
     * @param plainText 待加密的原文
     * @param factor    加密秘钥
     * @return java.lang.String
     * @Description: Jasyp加密(PBEWithMD5AndDES)
     * @Version: 1.0.0
     */
public static String encryptWithMD5(String plainText, String factor) {
         // 1. 创建加解密工具实例
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        // 2. 加解密配置
        EnvironmentStringPBEConfig config = new EnvironmentStringPBEConfig();
        config.setAlgorithm(PBEWITHMD5ANDDES);
        config.setPassword(factor);
        encryptor.setConfig(config);
        // 3. 加密
        return encryptor.encrypt(plainText);
    }
    
/**
     * @param encryptedText 待解密密文
     * @param factor        解密秘钥
     * @return java.lang.String
     * @Description: Jaspy解密(PBEWithMD5AndDES)
     * @Version: 1.0.0
     */
     public static String decryptWithMD5(String encryptedText, String factor) {
        // 1. 创建加解密工具实例
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        // 2. 加解密配置
         EnvironmentStringPBEConfig config = new EnvironmentStringPBEConfig();
        config.setAlgorithm(PBEWITHMD5ANDDES);
        config.setPassword(factor);
        encryptor.setConfig(config);
        // 3. 解密
        return encryptor.decrypt(encryptedText);
    }

/**
     * @param plainText 待加密的原文
     * @param factor    加密秘钥
     * @return java.lang.String
     * @Description: Jasyp 加密(PBEWITHHMACSHA512ANDAES_256)
     * @Version: 2.1.1;2.1.1;3.0.3
     */
     public static String encryptWithSHA512(String plainText, String factor) {
        // 1. 创建加解密工具实例
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
       // 2. 加解密配置
       SimpleStringPBEConfig config = new SimpleStringPBEConfig();
       config.setPassword(factor);
        config.setAlgorithm(PBEWITHHMACSHA512ANDAES_256);
        // 为减少配置文件的书写,以下都是 Jasyp 3.x 版本,配置文件默认配置
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
		config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        // 3. 加密
        return encryptor.encrypt(plainText);
    }

/**
     * @param encryptedText 待解密密文
     * @param factor        解密秘钥
     * @return java.lang.String
     * @Description: Jaspy解密(PBEWITHHMACSHA512ANDAES_256)
     * @Version: 2.1.1;2.1.1;3.0.3
     */
     public static String decryptWithSHA512(String encryptedText, String factor) {
        // 1. 创建加解密工具实例
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        // 2. 加解密配置
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(factor);
        config.setAlgorithm(PBEWITHHMACSHA512ANDAES_256);
        // 为减少配置文件的书写,以下都是 Jasyp 3.x 版本,配置文件默认配置
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        // 3. 解密
        return encryptor.decrypt(encryptedText);
    }

}
配置文件
datasource:
    url: jdbc:postgresql://xxip:host/publicdb?allowMultiQueries=true
    username: postgres
    password: PASSWORD(jzABo0VIZC0/vnIHIO4yuzeqiiNSlOMm)     
    driver-class-name: org.postgresql.Driver
jasypt:
  encryptor:
    #密钥
    password: 1x6rd4Yu7IVQ1/O64gggqw==    
    algorithm: PBEWITHHMACSHA512ANDAES_256
    #指定前缀、后缀
    property:
      prefix: 'PASSWORD('
      suffix: ')'

配置文件加入秘钥配置项jasypt.encryptor.password,并将需要脱敏的value值替换成预先经过加密的内容

PASSWORD(jzABo0VIZC0/vnIHIO4yuzeqiiNSlOMm)

关于密钥放置位置:

1.配置文件中(若使用明文配置,仍有安全问题,可加密后放置于配置文件中)

jasypt:
  encryptor:
    password: 1x6rd4Yu7IVQ1/O64gggqw== 

2.启动类里:不易更改密钥

public class SpringBootRun {
    public static void main(String[] args) {
     /*配置加解密密钥,与配置文件的密文分开方*/
     System.setProperty("jasypt.encryptor.password","1x6rd4Yu7IVQ1/O64gggqw==");
     SpringApplication.run(SpringBootRun.class, args);
    }

3.放置于启动配置中

-Djasypt.encryptor.password=1x6rd4Yu7IVQ1/O64gggqw==

日志文件脱敏

Java程序中实现日志文件脱敏的方法有很多种,以下是其中一些常用的方法:

  1. 使用log4j等日志框架:许多Java应用程序使用日志框架(如log4j)来记录日志。这些框架通常提供了自定义布局和过滤器等功能,可以轻松地实现日志脱敏。例如,在log4j中,您可以编写自定义布局或过滤器来删除或替换敏感信息,然后将其添加到日志配置文件中。
  2. 在代码中手动替换敏感信息:您可以在Java代码中手动替换敏感信息,而不是将其打印到日志文件。例如,如果需要脱敏的信息是电话号码,则可以使用正则表达式或其他替换方法将其替换为星号或其他字符。然后,您可以将替换后的字符串记录到日志文件中。
  3. 使用AOP或拦截器:另一种实现日志文件脱敏的方法是使用AOP(面向切面编程)或拦截器。在这种方法中,您可以创建一个切面或拦截器,并将其应用于所有需要记录的方法上。然后,在切面或拦截器中,您可以检查参数、返回值和异常,并删除或替换敏感信息。

无论您选择哪种方法,都需要仔细考虑哪些信息应该脱敏以及如何脱敏。请注意,脱敏不一定是完全安全的,因此需要在记录日志时采取其他安全措施,例如加密和访问控制等。

log4j实现脱敏
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * log4j2 脱敏插件
 * 继承AbstractStringLayout
 **/
@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class CustomPatternLayout extends AbstractStringLayout {
			public final static Logger logger = LoggerFactory.getLogger(CustomPatternLayout.class);
            private PatternLayout patternLayout;
			 protected CustomPatternLayout(Charset charset, String pattern) {
           			super(charset);
       			    patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
                    initRule();
            }
		
			/**
 		    * 要匹配的正则表达式map
    		*/
    		private static Map<String, Pattern> REG_PATTERN_MAP = new HashMap<>();
   		   private static Map<String, String> KEY_REG_MAP = new HashMap<>();
			
			private void initRule() {
        		try {
            		if (MapUtils.isEmpty(Log4j2Rule.regularMap)) {
               			 return;
           			 }
					Log4j2Rule.regularMap.forEach((a, b) -> {
               			 if (StringUtils.isNotBlank(a)) {
                  	    	   Map<String, String> collect = Arrays.stream(a.split(",")).collect(Collectors.toMap(c -> c, w -> b, (key1, key2) -> key1));
                  	     		KEY_REG_MAP.putAll(collect);
               			 }
						 Pattern compile = Pattern.compile(b);
                         REG_PATTERN_MAP.put(b, compile);
                  });
             } catch (Exception e) {
            logger.info(">>>>>> 初始化日志脱敏规则失败 ERROR:", e);
        }

    }
  
	/**
     * 处理日志信息,进行脱敏
     * 1.判断配置文件中是否已经配置需要脱敏字段
     * 2.判断内容是否有需要脱敏的敏感信息
     * 2.1 没有需要脱敏信息直接返回
     * 2.2 处理: 身份证 ,姓名,手机号敏感信息
     */
	public String hideMarkLog(String logStr) {
        try {
			//1.判断配置文件中是否已经配置需要脱敏字段
            if (StringUtils.isBlank(logStr) || MapUtils.isEmpty(KEY_REG_MAP) || MapUtils.isEmpty(REG_PATTERN_MAP)) {
                return logStr;
            }
            //2.判断内容是否有需要脱敏的敏感信息
            Set<String> charKeys = KEY_REG_MAP.keySet();
            for (String key : charKeys) {
                if (logStr.contains(key)) {
                    String regExp = KEY_REG_MAP.get(key);
                    logStr = matchingAndEncrypt(logStr, regExp, key);
                }
            }
             return logStr;
        } catch (Exception e) {
            logger.info(">>>>>>>>> 脱敏处理异常 ERROR: ", e);
            //如果抛出异常为了不影响流程,直接返回原信息
             return logStr;
        }
    }


   /**
     * 正则匹配对应的对象
     * @param msg
     * @param regExp
     * @return
     */
	private static String matchingAndEncrypt(String msg, String regExp, String key) {
        Pattern pattern = REG_PATTERN_MAP.get(regExp);
		if (pattern == null) {
            logger.info(">>> logger 没有匹配到对应的正则表达式 ");
            return msg;
        }
		Matcher matcher = pattern.matcher(msg);
        int length = key.length() + 5;
        boolean contains = Log4j2Rule.USER_NAME_STR.contains(key);
        String hiddenStr = "";
		while (matcher.find()) {
            String originStr = matcher.group();
            if (contains) {
				// 计算关键词和需要脱敏词的距离小于5。
                int i = msg.indexOf(originStr);
                if (i < 0) {
                    continue;
                }
                int span = i - length;
                int startIndex = span >= 0 ? span : 0;
                String substring = msg.substring(startIndex, i);
                if (StringUtils.isBlank(substring) ||  !substring.contains(key)) {
                    continue;
                }
                hiddenStr = hideMarkStr(originStr);
                msg = msg.replace(originStr, hiddenStr);
         } else {
                hiddenStr = hideMarkStr(originStr);
                msg = msg.replace(originStr, hiddenStr);
            }
            }
        return msg;
    }

    /**
     * 标记敏感文字规则
     * @param needHideMark
     * @return
     */
	private static String hideMarkStr(String needHideMark) {
        if (StringUtils.isBlank(needHideMark)) {
            return "";
        }
        int startSize = 0, endSize = 0, mark = 0, length = needHideMark.length();

        StringBuffer hideRegBuffer = new StringBuffer("(\\S{");
        StringBuffer replaceSb = new StringBuffer("$1");
        if (length > 4) {
            int i = length / 3;
            startSize = i;
            endSize = i;
        } else {
            startSize = 1;
            endSize = 0;
        }

		mark = length - startSize - endSize;
        for (int i = 0; i < mark; i++) {
            replaceSb.append("*");
        }
        hideRegBuffer.append(startSize).append("})\\S*(\\S{").append(endSize).append("})");
        replaceSb.append("$2");
        needHideMark = needHideMark.replaceAll(hideRegBuffer.toString(), replaceSb.toString());
        return needHideMark;
    }


    /**
     * 创建插件
     */
	@PluginFactory
    public static Layout createLayout(@PluginAttribute(value = "pattern") final String pattern,@PluginAttribute(value = "charset") final Charset charset) {
		return new CustomPatternLayout(charset, pattern);
    }

  @Override
    public String toSerializable(LogEvent event) {
        return hideMarkLog(patternLayout.toSerializable(event));
    }

}

import java.util.HashMap;
import java.util.Map;

/**
 * 现在拦截加密的日志有三类:
 * 1,身份证
 * 2,姓名
 * 3,身份证号
 * 加密的规则后续可以优化在配置文件中
 **/
public class Log4j2Rule {

    /**
     * 正则匹配 关键词 类别
     */
	public static Map<String, String> regularMap = new HashMap<>();

	/**
     * TODO  可配置
     * 此项可以后期放在配置项中
     */
	public static final String USER_NAME_STR = "Name,name,联系人,姓名";
    public static final String USER_IDCARD_STR = "empCard,idCard,身份证,证件号";
	public static final String USER_PHONE_STR = "mobile,Phone,phone,电话,手机";
    public static final String USER_address_STR = "address,地址";

	/**
     * 正则匹配,自己根据业务要求自定义
     */
     private static String IDCARD_REGEXP = "(\\d{17}[0-9Xx]|\\d{14}[0-9Xx])";
    private static String USERNAME_REGEXP = "[\\u4e00-\\u9fa5]{2,4}";
    private static String PHONE_REGEXP = "(?<!\\d)(?:(?:1[3456789]\\d{9})|(?:861[356789]\\d{9}))(?!\\d)";

	private static String ADDRESS_REGEXP = "(\\S{3})\\S{2}(\\S*)\\S{2}";

    static {
		regularMap.put(USER_NAME_STR, USERNAME_REGEXP);
        regularMap.put(USER_IDCARD_STR, IDCARD_REGEXP);
        regularMap.put(USER_PHONE_STR, PHONE_REGEXP);
        regularMap.put(USER_address_STR,ADDRESS_REGEXP);
    }

}

自定义脱敏策略
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义jackson注解,标注在属性上
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
    //脱敏策略
    SensitiveStrategy strategy();
}
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 java.io.IOException;
import java.util.Objects;

/**
 * 序列化注解自定义实现
 * JsonSerializer<String>:指定String 类型,serialize()方法用于将修改后的数据载入
 */
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
		private SensitiveStrategy strategy;
		 @Override
       public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
			gen.writeString(strategy.desensitizer().apply(value));
    }

	/**
     * 获取属性上的注解属性
     */
	@Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
		Sensitive annotation = property.getAnnotation(Sensitive.class);
		if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
			this.strategy = annotation.strategy();
            return this;
        }
		return prov.findValueSerializer(property.getType(), property);

    }
}
import java.util.function.Function;

/**
 * 脱敏策略,枚举类,针对不同的数据定制特定的策略
 */
public enum SensitiveStrategy {
    /**
     * 用户名
     */
	USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    /**
     * 身份证
     */
	ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
    /**
     * 手机号
     */
	PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
    /**
     * 地址
     */
     ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));

	private final Function<String, String> desensitizer;

    SensitiveStrategy(Function<String, String> desensitizer) {
        this.desensitizer = desensitizer;
    }

	public Function<String, String> desensitizer() {
        return desensitizer;
    }
}
import lombok.Data;

@Data
public class Person {

	/**
     * 真实姓名
     */
    @Sensitive(strategy = SensitiveStrategy.USERNAME)
    private String realName;

	/**
     * 地址
     */
//    @Sensitive(strategy = SensitiveStrategy.ADDRESS)
    private String address;

	/**
     * 电话号码
     */
//    @Sensitive(strategy = SensitiveStrategy.PHONE)
    private String phoneNumber;

	/**
     * 身份证号码
     */
//    @Sensitive(strategy = SensitiveStrategy.ID_CARD)
    private String idCard;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值