Java数据脱敏

数据脱敏

敏感数据在存储过程中为是否为明文, 分为两种

  • 落地脱敏: 存储的都是明文, 返回之前做脱敏处理
  • 不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密

落地脱敏

这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏

MyBatis插件脱敏

Mybatis插件的相关介绍
Interceptor接口

Mybatis中使用插件, 需要实现拦截器接口org.apache.ibatis.plugin.Interceptor

public interface Interceptor {
    // 需要实现这个方法
    Object intercept(Invocation invocation) throws Throwable;
    
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
        // NOP
    }

}
Invocation类

这个类包含了一些拦截对象的信息

/**
* 拦截类
*/
public class Invocation {
    // 拦截的对象 
    private final Object target;
    // 拦截target中的具体方法, 也就是说Mybatis插件的粒度是精确到方法级别的
    private final Method method;
    // 拦截到的参数
    private final Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    public Object getTarget() {
        return target;
    }

    public Method getMethod() {
        return method;
    }

    public Object[] getArgs() {
        return args;
    }
    // 执行被拦截到的方法, 你可以在执行的前后做一些事情
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }

}
拦截签名

Mybatis插件的粒度是精确到方法级别的, 那么疑问来了, 插件如何知道轮到它工作?

签名机制解决的就是这个问题, 通过在插件接口上使用注解@Intercepts标注来解决这个问题

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * 返回要拦截的方法签名
   *
   * @return 方法签名
   */
  Signature[] value();
}
/**
* 这个注解用于标识方法签名
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * 返回java类型
   *
   * @return java类型
   */
  Class<?> type();

  /**
   * 返回方法名
   *
   * @return 方法名
   */
  String method();

  /**
   * 返回方法参数的java类型
   *
   * @return 方法参数的java类型
   */
  Class<?>[] args();
}
插件的作用域

Mybatis插件能拦截哪些对象/Mybatis插件能在哪个生命周期阶段起作用?

如下

  • ExecutorSQL执行器, 包含了组装参数, 组装结果集到返回值以及执行SQL的过程, 粒度比较粗

    • update: insert, delete, update语句
    • query: query语句
    • flushStatements: 刷新Statement
    • commit: 提交事务
    • rollback: 回滚事务
    • getTransaction: 获取事务
    • close: 关闭事务
    • isClosed: 判断是否事务
  • StatementHandler 用来处理 SQL 的执行过程, 我们可以在这里重写SQL非常常用

    • prepare: 预编译SQL
    • parametersize: 设置参数, 即是SQL的占位符进行赋值
    • batch: 批处理
    • update: insert, delete, update语句
    • query: query语句
  • ParameterHandler 用来处理传入SQL的参数, 我们可以重写参数的处理规则

    • getParameterObject(): 获取参数

    • setParameters(): 设置参数

  • ResultSetHandler 用于处理结果集, 我们可以重写结果集的组装规则

    • handleResultSets(): 处理结果集
    • handleCursorResultSets(): 批量处理结果集
    • handleOutputParameters(): 处理存储过程的参数
MetaObject

Mybatis提供了一个工具类org.apache.ibatis.reflection.MetaObject。它通过反射来读取和修改对象的元信息。我们可以利用它来处理四大对象的一些属性, 这是Mybatis插件开发的一个常用工具类。

  • Object getValue(String name) 根据名称获取对象的属性值, 支持OGNL表达式。
  • void setValue(String name, Object value) 设置某个属性的值。
  • Class<?> getSetterType(String name) 获取setter方法的入参类型。
  • Class<?> getGetterType(String name) 获取getter方法的返回值类型

通常情况下, 我们会选择使用静态方法SystemMetaObject.forObject(Object object)来实例化MetaObject对象

public final class SystemMetaObject {

    public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    // 这里组合一个MetaObject
    public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(new NullObject(), DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());

    private SystemMetaObject() {
        // 防止静态类的实例化
        // Prevent Instantiation of Static Class
    }

    private static class NullObject {
    }

    public static MetaObject forObject(Object object) {
        return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
    }

}
Mybatis插件脱敏
脱敏策略
import java.util.function.Function;

/**
 * 具体策略的函数
 **/
@FunctionalInterface
public interface Desensitizer extends Function<String,String> {

}
脱敏枚举
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 脱敏策略, 枚举类, 针对不同的数据定制特定的策略
 */
@Getter
@AllArgsConstructor
public enum SensitiveStrategy {
    // ------------ 枚举 start ------------
    /**
     * 身份证脱敏: 显示前3位, 后4位
     */
    ID_CARD("identify", "身份证号", str -> DesensitizedUtil.idCardNum(str, 3, 4)),

    /**
     * 银行卡脱敏: 显示前4位, 后4位
     */
    ACCNO("account_no", "账户号", DesensitizedUtil::bankCard),
    /**
     * 手机号脱敏: 显示前3位, 后4位
     */
    PHONE("phone", "手机号", DesensitizedUtil::mobilePhone),

    /**
     * 地址脱敏: 显示前8位
     */
    ADDRESS("address", "地址", str -> DesensitizedUtil.address(str, 8)),

    /**
     * 邮箱脱敏: 邮箱前缀仅显示第一个字母, 前缀其他隐藏
     */
    EMAIL("email", "邮箱", DesensitizedUtil::email),

    BANK_CARD2("bankcard", "银行卡号", str -> {
        return str.trim();
    }),

    /**
     * 银行卡: 显示前4位, 后4位
     */
    BANK_CARD("bankcard", "银行卡号", DesensitizedUtil::bankCard);
    // ------------ 枚举 end ------------

    // ------------ 字段 start ------------
    /**
     * 脱敏类型
     */
    private final String type;

    /**
     * 脱敏类型描述
     */
    private final String desc;

    /**
     * 脱敏策略
     */
    private final Desensitizer desensitizer;
    // ------------ 字段 end ------------
}
脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
    SensitiveStrategy strategy();
}
拦截签名

由于确定要在ORM之后进行拦截, 也就是Mybatis返回结果集的时候做拦截处理, 将数据脱敏, 那么拦截时机就是ResultSetHandler, 拦截的方法就是handleResultSets, 拦截签名代码如下

@Intercepts(@Signature(type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}))
实现Mybatis的Interceptor

下边有两个拦截器, 拦截时期有些不同, 但是都是可以的, 选择启动一个即可

ResultSetHandler#handleResultSets

@Slf4j
@Intercepts(
    @Signature(type = ResultSetHandler.class, method = "handleResultSets",
               args = {Statement.class})
)
public class SensitiveInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        log.debug("进入数据脱敏拦截器...");

        if (result instanceof List) {
            List<?> records = (List<?>) result;
            records.forEach(this::sensitive);
            return records;
        } else if (result instanceof Map) {
            Map<?, ?> records = (Map<?, ?>) result;
            records.values().forEach(this::sensitive);
            return records;
        } else {
            log.info("数据脱敏失败, 脱敏的数据: {}", result);
        }
        return result;
    }


    /**
	* 数据脱敏
	* @param source 要脱敏的数据
	*/
    private void sensitive(Object source) {
        // 拿到返回值类型
        Class<?> sourceClass = source.getClass();
        // 初始化返回值类型的 MetaObject
        MetaObject metaObject = SystemMetaObject.forObject(source);
        // 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理
        Stream.of(sourceClass.getDeclaredFields())
            .filter(field -> field.isAnnotationPresent(Sensitive.class))
            .forEach(field -> doSensitive(metaObject, field));
    }

    /**
	* @param metaObject metaObject工具类
	* @param field 脱敏字段
	*/
    private void doSensitive(MetaObject metaObject, Field field) {
        // 拿到属性名
        String name = field.getName();
        // 获取属性值
        Object value = metaObject.getValue(name);
        // 只有字符串类型才能脱敏  而且不能为null
        if (String.class == metaObject.getGetterType(name) && value != null) {
            String str = (String) value;
            Sensitive sensitive = field.getAnnotation(Sensitive.class);
            // 获取对应的脱敏策略 并进行脱敏
            SensitiveStrategy type = sensitive.strategy();
            Object o = type.getDesensitizer().apply(str);
            // 把脱敏后的值塞回去
            metaObject.setValue(name, o);
        }
    }
}

