接口加解密及数据加解密

目录

一、 加解密方式介绍

1.1 Hash算法加密

1.2. 对称加密

1.3 非对称加密

二、 我们要讲什么?

三、 接口加解密

四、 数据加解密


一、 加解密方式介绍

        所有的加密方式我们可以分为三类:对称加密、非对称加密、Hash算法加密。

        算法内部的具体实现见:(我们只是算法的搬运工)13种加密与解密算法【一】-CSDN博客

1.1 Hash算法加密

        这种加密方式是一种不可逆的加密方式,它通常用于验证数据的完整性(例如密码)。

        例如:MD5(严格的不可逆、单向加密、加盐)、Base64(严格说是一种编码转换、可以加解密)

1.2. 对称加密

        这种加密方式就是在加密和解密的过程中都使用了一个相同的秘钥(不妨我们将其记作password(和密码同名-_-))。那么我们对数据的加密、解密都使用这个秘钥。特点是加解密速度快。缺点是被人知道秘钥就GG。

        这种加密方式的算法例如:DES、AES、SM4(国密4)……

1.3 非对称加密

        在这种加密的方式中会出现密钥对,包括一个私钥和一个公钥。其中公钥和私钥无关(不能相互推出)。而实际上在双方之间进行数据的加密传输,在每一方自己的操作中进行数据的解密,会涉及到两对秘钥对。

        我们看这个图就很清楚。我们将甲方比作是后端,乙方比作是前端。 后端会有一对密钥对,同理前端也会有一对密钥对。需要注意的是我们的公钥是可以被公布的(和私钥无关),即我们的数据被截获也无法解密。这样一来,以后端为例,后端持有前端的公钥,当后端对前端返回数据的时候,后端使用前端的公钥对数据进行加密。从而前端获取到数据后使用自己的私钥进行解密。同理,前端给后端的数据也会使用后端的公钥进行加密,后端接受到数据后,就会使用后端的私钥进行解密。从而实现前后端之间数据的加密传输。

        这种非对称加密方式的算法有:RSA、SM2(国密2)……

二、 我们要讲什么?

        我们先从前端给的一个请求说起。前端想要对着接口给后端一些数据从而让后端完成业务处理和持久化。很好,好比一个登录注册的请求,后端完成了这方面的接口。前端也对着接口发送数据,然后被恶意人员截获,好啊,我们的数据都是明文,恶意人员又把这个请求放走,完成我们的业务逻辑与持久化。那么其实我们的这个账号就十分危险,其他方面也各种意义的危险。

        所以从这里,我们不希望前后端之间的数据被其他人(各种其他人)所看见,就是某种意义上不能裸奔!所以前端给后端发的数据应该是被加密过的,并且我们需要保证后端能够处理这段加密的数据。那么同理,后端完成业务逻辑与持久化之后,返回给前端的响应也不能裸奔,也需要进行加密,并且保证前端可以处理被加密的数据。

        那么如果我们使用对称加密,恶意人员各种操作,总之搞到了我们的秘钥。其实还是裸奔。所以我们回去使用非对称加密。

        好!继续说上边的,前端给后端加密后的数据,在后端里我们先对数据进行了解密,从而到达我们的业务逻辑处理。然后后端调用Mapper完成持久化,存储到数据库中。现在又有一个技术高超的恶意人员黑到了我们的数据库。哈哈,你的数据都是裸奔。这也就很不安全,各种意义上的危险。

        于是乎,后端在调用Mapper的时候,也应该对必要的字段进行加密,所以后端会有个拦截器去看看:“这家伙要存储的东西有没有是要加密的?”。然后对数据加密之后存储到数据库中。那么拿出来的时候也一样,后端还有个拦截器,看看:“有没有数据是被加密的?”。之后对数据解密,从而完成业务逻辑处理。

三、 接口加解密

        我们讲解ruoyi项目中如下文件。

        Request流只能读取一次?

package org.dromara.common.encrypt.annotation;

import java.lang.annotation.*;

/**
 * 强制加密注解
 *
 * @author Michelle.Chung
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
    // 最终实现 ApiEncrypt 的注解,用于加在 Controller 的方法上,指定是否对请求进行解密,对响应进行加密

    /**
     * 响应加密忽略,默认不加密,为 true 时加密
     */
    boolean response() default false;

}
package org.dromara.common.encrypt.config;

