使用原因:在项目中,经常需要在配置文件里配置一些敏感信息,比如数据库用户名和密码,redis、mq的连接信息等。如果直接写明文,很容易造成密码泄露等安全问题。
Jasypt是一个Java库,它允许开发者以最小的改动为项目添加基本的加密功能,而且不需要对密码学的工作原理有深刻的了解。
可以加密所有的Spring环境配置信息,比如系统变量、环境变量、命令行变量、applicationproperties,yaml配置等
com.github.ulisesbocchio
jasypt-spring-boot-starter
2.0.0
com.github.ulisesbocchio
jasypt-spring-boot
3.0.3
需要注意版本对应
jasypt-spring-boot-starter | 依赖的 spring-boot-starter |
---|---|
2.1.0 | 2.0.3.RELEASE 2.2.6.RELEASE |
2.0.0 | 2.0.0.RELEASE 2.2.6.RELEASE |
1.18 | 1.5.10.RELEASE 2.2.6.RELEASE |
1.12 | 1.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程序中实现日志文件脱敏的方法有很多种,以下是其中一些常用的方法:
无论您选择哪种方法,都需要仔细考虑哪些信息应该脱敏以及如何脱敏。请注意,脱敏不一定是完全安全的,因此需要在记录日志时采取其他安全措施,例如加密和访问控制等。
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 REG_PATTERN_MAP = new HashMap<>();
private static Map 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 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 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 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 = "(?
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 类型,serialize()方法用于将修改后的数据载入
*/
public class SensitiveJsonSerializer extends JsonSerializer 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 desensitizer;
SensitiveStrategy(Function desensitizer) {
this.desensitizer = desensitizer;
}
public Function 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;
}