Executor#query

@Slf4j
@Component
@Intercepts({
    // 拦截query
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SensitiveInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.debug("进入数据脱敏拦截器前...");
        // 脱敏入库
			Object result = invocation.proceed();
        // 数据
        Object result = invocation.proceed();
        log.debug("进入数据脱敏拦截器...");

        if (result instanceof List) {
            List<?> records = (List<?>) result;
            records.forEach(this::sensitive);
            return records;
        } else if (result instanceof Map) {
            Map<?, ?> records = (Map<?, ?>) result;
            records.values().forEach(this::sensitive);
            return records;
        } else {
            log.info("数据脱敏失败, 脱敏的数据: {}", result);
        }
        return result;
    }


    /**
	* 数据脱敏
	* @param source 要脱敏的数据
	*/
    private void sensitive(Object source) {
        // 拿到返回值类型
        Class<?> sourceClass = source.getClass();
        // 初始化返回值类型的 MetaObject
        MetaObject metaObject = SystemMetaObject.forObject(source);
        // 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理
        Stream.of(sourceClass.getDeclaredFields())
            .filter(field -> field.isAnnotationPresent(Sensitive.class))
            .forEach(field -> this.doSensitive(metaObject, field));
    }

    /**
	* @param metaObject metaObject工具类
	* @param field 脱敏字段
	*/
    private void doSensitive(MetaObject metaObject, Field field) {
        // 拿到属性名
        String name = field.getName();
        // 获取属性值
        Object value = metaObject.getValue(name);
        // 只有字符串类型才能脱敏  而且不能为null
        if (String.class == metaObject.getGetterType(name) && value != null) {
            String str = (String) value;
            Sensitive sensitive = field.getAnnotation(Sensitive.class);
            // 获取对应的脱敏策略 并进行脱敏
            SensitiveStrategy type = sensitive.strategy();
            Object o = type.getDesensitizer().apply(str);
            // 把脱敏后的值塞回去
            metaObject.setValue(name, o);
        }
    }
}

Jackson序列化中脱敏

脱敏策略

同上

脱敏枚举

同上

ORM查询出来后需要部分逻辑处理, 如果此时脱敏了, 那么就没法处理该逻辑, 脱敏放置在JSON序列化后较为合适

自定义脱敏序列化
/**
 * 自定义脱敏序列化
 * JsonSerializer<String>: 指定String 类型
 * serialize()方法用于将修改后的数据载入
 */
@Slf4j
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveStrategy strategy;

    /**
     * 执行脱敏序列化逻辑
     */
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        try {
            SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class);
            // 开启了脱敏
            if (ObjectUtil.isNotNull(sensitiveService) && sensitiveService.isSensitive()) {
                // 用指定的脱敏策略脱敏
                gen.writeString(this.strategy.desensitizer().apply(value));
            } else {
                // 不脱敏
                gen.writeString(value);
            }
        } catch (BeansException e) {
            log.error("脱敏策略未指定, 将不进行脱敏操作, 待脱敏数据为: {}", e.getMessage());
            gen.writeString(value);
        }
    }

    /**
     * 获取实体类上的@Sensitive注解并根据条件初始化对应的JsonSerializer对象
     */
    @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);
    }
}

Jackson相关注解和使用参考Jackson 进阶之自定义序列化器

脱敏注解
/**
 * 自定义jackson注解, 标注在属性上
 */
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
    SensitiveTypeEnum strategy();
}

@JacksonAnnotationsInside: 将多个注解组合到一起, 这里将把上面自定义的JSON序列化和脱敏策略绑定到一起

@JsonSerialize: 声明使用自定义的序列化方法SensitiveJsonSerializer

JackSon相关注解和使用参考Jackson 进阶之自定义序列化器

使用如下
@Data
public class User {
    /**
     * 电话号码
     */
    @Sensitive(strategy = SensitiveStrategy.PHONE)
    private String phoneNumber;

    // ......
}

Mybatis插件脱敏和Jackson序列化脱敏对比

相对于Mybatis插件脱敏, Jackson脱敏则是更加好

假设查询列中有个手机号, ORM之后需要对手机号进行一些判断, 但是手机号已经脱敏, 不足以用于判断, 那么此时就是很麻烦的

而JSON之后序列化则是解决了这个问题, ORM之后手机号还是没有脱敏的, 此时可以继续对手机号做业务逻辑判断, 而将数据返回给前端之前, Spring会默认执行JSON序列化, 而此时进行脱敏, 那么最终返回给前端的效果还是脱敏的

在这里插入图片描述

不落地脱敏

指的是数据库中存储的是密文数据, 相对于上述明文存储的数据, 安全性大大增强, 即是发生了拖库, 黑客获取到用户的敏感信息也是加密的, 也没法进一步损害客户利益

配置脱敏

介绍

Java解密工具类jasypt实现脱敏

该工具提供了单密钥对称加密非对称加密两种脱敏方式

单密钥对称加密: 一个密钥加盐, 可以同时用作内容的加密和解密依据

非对称加密: 公钥和私钥两个密钥, 公钥加密, 私钥解密

引入依赖

引入jasypt依赖实现单密钥对称加密

 <!--配置文件加密-->
 <dependency>
     <groupId>com.github.ulisesbocchio</groupId>
     <artifactId>jasypt-spring-boot-starter</artifactId>
     <version>2.1.0</version>
 </dependency>

总配置

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--配置文件加密-->
    <dependency>
        <groupId>com.github.ulisesbocchio</groupId>
        <artifactId>jasypt-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>

    <!-- druid数据源驱动 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

</dependencies>
yaml配置

脱敏的一些配置

# 密钥对安全性要求比较高, 不建议直接显示在项目中, 可以通过启动时-D参数注入, 或者放在配置中心
# 例如password, prefix, suffix, algorithm都简易-D参数注入, 最低最低要求password要通过-D注入
# 密钥相关配置
jasypt:
  encryptor:
    # 秘钥配置项, 密钥不支持中文
    password: whitebrocade
    property:
      # 前缀, 后缀 
      # 和要加密的元素拼接, 例如加密值为12345678, 12是前缀, 78是后缀, 3456是特有的值 那么配置了前后缀就是12345678  对拼接的字符串进行加密
      prefix: "12"
      suffix: "78"
    # 加密算法, 默认是PBEWITHMD5ANDDES
    algorithm: PBEWithMD5AndDES

例如启动程序命令如下

java -jar -Djasypt.encryptor.password=whitebrocad jasypt-demo.jar

java -jar -Djasypt.encryptor.password=whitebrocad -Djasypt.encryptor.property.prefix="12" -Djasypt.encryptor.property.suffix="78" -Djasypt.encryptor.algorithm=PBEWithMD5AndDES jasypt-demo.jar
使用流程

假设现在要对MySQL的密码进行进行脱敏

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
	# 对MySQL的密码进行加密脱敏
    password: 12345678
    