import jakarta.servlet.DispatcherType;
import org.dromara.common.encrypt.filter.CryptoFilter;
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;

/**
 * api 解密自动配置
 *
 * @author wdhcr
 */
@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class) // 获取接口加解密相关配置信息
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
public class ApiDecryptAutoConfiguration {

    @Bean // 注册过滤器 Filter
    public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
        FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        // 设置过滤器并且将配置类传给过滤器
        registration.setFilter(new CryptoFilter(properties));
        // 对全部路径进行过滤
        registration.addUrlPatterns("/*");
        registration.setName("cryptoFilter");
        // 优先级最高
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
        return registration;
    }
}
package org.dromara.common.encrypt.filter;

import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.io.IOException;


/**
 * Crypto 过滤器
 *
 * @author wdhcr
 */
public class CryptoFilter implements Filter { // 实现过滤器,重写其 init()、doFilter()、destroy() 方法
    private final ApiDecryptProperties properties;

    public CryptoFilter(ApiDecryptProperties properties) {
        this.properties = properties;
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        // 强转请求和响应
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpServletResponse servletResponse = (HttpServletResponse) response;

        // 这里是封装了一个方法,用来 从请求中获取到注解,判断是否为加密请求,如果是,那么就返回注解,否则返回 null
        // 那么如果 不是加密请求,null, 就会跳过,直接放行
        // 是的话 就会返回注解,然后进行解密
        ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);


        // apiEncrypt 不为空 说明 存在加密注解,那么就进行解密
        // 且 响应加密,那么 responseFlag 为 true
        boolean responseFlag = apiEncrypt != null && apiEncrypt.response();

        // 新建请求和响应的包装类
        ServletRequest requestWrapper = null;
        ServletResponse responseWrapper = null;
        EncryptResponseBodyWrapper responseBodyWrapper = null;

        /**
         *  对请求的数据进行解密
         *  对响应的数据进行加密
         */

        // 这里需要说明一下,就是标识头是被 AES 加密过的