jasypt:
  encryptor:
    password: whitebrocade
    property:
      prefix: "12"
      suffix: "78"
    algorithm: PBEWithMD5AndDES

首先明确的是, 12345678是不能直接显示, 所以这里的password是一个加密值, 需要提前生成

生成方式如下

  1. 代码API生成

    • @Autowired
      private StringEncryptor stringEncryptor;
      
      public void encrypt(String content) {
          String encryptStr = stringEncryptor.encrypt(content);
          System.out.println("加密后的内容: " + encryptStr);
      }
      
  2. Java命令生成

    • java -cp  E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="12345678" password=whitebrocade algorithm=PBEWithMD5AndDES
      
      • E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar: 为jasypt核心jar包: 这个路径是你jasypt的在maven中保存的路径, 根据自己的存储情况而定
      • input: 待加密文本, 这里传入12345678
        • password: 秘钥, 为whitebrocade, 秘钥随意, 需要注意秘钥的密码强度以及秘钥的保密
      • algorithm: 为使用的加密算法, 建议不要用默认的加密算法, 加大破解难度

OUTPUT是加密后的密码, 注意了, 每次生成的效果都不一样, 但是都是可以解密的
在这里插入图片描述

将生成的密码0jSWFsiP9ZVKg3USneAl76beGfuovVlG复制到yaml中, 如下

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    # 对MySQL
    password: ENC(0jSWFsiP9ZVKg3USneAl76beGfuovVlG)

jasypt:
  encryptor:
    password: whitebrocade
    property:
      prefix: "12"
      suffix: "78"
    algorithm: PBEWithMD5AndDES

表示一个加密操作, 那么此时需要加密的内容就是prefix+phone+suffix拼接成的内容, 即ENC(prefix+phone+suffix), 这里的前缀和后缀起了一个盐值的作用

ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候jasypt将保持原值,不进行解密

相关测试

相关相关测试代码

@Controller
public class MyTestController {
    @Autowired
    private StringEncryptor stringEncryptor;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @ResponseBody
    @RequestMapping("/test")
    public void encrypt(){
        String content = "12345678";
        String encryptStr = stringEncryptor.encrypt(content);
        System.out.println("加密后的内容:" + encryptStr);
        String decryptStr = stringEncryptor.decrypt(encryptStr);
        System.out.println("解密后的内容:" + decryptStr);
        this.list();
    }

    /**
     * 查询数据库信息
     */
    public void list(){
        // 数据库中有t1表, 并且有数据
        String sql="select * from t1";
        List<Map<String,Object>> list_map = jdbcTemplate.queryForList(sql);
        System.out.println("list_map = " + list_map);
    }
}

运行结果如下, 发现确实可以连接数据库

在这里插入图片描述

敏感字段脱敏

生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理

AOP脱敏

入库前的脱敏, 查询时的反向解密, 一前一后适合使用AOP来实现

这里是全脱敏, 不支持模糊查询!

模糊查询可以通过分词密文映射表查询, 后续再说

自定义注解

自定义两个注解@EncryptField@EncryptMethod分别用在字段属性和方法

@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {

    String[] value() default "";
}
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {

    String type() default ENCRYPT;
}
定义常量
public interface EncryptConstant {
    // 加密
    String ENCRYPT = "encrypt";

    // 解密
    String DECRYPT = "decrypt";
}
切面类
@Slf4j
@Aspect
@Component
public class EncryptHandler {
    @Autowired
    private StringEncryptor stringEncryptor;

    @Pointcut("@annotation(com.whitebrocade.jasyptdemo.demos.anno.EncryptMethod)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        // 加密
        this.encrypt(joinPoint);
        // 解密
        Object decrypt = this.decrypt(joinPoint);
        return decrypt;
    }

    /**
     * 加密
     */
    public void encrypt(ProceedingJoinPoint joinPoint) {

        try {
            Object[] objects = joinPoint.getArgs();
            if (objects.length != 0) {
                for (Object o : objects) {
                    if (o instanceof String) {
                        this.encryptStr(o);
                    } else {
                        this.handler(o, ENCRYPT);
                    }
                    //TODO 其余类型自己看实际情况加
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解密
     */
    public Object decrypt(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            Object obj = joinPoint.proceed();
            if (obj != null) {
                if (obj instanceof String) {
                    this.decryptStr(obj);
                } else {
                    result = this.handler(obj, DECRYPT);
                }
                // TODO 其余类型自己看实际情况加
            }
        } catch (Throwable e) {
            log.error("解密失败", e);
            throw new RuntimeException();
        }
        return result;
    }

    /**
     * 解密或者解密
     * @param obj 要加密/解密的元素
     * @param type 加密/解密
     * @return 加密/解密后的内容
     */
    private Object handler(Object obj, String type) throws IllegalAccessException {
        if (Objects.isNull(obj)) {
            return null;
        }
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 获取EncryptField标识的注解
            boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
            if (hasSecureField) {
                field.setAccessible(true);
                String realValue = (String) field.get(obj);
                String value;
                if (DECRYPT.equals(type)) {
                    value = stringEncryptor.decrypt(realValue);
                } else {
                    value = stringEncryptor.encrypt(realValue);
                }
                field.set(obj, value);
            }
        }
        return obj;
    }

    /**
     * 字符串内容加密
     * @param realValue 字符串
     * @return 加密后的字符串
     */
    public String encryptStr(Object realValue) {
        String value = null;
        try {
            value = stringEncryptor.encrypt(String.valueOf(realValue));
        } catch (Exception e) {
            log.error("加密失败", e);
            return value;
        }
        return value;
    }

    /**
     * 字符串内容解密
     * @param realValue 要解密的字符串
     * @return 解密后的字符串
     */
    public String decryptStr(Object realValue) {
        String value = String.valueOf(realValue);
        try {
            value = stringEncryptor.decrypt(value);
        } catch (Exception e) {
            log.error("解密失败", e);
            return value;
        }
        return value;
    }
}
实体类
@Data
public class UserVo implements Serializable {
    private Long userId;

    @EncryptField
    private String mobile;

    @EncryptField
    private String address;

    private String age;
}
测试类
@RestController
public class MyTestController {    
    @EncryptMethod
    @PostMapping(value = "/test")
    @ResponseBody
    public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) {
        System.out.println("前端传入参数user:  " + JSONUtil.toJsonStr(user));
        return this.insertUser(user, name);
    }

    private UserVo insertUser(UserVo user, String name) {
        System.out.println("加密后的数据:user" + JSONUtil.toJsonStr(user));
        System.out.println("加密后的数据:name" + name);
        return user;
    }
}
测试

测试数据
在这里插入图片描述

测试结果

在这里插入图片描述

总结

发现前端传递的数据接受的时候就加密了, 如果需要在业务中做判断, 那么是比较麻烦的

Mybatis插件加密
  • 切入时机: Mybatis设置参数时对敏感数据进行加密
  • 解密时机: Mybatis返回结果集的时候
前期准备
相关SQL
CREATE TABLE student(
	id VARCHAR(50) COMMENT '学生ID',
	sname VARCHAR(100) COMMENT '学生姓名',
	classId VARCHAR(100) COMMENT '班级ID',
	birthday VARCHAR(100) COMMENT '学生生日',
	email VARCHAR(100) COMMENT '学生电子邮箱'
);


INSERT INTO student(id,sname,classId,birthday,email)
VALUES(1,'tom',101,1016,'1@163.com'),(2,'jack',101,511,'2@163.com'),
	  (3,'lucy',101,1016,'3@163.com'),(4,'amy',103,615,'4@163.com');
pom配置
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--配置文件加密-->
    <dependency>
        <groupId>com.github.ulisesbocchio</groupId>
        <artifactId>jasypt-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>

    <!-- druid数据源驱动 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
    </dependency>

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>

    <!-- Mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
</dependencies>
相关代码
yaml配置
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    # ENC中的值是可以不断替换的
    password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)

jasypt:
  encryptor:
    password: whitebrocade


mybatis:
  mapper-locations: classpath:mapper/*.xml

# application.yml
logging:
  level:
    com.whitebrocade.jasyptdemo.demos: debug

# -----------------
# 加密配置
whitebrocade:
  crypto:
    secret-key: whitebrocade1234
    algorithm: AES
注解
import java.lang.annotation.*;
/**
 * 该注解有两种使用方式
 * 1 配合@SensitiveData加在类中的字段上
 * 2 直接在Mapper中的方法参数上使用
 **/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
import java.lang.annotation.*;
/**
 * 该注解定义在类上
 * 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
 * 这个注解要配合EncryptTransaction注解
 **/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
import java.lang.annotation.*;
/**
 * 该注解有两种使用方式
 * 1 配合@SensitiveData加在类中的字段上
 * 2 直接在Mapper中的方法参数上使用
 **/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
实体类
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.Data;

import java.io.Serializable;

/**
 * 与数据库表结构相同
 */
@Data
@SensitiveData
public class StudentInfo implements Serializable {
    private String id;
    @EncryptTransaction
    private String sname;
    private String classId;
    private String birthday;
    private String email;
}
Mapper
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.domain.StudentInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface StudentMapper {
    /**
     * 根据学生ID查询学生信息
     */
    StudentInfo getInfo(@EncryptTransaction String id);

    /**
     * 根据姓名查用户
     */
    StudentInfo getInfoByName(@EncryptTransaction @Param("sname") String sname);

    /**
     * 插入新学生信息
     */
    void insertInfo(@EncryptTransaction StudentInfo studentInfo);

    /**
     * 根据ID删除学生信息
     */
    int deleteById(int id);

    /**
     * 根据id修改学生信息
     */
    int updateById(@EncryptTransaction StudentInfo studentInfo);

    /**
     * 查询全部学生信息
     */
    List<StudentInfo> selectAll();
}

Mapper的xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper">

    <select id="getInfo" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
        select *
        from student
        where id=#{id}
    </select>

    <select id="getInfoByName" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
        select *
        from student
        where sname=#{sname}
    </select>

    <insert id="insertInfo" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
        insert into student(id,sname,classId,birthday,email)
        values (#{id},#{sname},#{classId},#{birthday},#{email});
    </insert>

    <delete id="deleteById">
        delete
        from student
        where id=#{id}
    </delete>

    <update id="updateById" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
        update student
        set sname = #{sname},classId = #{classId},
            birthday = #{birthday}, email = #{email}
        where id = #{id}
    </update>

    <select id="selectAll" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
        select *
        from student
    </select>
</mapper>
加密拦截类(核心)
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;


/**
 * 加密拦截
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters",
                args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {
    @Autowired
    private Encoder encoder;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        // 取出参数
        // sname -> abc pararm1 -> abc
        Object parameterObject = parameterField.get(parameterHandler);

        // Class<ParameterHandler> handlerClass = ParameterHandler.class;
        Field mappedStatementFiled = parameterHandler.getClass().getDeclaredField("mappedStatement");
        mappedStatementFiled.setAccessible(true);

        MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler);
        // 方法全限定类名 com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper.getInfoByName
        String methodFullClassName = mappedStatement.getId();
        // 获取方法所在的类对象,这里是com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper
        String mapperClassName = methodFullClassName.substring(0, methodFullClassName.lastIndexOf('.'));
        Class<?> mapperClass = Class.forName(mapperClassName);
        // 简单方法名 getInfoByName
        String methodSimpleName = methodFullClassName.substring(methodFullClassName.lastIndexOf('.') + 1);

        // 通过方法名找到指定的Method
        Method[] methods = mapperClass.getDeclaredMethods();
        Method method = null;
        for (Method m : methods) {
            if (m.getName().equals(methodSimpleName)) {
                method = m;
                break;
            }
        }

        // 找到@EncryptTransaction的Mapper方法
        List<String> paramNames = null;
        if (ObjUtil.isNotNull(method)) {
            // 获取参数上的所有注解
            Annotation[][] pa = method.getParameterAnnotations();
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < pa.length; i++) {
                for (Annotation annotation : pa[i]) {
                    if (paramNames == null) {
                        paramNames = new ArrayList<>();
                    }

                    if (annotation instanceof EncryptTransaction) {
                        // 如果参数有@EncryptTransaction注解,则将参数名添加到集合中
                        paramNames.add(parameters[i].getName());
                    }

                    // 如果有@Param注解,则将参数名添加到集合中
                    if (annotation instanceof Param) {
                        paramNames.add(parameters[i].getName());
                        continue;
                    }
                }
            }
        }

        // 外界传入参数不为空
        if (ObjUtil.isNotNull(parameterObject)) {
            String entityClassName = null;
            // 之所以要分成几种类型,是因为查看通过返回值获取类型,增改可以传递的实体类获取类型,而删除传递为id, 返回值也不是我们所需要的
            // 查询类型
            if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                // 获取实体类的类名
                // com.whitebrocade.jasyptdemo.demos.domain.StudentInfo
                entityClassName = mappedStatement.getResultMaps().get(0).getType().getName();
            } else if(mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)
                    || mappedStatement.getSqlCommandType().equals(SqlCommandType.UPDATE)) { // 增,改都是获取注解上的类型
                Annotation[][] pa = method.getParameterAnnotations();
                Parameter[] parameters = method.getParameters();

                for (int i = 0; i < pa.length; i++) {
                    for (Annotation annotation : pa[i]) {
                        // 只有@EncryptTransaction注解的参数,才会被加密
                        if (annotation instanceof EncryptTransaction) {
                            entityClassName = parameters[i].getType().getTypeName();
                        }
                    }
                }
            } else if (mappedStatement.getSqlCommandType().equals(SqlCommandType.DELETE)) { // 通常来说,都是根据id删除,并且id类型都是int, long为主
                // 直接放行
                return invocation.proceed();
            }

            Class<?> entityClass = Class.forName(entityClassName);
            // 对类字段进行加密
            // 校验该实例的类是否被@SensitiveData所注解
            SensitiveData sensitiveData = AnnotationUtil.getAnnotation(entityClass, SensitiveData.class);
            if (ObjUtil.isNotNull(sensitiveData)) {
                //取出当前当前类所有字段,传入加密方法
                Field[] declaredFields = entityClass.getDeclaredFields();
                // 对外界参数进行加密
                parameterObject = this.encrypt(declaredFields, parameterObject);
            }

            // 将加密后的参数代替原来的参数
            if (CollUtil.isNotEmpty(paramNames)) {
                // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
                Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
                boundSqlField.setAccessible(true);
                PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
                // 改写的参数设置到原parameterHandler对象
                parameterField.set(parameterHandler, parameterObject);
                parameterHandler.setParameters(ps);
            }
        }

        // 执行查询
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 加密
     * @param declaredFields 对象的字段
     * @param paramsObject Mybatis传入参数
     * @return 加密后的对象
     */
    private Object encrypt(Field[] declaredFields, Object paramsObject) {
        // 取出所有被EncryptTransaction注解的字段
        for (Field field : declaredFields) {
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);

                // 字段名
                String paramName = field.getName();
                Object obj = null;
                Map<String, Object> map = null;
                if (paramsObject instanceof String) {
                    // 表示只传有一个参数
                    obj = (String) paramsObject;
                } else if (paramsObject instanceof Map) {
                    map = (Map<String, Object>) paramsObject;
                    // 获取该字段对应的参数,非空就跳过
                    obj = map.get(paramName);
                } else { // 如果是具体的实体对象,就转换成map
                    map = BeanUtil.beanToMap(paramsObject);
                    // 获取该字段对应的参数
                    obj = map.get(paramName);
                }
                // 为空跳过
                if (Objects.isNull(obj)) {
                    continue;
                }

                // 字段类型
                Class<?> paramClass = field.getType();
                // 暂时只实现String类型的加密
                // 如果字段类型是字符串,且传入参数是类型, 那么就转换成字符串
                if (paramClass == String.class && obj instanceof String) {
                    String value = (String) obj;
                    //加密
                    try {
                        // 加密
                        String encryptStr = encoder.encrypt(value);
                        if (paramsObject instanceof String) {
                            paramsObject = encryptStr;
                            return encryptStr;
                        } else if (paramsObject instanceof Map) {
                            map.put(paramName, encryptStr);
                        } else { // 实体类对象
                            map.put(paramName, encryptStr);
                            paramsObject = BeanUtil.toBean(map, paramsObject.getClass());
                        }
                    } catch (Exception e) {
                        log.error("加密错误", e);
                        throw new RuntimeException("加密错误", e);
                    }
                }
            }
        }
        return paramsObject;
    }
}
解密拦截(核心)
import cn.hutool.core.util.ObjUtil;
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;