        // 首先看看是不是请求
        // 是否为 put 或者 post 请求,换句话说,DELETE 和 GET方法不用解密
        if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
            // 是否存在加密标头,这个就说明前端传来了密文
            String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
            if (StringUtils.isNotBlank(headerValue)) {
                // 请求解密,将请求,后端的私钥,以及加密标头放入,进行解密
                requestWrapper = new DecryptRequestBodyWrapper(servletRequest,
                                                    properties.getPrivateKey(), properties.getHeaderFlag());
            } else {
                // 就是说我们是没有看到加密标头,但是又存在加密注解,那么就抛出异常
                if (ObjectUtil.isNotNull(apiEncrypt)) {
                    // 获取 异常处理器, 然后抛出异常
                    HandlerExceptionResolver exceptionResolver =
                        SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);

                    exceptionResolver.resolveException(
                        servletRequest, servletResponse, null,
                        new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
                    return;
                }
                // 没有加密注解,没有加密标头,那么直接放行这个请求
                // 其实到这里 对请求来说 就结束了,因为没有加密注解,所以 responseFlag 为 false,直接放行
            }
        }

        // 判断是否响应加密
        if (responseFlag) {
            // 对应的响应内容进行包装加密
            // 这一步只是 对 response 进行包装,并没有对 response 进行加密,因为 下边还有判断 是不是直接放行
            responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
            responseWrapper = responseBodyWrapper;
        }

        // 如果 requestWrapper 和 responseWrapper 都为空,那么直接放行
        chain.doFilter(
            ObjectUtil.defaultIfNull(requestWrapper, request),
            ObjectUtil.defaultIfNull(responseWrapper, response));

        // 不为空且响应加密,那么对 responseBodyWrapper 进行加密处理
        if (responseFlag) {
            // 重置 response
            servletResponse.reset();
            // 对原始内容加密,将响应,后端的公钥,以及加密标头放入,进行加密
            String encryptContent = responseBodyWrapper.getEncryptContent(
                servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
            // 对加密后的内容写出
            servletResponse.getWriter().write(encryptContent);
        }
    }

    /**
     * 获取 ApiEncrypt 注解
     */
    private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
        RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        // 获取注解
        try {
            HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
            if (ObjectUtil.isNotNull(mappingHandler)) {
                Object handler = mappingHandler.getHandler();
                if (ObjectUtil.isNotNull(handler)) {
                    // 从handler获取注解
                    if (handler instanceof HandlerMethod handlerMethod) {
                        return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    @Override
    public void destroy() {
    }
}
package org.dromara.common.encrypt.filter;

import cn.hutool.core.io.IoUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.encrypt.utils.EncryptUtils;
import org.springframework.http.MediaType;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * 解密请求参数工具类
 *
 * @author wdhcr
 */
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {

    // 存储请求体,以便能够重复读取
    private final byte[] body;

    public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
        super(request);
        // 以下的操作使得最终 被加密的body 被解密出来

        // body 是通过 AES 加密
        // 只有我们获得了 AES 密码, 我们才可以 解密 body

        // 根据请求头的 headerFlag 获取到 RSA 加密后的 AES 密码
        String headerRsa = request.getHeader(headerFlag);
        // 将 AES 密码从 RSA 解密出来
        String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
        // 将 AES 密码从 Base64 解密出来
        String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
        // 设置 请求的编码格式
        request.setCharacterEncoding(Constants.UTF8);
        // 读取请求体
        byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
        String requestBody = new String(readBytes, StandardCharsets.UTF_8);
        // 将 body 从 AES 解密出来
        String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
        body = decryptBody.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }


    @Override
    public int getContentLength() {
        return body.length;
    }

    @Override
    public long getContentLengthLong() {
        return body.length;
    }

    @Override
    public String getContentType() {
        return MediaType.APPLICATION_JSON_VALUE;
    }


    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() {
                return bais.read();
            }

            @Override
            public int available() {
                return body.length;
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}
package org.dromara.common.encrypt.filter;

import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.dromara.common.encrypt.utils.EncryptUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * 加密响应参数包装类
 *
 * @author Michelle.Chung
 */
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {

    private final ByteArrayOutputStream byteArrayOutputStream;
    private final ServletOutputStream servletOutputStream;
    private final PrintWriter printWriter;

    public EncryptResponseBodyWrapper(HttpServletResponse response) throws IOException {
        super(response);
        // 新建一个输出流
        this.byteArrayOutputStream = new ByteArrayOutputStream();
        this.servletOutputStream = this.getOutputStream();
        this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
    }

    @Override
    public PrintWriter getWriter() {
        return printWriter;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (servletOutputStream != null) {
            servletOutputStream.flush();
        }
        if (printWriter != null) {
            printWriter.flush();
        }
    }

    @Override
    public void reset() {
        byteArrayOutputStream.reset();
    }

    public byte[] getResponseData() throws IOException {
        flushBuffer();
        return byteArrayOutputStream.toByteArray();
    }

    public String getContent() throws IOException {
        flushBuffer();
        return byteArrayOutputStream.toString();
    }

    /**
     * 获取加密内容
     *
     * @param servletResponse response
     * @param publicKey       RSA公钥 (用于加密 AES 秘钥)
     * @param headerFlag      请求头标志
     * @return 加密内容
     * @throws IOException
     */
    public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
        // 生成秘钥
        String aesPassword = RandomUtil.randomString(32);
        // 秘钥使用 Base64 编码
        String encryptAes = EncryptUtils.encryptByBase64(aesPassword);
        // Rsa 公钥加密 Base64 编码
        String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);

        // 设置响应头
        servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
        servletResponse.setHeader(headerFlag, encryptPassword);
        servletResponse.setHeader("Access-Control-Allow-Origin", "*");
        servletResponse.setHeader("Access-Control-Allow-Methods", "*");
        servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());

        // 获取原始内容
        String originalBody = this.getContent();
        // 对内容进行加密
        return EncryptUtils.encryptByAes(originalBody, aesPassword);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }

            @Override
            public void write(int b) throws IOException {
                byteArrayOutputStream.write(b);
            }

            @Override
            public void write(byte[] b) throws IOException {
                byteArrayOutputStream.write(b);
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                byteArrayOutputStream.write(b, off, len);
            }
        };
    }

}

四、 数据加解密

        我们讲解ruoyi中如下文件:

package org.dromara.common.encrypt.annotation;

import org.dromara.common.encrypt.enumd.AlgorithmType;
import org.dromara.common.encrypt.enumd.EncodeType;

import java.lang.annotation.*;

/**
 * 字段加密注解
 *
 * @author 老马
 */
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField { // 最终我们就是在字段上加这个注解,然后通过反射获取到注解信息,然后对字段进行加密解密

    /**
     * 加密算法
     */
    AlgorithmType algorithm() default AlgorithmType.DEFAULT;

    /**
     * 秘钥。AES、SM4需要
     */
    String password() default ""; // 处理对称加密的秘钥

    // 下边两个是非对称加密的秘钥

    /**
     * 公钥。RSA、SM2需要
     */
    String publicKey() default "";

    /**
     * 私钥。RSA、SM2需要
     */
    String privateKey() default "";


    // 编码方式

    /**
     * 编码方式。对加密算法为BASE64的不起作用
     */
    EncodeType encode() default EncodeType.DEFAULT;

}
package org.dromara.common.encrypt.config;

import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.encrypt.core.EncryptorManager;
import org.dromara.common.encrypt.interceptor.MybatisDecryptInterceptor;
import org.dromara.common.encrypt.interceptor.MybatisEncryptInterceptor;
import org.dromara.common.encrypt.properties.EncryptorProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
 * 加解密配置
 *
 * @author 老马
 * @version 4.6.0
 */
@AutoConfiguration(after = MybatisPlusAutoConfiguration.class) // 需在 MybatisPlusAutoConfiguration 之后加载
                                                            // 使用这个注解会自动注入到 IoC 容器
@EnableConfigurationProperties(EncryptorProperties.class) // 加载配置文件
@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true") // 只有配置了
                                                            // mybatis-encryptor.enable=true 时才
                                                            // 会创建 Bean
@Slf4j
public class EncryptorAutoConfiguration {

    @Autowired // 获取配置文件,成为 Bean
    private EncryptorProperties properties;

    // 注入 EncryptorManager 加密管理器
    // 当项目一启动,就会把 EncryptorManager 加载到 IoC 容器中
    @Bean
    public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {
        return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());
    }

    // 加密拦截器
    @Bean
    public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {
        return new MybatisEncryptInterceptor(encryptorManager, properties);
    }

    // 解密拦截器
    @Bean
    public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
        return new MybatisDecryptInterceptor(encryptorManager, properties);
    }

}



package org.dromara.common.encrypt.properties;

import org.dromara.common.encrypt.enumd.AlgorithmType;
import org.dromara.common.encrypt.enumd.EncodeType;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 加解密属性配置类
 *
 * @author 老马
 * @version 4.6.0
 */
@Data
@ConfigurationProperties(prefix = "mybatis-encryptor") // 根据前缀匹配
public class EncryptorProperties {

    /**
     * 过滤开关
     */
    private Boolean enable;

    /**
     * 默认算法
     */
    private AlgorithmType algorithm;

    /**
     * 安全秘钥
     */
    private String password;

    /**
     * 公钥
     */
    private String publicKey;

    /**
     * 私钥
     */
    private String privateKey;

    /**
     * 编码方式,base64/hex
     */
    private EncodeType encode;

}
package org.dromara.common.encrypt.interceptor;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.dromara.common.encrypt.core.EncryptContext;
import org.dromara.common.encrypt.core.EncryptorManager;
import org.dromara.common.encrypt.enumd.AlgorithmType;
import org.dromara.common.encrypt.enumd.EncodeType;
import org.dromara.common.encrypt.properties.EncryptorProperties;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.*;

/**
 * 出参解密拦截器
 *
 * @author 老马
 * @version 4.6.0
 */
@Slf4j
@Intercepts({@Signature(
    type = ResultSetHandler.class,
    method = "handleResultSets",
    args = {Statement.class})
}) // 这个意思是拦截 目标为:ResultSetHandler.class, 拦截结果集
    // 方法名为:handleResultSets,
    // 参数为:Statement.class
@AllArgsConstructor
public class MybatisDecryptInterceptor implements Interceptor {