/**
 * 解密拦截
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryInterceptor implements Interceptor {
    @Autowired
    private Encoder encoder;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        // 基于selectList
        if (resultObject instanceof ArrayList) {
            @SuppressWarnings("unchecked")
            ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject;
            if (! CollectionUtils.isEmpty(resultList) && this.needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    this.decrypt(result);
                }
            }
            // 基于selectOne
        } else {
            if (this.needToDecrypt(resultObject)) {
                this.decrypt(resultObject);
            }
        }
        return resultObject;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 是否需要加密,通过判断实体类是否添加@SensitiveData注解
     * @param object 实体类
     * @return 有添加@SensitiveData注解返回true, 没有返回false
     */
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return ObjUtil.isNotNull(sensitiveData);
    }

    /**
     * 解密
     * @param result 要解密的对象
     * @return 解密后的对象
     * @param <T> 对象的类型
     * @throws IllegalAccessException
     */
    private <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            // 取出所有被EncryptTransaction注解的字段
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(result);
                // String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    // 对注解的字段进行逐一解密
                    try {
                        String decryptStr = encoder.decrypt(value);
                        field.set(result, decryptStr);
                    } catch (Exception e) {
                        log.error("解密失败", e);
                        throw new RuntimeException("解密失败");
                    }
                }
            }
        }
        return result;
    }
}
加密/解密辅助类
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;


/**
 * 脱敏加密/解密
 * 加密模式为ECB, 所以不支持加盐
 */
@Data
@Slf4j
@Component
public class Encoder {
    /**
     * 密钥建议就是从参数中读取
     */
    @Value("${whitebrocade.crypto.secret-key}")
    private Object secretKey;

    /**
     * 对称加密的算法
     */
    @Value("${whitebrocade.crypto.algorithm}")
    private Object algorithm;

    /**
     * 缓存
     */
    private SymmetricCrypto crypto;

    /**
     * 获取SymmetricCrypto
     */
    private SymmetricCrypto getSymmetricCrypto() {
        if (ObjUtil.isNotNull(crypto)) {
            return crypto;
        }
        this.initSymmetricCrypto();
        return crypto;
    }

    /**
     * 初始化SymmetricCrypto
     */
    private void initSymmetricCrypto() {
        // 如果KEY的长度不为16, 24, 32那么提示错误
        // 密钥要求程度就如此,遵守它即可,不用多想
        String tempSecretKey = String.valueOf(secretKey);

        if (! (tempSecretKey.length() == 16 || tempSecretKey.length() == 24 || tempSecretKey.length() == 32)) {
            throw new RuntimeException("secret-key字符串的长度必须为16,24,32长度");
        }

        // 获取加密算法
        String tempAlgorithm = String.valueOf(algorithm);
        SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.valueOf(tempAlgorithm);
        if (ObjUtil.isNull(symmetricAlgorithm)) {
            throw new RuntimeException("symmetricAlgorithm算法不存在,算法名区分大小写,请参考cn.hutool.crypto.symmetric.SymmetricAlgorithm中算法进行配置");
        }
        // AES加密
        byte[] bytes = SecureUtil
                .generateKey(symmetricAlgorithm.getValue(), tempSecretKey.getBytes())
                .getEncoded();
        // 构建
        crypto = new SymmetricCrypto(symmetricAlgorithm, bytes);
    }

    /**
     * 加密
     */
    public String encrypt(String content) {
        SymmetricCrypto crypto = this.getSymmetricCrypto();
        String encryptStr = crypto.encryptBase64(content);
        return encryptStr;
    }

    /**
     * 解密
     */
    public String decrypt(String content) {
        SymmetricCrypto crypto = this.getSymmetricCrypto();
        String decryptStr = crypto.decryptStr(content);
        return decryptStr;
    }
}
Controller
@RestController
public class MyTestController {  
    @Autowired
    private StudentMapper studentMapper;

    @ResponseBody
    @RequestMapping("/getInfo")
    public void getInfo(@Param("id") String id) {
        StudentInfo stu = studentMapper.getInfo(id);
        System.out.println("stu = " + stu);
    }

    // http://localhost:8080/test5?sname=tom
    @ResponseBody
    @RequestMapping("/getInfoByName")
    public StudentInfo getInfoByName(@Param("sname") String sname) {
        StudentInfo stu = studentMapper.getInfoByName(sname);
        System.out.println("stu = " + stu);
        return stu;
    }

    @ResponseBody
    @PostMapping("/insertInfo")
    public StudentInfo insertInfo(@RequestBody StudentInfo studentInfo) {
        studentMapper.insertInfo(studentInfo);
        return studentInfo;
    }

    @ResponseBody
    @PostMapping("/updateById")
    public StudentInfo updateById(@RequestBody StudentInfo studentInfo) {
        studentMapper.updateById(studentInfo);
        return studentInfo;
    }

    @ResponseBody
    @GetMapping("/selectAll")
    public List<StudentInfo> selectAll() {
        return studentMapper.selectAll();
    }

    @ResponseBody
    @DeleteMapping("/deleteById")
    public void deleteById(int id) {
        studentMapper.deleteById(id);
    }
}

需要注意的是,上述代码中不要引入Mybatis-plus,还未适配

jasypt对盐值,密钥等相关进行加密

再补充一下,既然我们直接将盐值,密钥等写入yaml中不安全,那么我们就可以借助之前的jasypt对这些信息进行加密,也就实现了密钥轮替,安全性提高了

  • 对Myabtis加密脱敏的密钥加密
java -cp  E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="whitebrocade1234" password=whitebrocade algorithm=PBEWithMD5AndDES

在这里插入图片描述

对Myabtis加密脱敏所使用的算法进行加密

java -cp  E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="AES" password=whitebrocade algorithm=PBEWithMD5AndDES

在这里插入图片描述

修改后的yaml配置如下

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
	# url其实加密都不错的
    url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    # ENC中的值是可以不断替换的
    password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)

mybatis:
  mapper-locations: classpath:mapper/*.xml

# application.yml
logging:
  level:
    com.whitebrocade.jasyptdemo.demos: debug

# -----------------
# Mybatis的脱敏加密配置
whitebrocade:
  crypto:
    secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=)
    algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==)

# 这里的password建议外部传入
jasypt:
  encryptor:
    password: whitebrocade

效果如下, 正常查询能显示
在这里插入图片描述

实际中数据库就是加密了
在这里插入图片描述

Sharding-JDBC脱敏

在这里插入图片描述

  • 数据源配置:是指DataSource的配置。

  • 加密器配置:是指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5。用户还可以通过实现ShardingSphere提供的接口,自行实现一套加解密算法

    • 后续我们实现ShardingSphere提供的接口, 通过SPI机制专配,SPI相关介绍见–>JDK和Spring的SPI机制原理分析
  • 脱敏表配置:用于告诉ShardingSphere数据表里哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)

  • 查询属性的配置:当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。

新增SPI配置
  1. 新增resources/META-INF/services目录下

  2. 该目录下新增配置,配置文件名为org.apache.shardingsphere.encrypt.strategy.spi.Encryptor

  3. 配置文件里的内容,放入自定义的加密策略的类的全路径,和要使用官方内置的加密策略的类的全路径

    • 内置的加密策略为:AESEncryptorMD5Encryptor
    • 自定义加密策略为:CustomEncryptor
    org.apache.shardingsphere.encrypt.strategy.impl.AESEncryptor
    org.apache.shardingsphere.encrypt.strategy.impl.MD5Encryptor
    com.whitebrocade.jasyptdemo.demos.encryptor.CustomEncryptor
    com.whitebrocade.jasyptdemo.demos.encryptor.CustomQueryAssistedEncryptor
    
相关SQ
CREATE TABLE `t_user` (
  `user_id` int NOT NULL COMMENT '用户Encoder {id',
  `user_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名称',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码明文',
  `password_encrypt` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码密文',
  `password_assisted` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '辅助查询列',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;Encoder {
实体类
@Data
public class UserEntity {
    private Integer userId;
    private String userName;
    private String password;
    private String passwordEncrypt;
    private String passwordAssisted;
}
Mapper
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface UserMapper {

    @Insert("insert into t_user(user_id,user_name,password) values(#{userId},#{userName},#{password})")
    void insertUser(UserEntity userEntity);

    @Select("select * from t_user where user_name=#{userName} and password=#{password}")
    @Results({
            @Result(column = "user_id", property = "userId"),
            @Result(column = "user_name", property = "userName"),
            @Result(column = "password", property = "password"),
            @Result(column = "password_assisted", property = "passwordAssisted")
    })
    List<UserEntity> getUserInfo(@Param("userName") String userName, @Param("password") String password);
}
yaml
spring:
  # 分库分表下的脱敏
  shardingsphere:
    datasource:
      names: demo
      demo:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1.101:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
        username: root
        # ENC中的值是可以不断替换的
        password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)
    encrypt:
      encryptors:
        my-encryptor:
          # 加密算法类型
          type: CustomEncryptor
#          type: CustomQueryAssistedEncryptor
      # 要加密的表
      tables:
        t_user:
          columns:
            password:
              # 真实列
              plain-column: password
              # 加密列
              cipher-column: password_encrypt
              # 辅助查询列
              # assisted-query-column: password_assisted
              # 加密算法
              encryptor: my-encryptor
    # 查询是否使用密文列 ture显示cipher-column false显示plain-column
    props:
      query.with.cipher.column: true

# 加密配置
whitebrocade:
  crypto:
    # 密钥,16/24/32字节
    secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=)
    algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==)

# Mybatis XML配置
mybatis:
  mapper-locations: classpath:mapper/*.xml

# application.yml
logging:
  level:
    com.whitebrocade.jasyptdemo.demos: debug

# 加密
jasypt:
  encryptor:
    password: whitebrocade
加密/解密辅助类
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;


/**
 * 脱敏加密/解密
 * 加密模式为ECB, 所以不支持加盐
 */