    private final EncryptorManager encryptorManager;
    private final EncryptorProperties defaultProperties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 拦截到结果集
        Object result = invocation.proceed();
        if (result == null) {
            return null;
        }
        // 如果结果集不空,就将结果集解密后返回。
        decryptHandler(result);
        return result;
    }

    /**
     * 解密对象
     *
     * @param sourceObject 待加密对象
     */
    private void decryptHandler(Object sourceObject) {
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }
        if (sourceObject instanceof Map<?, ?> map) {
            new HashSet<>(map.values()).forEach(this::decryptHandler);
            return;
        }
        if (sourceObject instanceof List<?> list) {
            if(CollUtil.isEmpty(list)) {
                return;
            }
            // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                return;
            }
            list.forEach(this::decryptHandler);
            return;
        }
        // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
        Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
        if(ObjectUtil.isNull(fields)){
            return;
        }
        try {
            for (Field field : fields) {
                field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
            }
        } catch (Exception e) {
            log.error("处理解密字段时出错", e);
        }
    }

    /**
     * 字段值进行加密。通过字段的批注注册新的加密算法
     *
     * @param value 待加密的值
     * @param field 待加密字段
     * @return 加密后结果
     */
    private String decryptField(String value, Field field) {
        if (ObjectUtil.isNull(value)) {
            return null;
        }
        EncryptField encryptField = field.getAnnotation(EncryptField.class);
        EncryptContext encryptContext = new EncryptContext();
        encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
        encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
        encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
        encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
        encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
        return this.encryptorManager.decrypt(value, encryptContext);
    }

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

    @Override
    public void setProperties(Properties properties) {

    }
}
package org.dromara.common.encrypt.interceptor;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.dromara.common.encrypt.core.EncryptContext;
import org.dromara.common.encrypt.core.EncryptorManager;
import org.dromara.common.encrypt.enumd.AlgorithmType;
import org.dromara.common.encrypt.enumd.EncodeType;
import org.dromara.common.encrypt.properties.EncryptorProperties;
import org.springframework.web.servlet.HandlerInterceptor;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.*;

/**
 * 入参加密拦截器
 *
 * @author 老马
 * @version 4.6.0
 */
@Slf4j
@Intercepts({@Signature(
    type = ParameterHandler.class,
    method = "setParameters",
    args = {PreparedStatement.class})
}) // 这个意思是拦截 目标为:ParameterHandler.class, 拦截参数处理
    // 方法名为:setParameters,
    // 参数为:PreparedStatement.class
@AllArgsConstructor
public class MybatisEncryptInterceptor implements Interceptor { // 继承Interceptor接口,插件需要实现这个接口
                                                            // 而我们平常写的拦截器是继承了 HandlerInterceptor 接口
                                                            // 实现 Mybatis 自定义插件

    private final EncryptorManager encryptorManager;
    private final EncryptorProperties defaultProperties;

    @Override
    // 这个是核心方法,用于实现拦截逻辑
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation;
    }

    @Override
    // 这个方法用于对目标进行包装,返回一个代理对象
    public Object plugin(Object target) {
        // 拦截器只对ParameterHandler进行加密
        if (target instanceof ParameterHandler parameterHandler) {
            // 获得参数 -- 待加密对象
            Object parameterObject = parameterHandler.getParameterObject();
            if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
                // 对参数加密进行加密
                this.encryptHandler(parameterObject);
            }
        }
        return target;
    }

    /**
     * 加密对象
     *
     * @param sourceObject 待加密对象
     */
    private void encryptHandler(Object sourceObject) {
        // 如果对象为空,直接返回
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }
        // 判断是否为map,如果是,就递归加密
        if (sourceObject instanceof Map<?, ?> map) {
            new HashSet<>(map.values()).forEach(this::encryptHandler);
            return;
        }
        // 判断是否为list,如果是,就递归加密
        if (sourceObject instanceof List<?> list) {
            if(CollUtil.isEmpty(list)) {
                return;
            }
            // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
            // List中放的是对象, 它们的字段都相同, 所以只需要判断第一个元素即可
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                return;
            }
            list.forEach(this::encryptHandler);
            return;
        }
        // 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
        Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
        if(ObjectUtil.isNull(fields)){
            return;
        }
        try {
            // 对加密字段进行加密
            for (Field field : fields) {
                field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));
            }
        } catch (Exception e) {
            log.error("处理加密字段时出错", e);
        }
    }

    /**
     * 字段值进行加密。通过字段的批注注册新的加密算法
     *
     * @param value 待加密的值
     * @param field 待加密字段
     * @return 加密后结果
     */
    private String encryptField(String value, Field field) {
        if (ObjectUtil.isNull(value)) {
            return null;
        }
        EncryptField encryptField = field.getAnnotation(EncryptField.class);
        // 设置加密上下文
        EncryptContext encryptContext = new EncryptContext();
        encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
        encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
        encryptContext.setPassword(StringUtils.isBlank(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
        encryptContext.setPrivateKey(StringUtils.isBlank(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
        encryptContext.setPublicKey(StringUtils.isBlank(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
        // 进行加密
        return this.encryptorManager.encrypt(value, encryptContext);
    }


    @Override
    public void setProperties(Properties properties) {
    }
}
package org.dromara.common.encrypt.core;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.Resources;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * 加密管理类
 *
 * @author 老马
 * @version 4.6.0
 */
@Slf4j
@NoArgsConstructor
public class EncryptorManager {

    /**
     * 缓存加密器
     */
    // key:加密配置,value:加密执行者
    Map<EncryptContext, IEncryptor> encryptorMap = new ConcurrentHashMap<>();

    /**
     * 类加密字段缓存
     */
    // key:实体类,value:加密字段集合
    Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();

    /**
     * 构造方法传入类加密字段缓存
     *
     * @param typeAliasesPackage 实体类包
     */
    public EncryptorManager(String typeAliasesPackage) {
        // 当前项目启动,扫描实体类包,获取加密字段
        scanEncryptClasses(typeAliasesPackage);
    }


    /**
     * 获取类加密字段缓存
     */
    public Set<Field> getFieldCache(Class<?> sourceClazz) {
        if (ObjectUtil.isNotNull(fieldCache)) {
            return fieldCache.get(sourceClazz);
        }
        return null;
    }

    /**
     * 注册加密执行者到缓存
     *
     * @param encryptContext 加密执行者需要的相关配置参数
     */
    public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
        if (encryptorMap.containsKey(encryptContext)) {
            return encryptorMap.get(encryptContext);
        }
        // 根据算法获取加密执行者
        IEncryptor encryptor = ReflectUtil.newInstance(encryptContext.getAlgorithm().getClazz(), encryptContext);
        encryptorMap.put(encryptContext, encryptor);
        return encryptor;
    }

    /**
     * 移除缓存中的加密执行者
     *
     * @param encryptContext 加密执行者需要的相关配置参数
     */
    public void removeEncryptor(EncryptContext encryptContext) {
        this.encryptorMap.remove(encryptContext);
    }

    /**
     * 根据配置进行加密。会进行本地缓存对应的算法和对应的秘钥信息。
     *
     * @param value          待加密的值
     * @param encryptContext 加密相关的配置信息
     */
    public String encrypt(String value, EncryptContext encryptContext) {
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
        return encryptor.encrypt(value, encryptContext.getEncode());
    }

    /**
     * 根据配置进行解密
     *
     * @param value          待解密的值
     * @param encryptContext 加密相关的配置信息
     */
    public String decrypt(String value, EncryptContext encryptContext) {
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
        return encryptor.decrypt(value);
    }

    /**
     * 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体
     */
    private void scanEncryptClasses(String typeAliasesPackage) {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
        // 获取所有扫描包的路径
        String[] packagePatternArray = StringUtils.splitPreserveAllTokens(typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
        try {
            // 对每个路径下的包进行扫描
            for (String packagePattern : packagePatternArray) {
                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
                for (Resource resource : resources) {
                    // 获取类元数据,获取类名,如果有加密注解,则加入缓存
                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
                    // 封装方法,获取加密字段集合
                    Set<Field> encryptFieldSet = getEncryptFieldSetFromClazz(clazz);
                    if (CollUtil.isNotEmpty(encryptFieldSet)) {
                        fieldCache.put(clazz, encryptFieldSet);
                    }
                }
            }
        } catch (Exception e) {
            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
        }
    }

    /**
     * 获得一个类的加密字段集合
     */
    private Set<Field> getEncryptFieldSetFromClazz(Class<?> clazz) {
        Set<Field> fieldSet = new HashSet<>();
        // 判断clazz如果是接口,内部类,匿名类就直接返回
        if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
            return fieldSet;
        }
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            fieldSet.addAll(Arrays.asList(fields));
            clazz = clazz.getSuperclass();
        }
        fieldSet = fieldSet.stream().filter(field ->
                field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
            .collect(Collectors.toSet());
        for (Field field : fieldSet) {
            field.setAccessible(true);
        }
        return fieldSet;
    }

}

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

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

相关文章

【后端开发】JavaEE初阶—线程的理解和编程实现

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解多线程的知识哟~~~&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【后端开发】JavaEE初阶——计算机是如何工作的&#xff1f;&#xff1f;&#xff1f;-CSDN博客 &#x1f308;感兴趣的小伙…

【设计模式】UML类图

目录 前言 一、类图概述 二、类图的作用 三、类图表示法 四、类之间关系的表示方法 1. 关联关系 1.1 单向关联 1.2 双向关联 1.3 自关联 2. 聚合关系 3. 组合关系 4. 依赖关系 5. 继承关系 6. 实现关系 总结 前言 统一建模语言&#xff08; Unified Modeling La…

游戏如何对抗定制挂

近年来&#xff0c;游戏安全对抗强度相比以往更加激烈&#xff0c;具体表现在“定制挂”趋势显著。在近期收集的近万款外挂样本中&#xff0c;定制挂约占比78%&#xff0c;常见的内存修改器、变速器等通用作弊手段占比正在下降。 所谓定制挂&#xff0c;是指针对某款游戏单独开…

九章云极DataCanvas公司荣获2024年服贸会“科技创新服务示范案例”

9月15日&#xff0c;2024年中国国际服务贸易交易会&#xff08;服贸会&#xff09;示范案例交流会暨颁奖典礼在北京国家会议中心举行&#xff0c;九章云极DataCanvas 公司自研的DataCanvas Alaya NeW智算操作系统凭借卓越的AI创新实力、前瞻性的市场布局以及突破性的技术革新成…

Python脚本每日自动备份MySQL数据库,无需mysqldump

编写一个Python脚本&#xff0c;每天凌晨3点开始备份 脚本具有以下特点 不需要安装mysql-client&#xff0c;并且Windows Linux都可以使用支持多个数据库连接的备份每个数据库支持多个表备份日志保存下来&#xff0c;方便第二天早上查看备份结果 首先安装需要的库 pip3 ins…

Mybatis Plus分页查询返回total为0问题

Mybatis Plus分页查询返回total为0问题 一日&#xff0c;乌云密布&#xff0c;本人看着mybatis plus的官方文档&#xff0c;随手写了个分页查询&#xff0c;如下 Page<Question> questionPage questionService.page(new Page<>(current, size),questionService.g…

[C语言]连子棋游戏

文章目录 一、前言二、游戏思路三、游戏方法1、初始化2、判断胜利3、交互4、电脑下棋 四、核心方法说明1、初始化游戏2、销毁棋盘3、显示游戏4、电脑下棋5、用户下棋6、判断游戏状态7、游戏交互 五、游戏效果展示与源码分享1、游戏效果2、源代码 一、前言 对于指针和数组理解尚…

DataGrip在Windows和MacOS平台上的快捷键

0. 背景信息 No.说明1测试DataGrip版本号 : 2024.2.2 1. Windows下快捷键 2. MacOS下快捷键

基于波特图的控制系统设计算法

波特图&#xff08;Bode Plot&#xff09;是一种用于描述线性控制系统频率响应的图形表示方法&#xff0c;通常用于分析和设计控制系统。它以控制系统的传递函数&#xff08;或频域传递函数&#xff09;为基础&#xff0c;将系统的幅频特性&#xff08;振幅-频率响应&#xff0…

PyCharm与Anaconda超详细安装配置教程

1、安装Anaconda&#xff08;过程&#xff09;-CSDN博客 2.创建虚拟环境conda create -n pytorch20 python3.9并输入conda activate pytorch20进入 3.更改镜像源conda/pip(只添加三个pip源和conda源即可) 4.安装PyTorch&#xff08;CPU版&#xff09; 5.安装Pycharm并破解&…

LED灯、蜂鸣器、继电器的控制

LED灯的控制 该专栏所有文章都默认使用STM32F103ZET6开发板 目录 LED灯的控制 一、简单的LED灯控制 1、初始化函数 led灯 2、应用函数 2、蜂鸣器 3、继电器 一、简单的LED灯控制 编程框架&#xff1a;初始化函数和应用函数 1、初始化函数 初始化函数一般包括&#xf…

SVTR文字识别

论文地址&#xff1a;https://arxiv.org/abs/2205.00159 notes&#xff1a; 论文2.5中说的N nodes&#xff0c;就是输出的类别数量&#xff0c;英文37&#xff0c;中文6625&#xff0c;英文37说的是最简单的英文文字识别任务&#xff0c;不区分大小写&#xff0c;就是26个字母…

软件测试 BUG 篇

目录 一、软件测试的生命周期 二、BUG 1. bug的概念 2. 描述bug的要素 3. bug的级别 4. bug的生命周期 5. 与开发产生争执怎么办&#xff1f;&#xff08;面试高频考题&#xff09; 5.1 先检查自身&#xff0c;是否bug描述不清楚 5.2 站在用户角度考虑并抛出问题 5.3 …

nginx架构篇(三)

文章目录 一、Nginx实现服务器端集群搭建1.1 Nginx与Tomcat部署1. 环境准备(Tomcat)2. 环境准备(Nginx) 1.2. Nginx实现动静分离1.2.1. 需求分析1.2.2. 动静实现步骤 1.3. Nginx实现Tomcat集群搭建1.4. Nginx高可用解决方案1.4.1. Keepalived1.4.2. VRRP介绍1.4.3. 环境搭建环境…

【已解决】SpringBoot3项目整合Druid依赖:Druid监控页面404报错

文章标题 问题描述原因分析解决方案参考资料 问题描述 最近&#xff0c;笔者在SpringBoot3项目中整合Druid连接池时&#xff0c;偶然翻到一条介绍Druid监控的短视频&#xff0c;兴致盎然之下尝试设置了一下Druid监控。 But&#xff0c;按照视频中提供的yml参数对照设置&#x…

【全网最全】2024华为杯数学建模C题高质量成品查看论文!【附带全套代码+数据】

题 目&#xff1a; ___基于数据驱动下磁性元件的磁芯损耗建模 完整版获取&#xff1a; 点击链接加入群聊【2024华为杯数学建模助攻资料】&#xff1a;http://qm.qq.com/cgi-bin/qm/qr?_wv1027&kxtS4vwn3gcv8oCYYyrqd0BvFc7tNfhV7&authKeyedQFZne%2BzvEfLEVg2v8FOm%…

山体滑坡检测系统源码分享

山体滑坡检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

使用vue创建项目

一、安装环境 二、创建vue框架&#xff08;创建文件夹&#xff0c;摁shift鼠标右键 打开&#xff09; 1、项目配置 2、新增目录 三、路径别名配置 输入/ ,VSCode会联想出src下的所有子目录和文件&#xff0c;统一文件路径访问时不容易出错 四、ElementPlus配置 1、组件分为…

华为全联接大会HC2024 观会感

9/19-21于上海&#xff0c;华为举办了他一年一届也是最重要的华为系展会-Huawei Connect 华为全联接大会&#xff0c;今天有幸赶在展会最后一天来参观一下 上午照常是keynote&#xff0c;由华为计算线总裁进行了今天的KN开场&#xff0c;介绍了华为在“算”方面的进展&#x…

web基础—dvwa靶场(九)Weak Session IDs

Weak Session IDs&#xff08;弱会话&#xff09; Weak Session IDs&#xff08;弱会话&#xff09;&#xff0c;用户访问服务器的时候&#xff0c;一般服务器都会分配一个身份证 session id 给用户&#xff0c;用于标识。用户拿到 session id 后就会保存到 cookies 上&#x…