@Data
@Slf4j
@Component
pEncoder {ublic class Encoder {
    /**
     * 密钥建议就是从参数中读取
     */
    @Value("${whitebrocade.crypto.secret-key}")
    private Object secretKey;

    /**
     * 对称加密的算法
     */
    @Value("${whitebrocade.crypto.algorithm}")
    private Object algorithm;

    /**
     * 缓存
     */
    private SymmetricCrypto crypto;

    /**
     * 获取SymmetricCrypto
     */
    private SymmetricCrypto getSymmetricCrypto() {
        if (ObjUtil.isNotNull(crypto)) {
            return crypto;
        }
        this.initSymmetricCrypto();
        return crypto;
    }

    /**
     * 初始化SymmetricCrypto
     */
    private void initSymmetricCrypto() {
        // 如果KEY的长度不为16, 24, 32那么提示错误
        // 密钥要求程度就如此,遵守它即可,不用多想
        String tempSecretKey = String.valueOf(secretKey);

        if (! (tempSecretKey.length() == 16 || tempSecretKey.length() == 24 || tempSecretKey.length() == 32)) {
            throw new RuntimeException("secret-key字符串的长度必须为16,24,32长度");
        }

        // 获取加密算法
        String tempAlgorithm = String.valueOf(algorithm);
        SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.valueOf(tempAlgorithm);
        if (ObjUtil.isNull(symmetricAlgorithm)) {
            throw new RuntimeException("symmetricAlgorithm算法不存在,算法名区分大小写,请参考cn.hutool.crypto.symmetric.SymmetricAlgorithm中算法进行配置");
        }
        // AES加密
        byte[] bytes = SecureUtil
                .generateKey(symmetricAlgorithm.getValue(), tempSecretKey.getBytes())
                .getEncoded();
        // 构建
        crypto = new SymmetricCrypto(symmetricAlgorithm, bytes);
    }

    /**
     * 加密
     */
    public String encrypt(String content) {
        SymmetricCrypto crypto = this.getSymmetricCrypto();
        String encryptStr = crypto.encryptBase64(content);
        return encryptStr;
    }

    /**
     * 解密
     */
    public String decrypt(String content) {
        SymmetricCrypto crypto = this.getSymmetricCrypto();
        String decryptStr = crypto.decryptStr(content);
        return decryptStr;
    }
}
自定义加密器CustomEncryptor
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.whitebrocade.jasyptdemo.demos.service.Encoder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.encrypt.strategy.spi.Encryptor;

import java.util.Properties;

/**
 * 该种加密方式特点: 相同数据存储内容一样
 */
@Slf4j
@Getter
@Setter
public class CustomEncryptor implements Encryptor {
    /**
     * 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化
     */
    private Encoder encoder;

    /**
     * 算法策略类型
     */
    private static final String TYPE = "CustomEncryptor";

    private Properties properties = new Properties();

    @Override
    public void init() {
        Encoder tmepEncoder = SpringUtil.getBean(Encoder.class);
        if (ObjUtil.isNull(tmepEncoder)) {
            log.error("Spring容器中不存在Encoder类型的Bean");
            throw new RuntimeException("Spring容器中不存在Encoder类型的Bean");
        }
        encoder = tmepEncoder;
    }

    /**
     * 加密
     * @param plaintext 需要加密的数据
     * @return 加密后的数据
     */
    @Override
    public String encrypt(Object plaintext) {
        if (ObjUtil.isNull(plaintext)) {
            return null;
        }
        return encoder.encrypt(String.valueOf(plaintext));
    }

    /**
     * 解密
     * @param ciphertext 需要解密的数据
     * @return 解密后的数据
     */
    @Override
    public Object decrypt(String ciphertext) {
        if (ObjUtil.isNull(ciphertext)) {
            return null;
        }
        return encoder.decrypt(ciphertext);
    }

    /**
     * 返回所使用的加密算法,后续配置文件中填写这个算法名
     */
    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
自定义加密器CustomQueryAssistedEncryptor
cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.encrypt.strategy.spi.QueryAssistedEncryptor;

import java.util.Properties;

/**
 * 该种加密方式特点: 相同数据存储会变化
 */
@Slf4j
@Getter
@Setter
public class CustomQueryAssistedEncryptor implements QueryAssistedEncryptor {
    /**
     * 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化
     */
    private Encoder encoder;

    /**
     * 摘要器
     */
    private static final Digester digester = new Digester(DigestAlgorithm.SHA256);

    /**
     * 算法策略类型
     */
    private static final String TYPE = "CustomQueryAssistedEncryptor";

    /**
     * 随机种子长度
     */
    private static final int seedLength = String.valueOf(System.currentTimeMillis()).length();

    private Properties properties = new Properties();

    /**
     * 初始化加密要用的Encoder
     */
    @Override
    public void init() {
        // 初始化Encoder
        Encoder tmepEncoder = SpringUtil.getBean(Encoder.class);
        if (ObjUtil.isNull(tmepEncoder)) {
            log.error("Spring容器中不存在Encoder类型的Bean");
            throw new RuntimeException("Spring容器中不存在Encoder类型的Bean");
        }
        encoder = tmepEncoder;
    }

    /**
     * 辅助查询列
     * @param plaintext plaintext 辅助查询列对象
     * @return 摘要时候的字符串
     */
    @Override
    public String queryAssistedEncrypt(String plaintext) {
        if (ObjUtil.isNull(plaintext)) {
            return null;
        }
        String digestHexStr = digester.digestHex(plaintext);
        return digestHexStr;
    }

    /**
     * 加密
     * @param plaintext 需要加密的数据
     * @return 加密后的数据
     */
    @Override
    public String encrypt(Object plaintext) {
        if (ObjUtil.isNull(plaintext)) {
            return null;
        }
        // 原始字符串 + 随机因子(这里采用时间戳)
        plaintext = plaintext + String.valueOf(System.currentTimeMillis());
        String encryptStr = encoder.encrypt(String.valueOf(plaintext));
        return encryptStr;
    }

    /**
     * 解密
     * @param ciphertext 需要解密的数据
     * @return 解密后的数据
     */
    @Override
    public Object decrypt(String ciphertext) {
        if (ObjUtil.isNull(ciphertext)) {
            return null;
        }
        String decryptStr = encoder.decrypt(ciphertext);
        String rawStr = StrUtil.sub(decryptStr, 0, decryptStr.length() - seedLength);
        return rawStr;
    }

    /**
     * 返回所使用的加密算法,后续配置文件中填写这个算法名
     */
    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
测试类
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@SpringBootTest
class JasyptDemoApplicationTests {
    @Resource
    private UserMapper userMapper;

    @Test
    void insertUser() {
        UserEntity userEntity = new UserEntity();
        userEntity.setUserId(1);
        userEntity.setUserName("tom");
        userEntity.setPassword("123456");
        userMapper.insertUser(userEntity);
    }

    @Test
    void insertUser2() {
        UserEntity userEntity = new UserEntity();
        userEntity.setUserId(1);
        userEntity.setUserName("tom");
        userEntity.setPassword("123456");
        userMapper.insertUser(userEntity);
        userEntity.setUserId(2);
        userMapper.insertUser(userEntity);
    }

    @Test
    void getUserInfo() {
        List<UserEntity> userEntityList = userMapper.getUserInfo("tom", "123456");
        userEntityList.forEach(System.out::println);
    }
}
测试CustomEncryptor
  1. 清空t_user表

  2. 修改yaml配置

    • type选择 CustomEncryptor
    • assisted-query-column参数注释掉
  3. 执行inserter()方法, 发现MySQL中新增数据
    在这里插入图片描述

  4. 执行getUserInfo, 发现解密成功
    在这里插入图片描述

测试CustomQueryAssistedEncryptor
  1. 清空t_user表

  2. 修改yaml配置

    • type选择 CustomQueryAssistedEncryptor,CustomEncrypto记得注释掉
    • assisted-query-column参数注释打开
  3. 执行inserter()2方法, 发现MySQL中新增2条数据(注意这里执行的是inster2方法), 并且即是密码都是123456,但是加密后字符串是不一样的
    在这里插入图片描述

  4. 执行getUserInfo, 发现解密成功
    在这里插入图片描述

脱敏后的模糊查询

加班加点补充中,

原理是分词密文映射表

分词密文映射表

新建一张分词密文映射表,在敏感字段数据新增、修改的后,对敏感字段进行分词组合,再对每个分词进行加密,建立起敏感字段的分词密文与目标数据行主键的关联关系;在处理模糊查询的时候,对模糊查询关键字进行加密,用加密后的模糊查询关键字,对分词密文映射表进行like查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询

参考资料

Jackson 进阶之自定义序列化器

自己动手编写一个Mybatis插件:mybatis脱敏插件

改造了以前写的数据脱敏插件, 更好用了

一个注解优雅的实现 接口数据脱敏-腾讯云开发者社区

数据脱敏 :: ShardingSphere (apache.org)

MyBatis 核心配置概述之 Executor

MyBatis 核心配置综述之 ResultSetHandler

MyBatis 核心配置综述之StatementHandler

大厂也在用的 6种 数据脱敏方案, 别做泄密内鬼

Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)

加密后的敏感字段还能进行模糊查询吗?该如何实现?_加密后的敏感字段还能进行模糊查询吗?该如何实现?

淘宝密文字段检索方案

mybatis(4)—自定义拦截器(下)对象详解

求求你别乱脱敏了!MyBatis 插件 + 注解轻松实现数据脱敏,So easy~! - Java技术栈

一种使用mybatis进行脱敏的思路

Apache ShardingSphere数据脱敏全解决方案详解(上)

ShardingSphere4.1.1:Sharding-JDBC数据加密及SPI加密策略实现

【Java】YAML读写常用工具包及使用示例

使用Hutool对AES加密解密

如何使用hutool进行AES加密和解密?

浅析AES加密工作模式 EBC/CBC 模式了解及具体如何进行补位、AES加密报错java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV处理

java实现对称加密—基本实现

加密的手机号,如何模糊查询?

(四)、Sharding-JDBC数据脱敏

ShardingJDBC源码阅读(十)数据脱敏实战

被问懵了,加密后的数据如何进行模糊查询?

Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏

老大一个接口加解密临时任务丢了过来,我却肝了3天,感觉可以收拾工位了

加密后的数据如何进行模糊查询

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/746287.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

带货直播部门的薪酬提成还有绩效考核怎么做!

直播带货公司一大片&#xff0c;老板一定要控制好自己利润很好的时候分钱的这个欲望&#xff0c;因为不怕分钱&#xff0c;就怕分错了之后收不回来。举例&#xff1a;你今年赚了 1, 000 万&#xff0c;然后你的运营或者你的投手是不是你感觉他的贡献很大&#xff0c;这时候你就…

时延降低 50%,小红书图数据库如何实现多跳查询性能大幅提升

多跳查询为企业提供了深入的数据洞察和分析能力&#xff0c;它在小红书众多在线业务中扮演重要的角色。然而&#xff0c;这类查询往往很难满足稳定的 P99 时延要求。小红书基础架构存储团队针对这一挑战&#xff0c;基于大规模并行处理&#xff08;MPP&#xff09;的理念&#…

【已解决】Pycharm:卡顿解决方案汇总

可能原因&#xff1a; 1、内存少 2、加载慢 3、文件多 4、硬件老 解决方案&#xff1a; 本机测试在 MAC&#xff0c;Windows、Linux也有相应的设置&#xff0c;请自行查询。 一、调整Pycharm使用内存 Help - Change Memory Settings 二、取消勾选 重复打开上次项目 Pych…

什么是Arkose Labs挑战及其解决方法

Arkose Labs挑战是一种复杂的机制&#xff0c;旨在验证用户是真正的人类&#xff0c;而不是自动化的机器人或脚本。这一挑战在维护在线服务的安全性和完整性方面发挥着关键作用&#xff0c;通过防止欺诈活动并确保只有真实用户才能访问某些功能。 目录 什么是Arkose Labs挑战&a…

地理空间数据格式GeoJSON扫盲,在CesiumJS中如何加载。

Hi&#xff0c;我是贝格前端工场&#xff0c;GIS已经越来越多的应用在可视化大屏中了&#xff0c;开发GIS类应用就少不了地理空间数据&#xff0c;本文介绍一下数据GeoJSON数据格式。 一、什么是GeoJSON数据格式&#xff0c;在GIS开发中有什么作用 GeoJSON是一种基于JSON&…

T100M2S2 M.2高清2路SDI采集卡

产品简介&#xff1a; 同三维T100M2S2一款支持全高清1080P 60HZ高清M2型两路SDI采集卡&#xff0c;板卡采用了高速的M.2-PCI-E接口&#xff0c;可实现1080P全实时不丢帧60帧传输。支持高清SDI输入&#xff0c;满足各种用户的需求&#xff0c;其最高分辨率可以实现1920&time…

文生视频模型Sora刷屏的背后的数据支持

前言&#xff1a;近日&#xff0c;OpenAI的首个文生视频模型Sora横空出世&#xff0c;引发了一波Sora热潮。与其相关的概念股连续多日涨停&#xff0c;多家媒体持续跟踪报道&#xff0c;央视也针对Sora进行了报道&#xff0c;称这是第一个真正意义上的视频生成大模型。 01 …

Java医院绩效考核系统源码:考核目标、考核指标、考核方法、考核结果与奖惩措施

Java医院绩效考核系统源码&#xff1a;考核目标、考核指标、考核方法、考核结果与奖惩措施 随着我国医疗体制的改革广大人民群的看病难&#xff0c;看病贵的问题一直没有得到有效地解决医疗费用的上涨&#xff0c;远远大于大多数家庭收入的增长速度。医院的改革已经势在必行&am…

早餐店小程序开发

在快节奏的城市生活中&#xff0c;早餐对于许多人来说是一天中最重要的一餐。然而&#xff0c;传统的早餐店在经营过程中常常面临客流量不稳定、服务效率低下等问题。为了解决这些问题&#xff0c;越来越多的早餐店老板开始寻求利用科技手段提升经营效率。早餐店小程序作为一种…

项目验收测试有必要找第三方软件测试机构吗?

在当今信息技术飞速发展的时代&#xff0c;软件测试成为了确保软件质量的重要环节。而在项目的验收测试中&#xff0c;很多企业都面临一个问题&#xff0c;那就是是否有必要找第三方软件测试机构进行验收测试?今天&#xff0c;我们就来探讨一下这个问题。 第三方软件测试机构…

python中的nan是什么意思

NaN&#xff08;not a number&#xff09;&#xff0c;在数学表示上表示一个无法表示的数&#xff0c;这里一般还会有另一个表述inf&#xff0c;inf和nan的不同在于&#xff0c;inf是一个超过浮点表示范围的浮点数&#xff08;其本质仍然是一个数&#xff0c;只是他无穷大&…

如何制作自己的网站

制作自己的网站可以帮助个人或组织在互联网上展示自己的品牌、作品、产品或服务。随着技术的发展&#xff0c;现在制作网站变得越来越简单。下面是一个简单的步骤指南&#xff0c;帮助你制作自己的网站。 1. 确定你的网站需求和目标 在开始之前&#xff0c;你需要明确你的网站的…

左右旋分辨

从端头看&#xff0c;切削路径顺时针是右旋&#xff0c;反时针左旋。

【JVM-1】JVM内存结构

目录 什么是JVMJava源码执行机制class文件的组成部分 JVM跨平台原理JVM的组成堆年轻代与老年代对象分配过程GC类型Full GC触发条件&#xff1a;对象进入老年代的触发条件 对象分配过程&#xff1a; 字符串常量池静态变量线程本地分配缓冲区&#xff08;TLAB&#xff09;TLAB相关…

SpringBoot前后端传递数据时常用的JSON格式数据是什么?【讲解JSON概念、语法、以及Java对象互转】

SpringBoot前后端传递数据时常用的JSON格式数据是什么&#xff1f; JSON概念JSON语法JSON的两种结构&#xff1a;JSON字符串和Java对象互转&#xff1a;objectMapper.writeValueAsString(person);objectMapper.readValue(jsonStr,Person.class); 在SpringMVC框架中&#xff0c;…

【GitOps】使用Google工具JIB实现本地无需安装容器推送镜像,加速SpringCloud项目开发

文章目录 一、效果展示二、简介三、安装Jib插件1、区分环境2、安装插件一、效果展示 本地是window系统,无docker环境,没有任何runtime,使用jib工具打包镜像并推送完成,用时20秒 二、简介 Jib 是 Google 开发的一款开源工具,旨在帮助 Java 开发者更高效地将 Java 应用程…

ZNB40 矢量网络分析仪

ZNB40 矢量网络分析仪 100kHz至40GHz的宽频率范围&#xff0c;具有四个端口和附加信号发生器 概述 R&SZNB40 提供 100 kHz 至 40 GHz 的宽频率范围&#xff0c;具有四个端口和附加信号发生器。 罗德与施瓦茨带四个端口和附加内部信号源的 40 GHz 中档矢量网络分析仪&…

Ubuntu20.04安装python2和python3及版本配置

Ubuntu20.04安装python2和python3及版本配置_ubuntu 20.04 python3-CSDN博客https://blog.csdn.net/pangc2014/article/details/117407413 >>>ubuntu 安装源码python2_mob649e8161c39d的技术博客_51CTO博客https://blog.51cto.com/u_16175489/7327966

【Academy】测试WebSockets安全漏洞Testing for WebSockets security vulnerabilities

测试WebSockets安全漏洞Testing for WebSockets security vulnerabilities 概述WebSockets是什么?HTTP和WebSockets有什么区别&#xff1f;如何建立WebSocket连接&#xff1f;WebSocket消息看起来像什么&#xff1f; 操纵WebSocket流量拦截和修改WebSocket消息重放和生成新的W…

ONLYOFFICE 8.1:引领桌面办公新潮流,功能升级全面提升

目录 一、ONLYOFFICE是什么&#xff1f; 二、功能完善的PDF编辑器 三、幻灯片版式升级 四、改进从右至左显示 五、新的本地化选项 六、多媒体功能增强 七、应用价值探讨 一、ONLYOFFICE是什么&#xff1f; ONLYOFFICE 是一款功能强大的办公套件&#xff0c;旨在提供全面…