接口加解密及数据加解密

目录

一、 加解密方式介绍

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 // 注册过滤器 Filterpublic 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;}@Overridepublic 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 为 trueboolean 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) {// 重置 responseservletResponse.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;}@Overridepublic 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);}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic int getContentLength() {return body.length;}@Overridepublic long getContentLengthLong() {return body.length;}@Overridepublic String getContentType() {return MediaType.APPLICATION_JSON_VALUE;}@Overridepublic ServletInputStream getInputStream() {final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() {return bais.read();}@Overridepublic int available() {return body.length;}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic 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));}@Overridepublic PrintWriter getWriter() {return printWriter;}@Overridepublic void flushBuffer() throws IOException {if (servletOutputStream != null) {servletOutputStream.flush();}if (printWriter != null) {printWriter.flush();}}@Overridepublic 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);}@Overridepublic ServletOutputStream getOutputStream() throws IOException {return new ServletOutputStream() {@Overridepublic boolean isReady() {return false;}@Overridepublic void setWriteListener(WriteListener writeListener) {}@Overridepublic void write(int b) throws IOException {byteArrayOutputStream.write(b);}@Overridepublic void write(byte[] b) throws IOException {byteArrayOutputStream.write(b);}@Overridepublic 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 // 获取配置文件,成为 Beanprivate EncryptorProperties properties;// 注入 EncryptorManager 加密管理器// 当项目一启动,就会把 EncryptorManager 加载到 IoC 容器中@Beanpublic EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());}// 加密拦截器@Beanpublic MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {return new MybatisEncryptInterceptor(encryptorManager, properties);}// 解密拦截器@Beanpublic 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;@Overridepublic 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);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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);}@Overridepublic 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.mzph.cn/bicheng/54769.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

选择租用徐州服务器机柜的作用有哪些?

企业为了线上网络业务&#xff0c;通常都会选择租用服务器来确保网络的稳定性&#xff0c;企业选择服务器租用和托管业务后&#xff0c;同时也需要租用服务器机柜来进行放置所使用的服务器&#xff0c;对于机柜企业可以选择租用徐州机柜&#xff0c;下面就来聊一下选择租用徐州…

LeetCode: 2207. 字符串中最多数目的子序列 一次遍历数组,时间复杂度O(n)

2207. 字符串中最多数目的子序列 today 2207 字符串中最多数目的子序列 题目描述 你一个下标从 0 开始的字符串 text 和另一个下标从 0 开始且长度为 2 的字符串 pattern &#xff0c;两者都只包含小写英文字母。 你可以在 text 中任意位置插入 一个 字符&#xff0c;这个插…

【html】基础(二)

本专栏内容为&#xff1a;前端专栏 记录学习前端&#xff0c;分为若干个子专栏&#xff0c;html js css vue等 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;js专栏 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &am…

http增删改查四种请求方式操纵数据库

注意&#xff1a;在manage.py项目入口文件中的路由配置里&#xff0c;返回响应的 return语句后面的代码不会执行&#xff0c;所以路由配置中每个模块代码要想都执行&#xff0c;不能出现return 激活虚拟环境&#xff1a;venv(我的虚拟环境名称&#xff09;\Scripts\activate …

ubuntu20.04编译安装opencv-4.9.0的cuda版本

NVIDIA显卡驱动550.107.02&#xff08;4060显卡&#xff1a;8.9&#xff0c;3060显卡&#xff1a;8.6&#xff09;cuda&#xff1a;12.1cudnn&#xff1a;8.9.7opencv4.9.0&#xff0c;opencv_contrib_4.9.0 前三个安装略过&#xff01; 主要编译安装opencv4.9.0: 下载openc…

Unity3D 小案例 像素贪吃蛇 03 蛇的碰撞

Unity3D 小案例 像素贪吃蛇 第三期 蛇的碰撞&#xff08;完结&#xff09; 像素贪吃蛇 碰撞蛇身 当蛇头碰撞到蛇身时&#xff0c;游戏应该判定为失败。 找到蛇身预制体&#xff0c;添加 Body 标签和碰撞体&#xff0c;碰撞体的大小为 0.5&#xff0c;跟蛇头和蛇身的碰撞体范…

使用数据泵(Data Pump)迁移Oracle数据库数据

目的 将一个oracle数据库实例数据复制给另一个实例 注意事项 sqlplus&#xff08;即在本机linux环境下执行sql&#xff09;、expdp、impdp等命令一般需要切换到oracle用户才能执行 如果你当前是linux的root用户&#xff0c;请切换用户 su - oracle //一般在oracle账号下才能…

node节点使用:

节点&#xff1a; 1.返回父节点 parentNode let par li1.parentNodepar.style.border 1px solid red 2.获取所有子节点的集合 childNodes let nodes par.childNodes 3.第一个子节点 firstChild let frist par.firstChild 4.最后一个 lastChild let last par.lastC…

AlDente Pro for Mac电池健康保护工具

AlDente Pro for Mac 是一款适用于 Mac 的实用电池健康保护工具。以下是它的主要特点和优势&#xff1a; 软件下载地址 一、保护电池寿命的原理 锂离子和聚合物电池&#xff08;如 Mac 笔记本中的电池&#xff09;在 30% 到 80% 之间运行时使用寿命最长。始终将电池电量保持…

service 命令:管理系统服务

一、命令简介 ​service​ 命令是 Linux 系统中用于管理服务的工具&#xff0c;它通过调用位于 /etc/init.d/ ​目录下的服务脚本&#xff0c;来启动、停止、重启、查询状态等操作系统服务&#xff08;守护进程&#xff09;。service ​命令是一个便捷的 shell 脚本&#xff0…

Windows 10 on ARM, version 22H2 (updated Aug 2024) ARM64 AArch64 中文版、英文版下载

Windows 10 on ARM, version 22H2 (updated Aug 2024) ARM64 AArch64 中文版、英文版下载 基于 ARM 的 Windows 10 请访问原文链接&#xff1a;https://sysin.org/blog/windows-10-arm/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;s…

【VUE3.0】动手做一套像素风的前端UI组件库---Message

目录 引言自己整一个UI设计稿代码编写1. 设计信息窗口基础样式2. 设置打开和关闭的方法3. 编写实例化组件的js文件4. 看下最终效果5. 组件完整代码6. 组件调用方式 总结 引言 本教程基于前端UI样式库 NES.css 的UI设计&#xff0c;自行研究复现。欢迎大家交流优化实现方法~ 此次…

深度学习(3):Tensor和Optimizer

文章目录 是什么Tensor1. Tensor 的基本概念2. 自动求导&#xff08;Autograd&#xff09;机制3. requires_grad 属性4. .data 和 .item()5. 梯度清零 Optimizer 是什么 Tensor&#xff08;张量&#xff09;&#xff1a;在 PyTorch 中&#xff0c;Tensor 是一种多维数组&#…

《线性代数》学渣笔记

文章目录 1 行列式1.1 克拉默法则1.2 基本性质1.3 余子式 M i j M_{ij} Mij​1.4 代数余子式 A i j ( − 1 ) i j ⋅ M i j A_{ij} (-1)^{ij} \cdot M_{ij} Aij​(−1)ij⋅Mij​1.5 具体型行列式计算&#xff08;化为基本型&#xff09;1.5.1 主对角线行列式&#xff1a;主…

基于数据挖掘的航空客户满意度分析预测系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 航空公司致力于提供多样化的服务以满足乘客需求&#xff0c;包括但不限于提供免费无线网络、免费食物饮品、提供网上预约服务、飞机出口位置、座椅舒适度、卫生状况等&#xff0c;并希望以此提升乘…

SQL面试常见题目

SQL面试常见题目涉及多个方面&#xff0c;包括数据查询、数据操作、表的设计与优化等。以下列举一些经典的SQL面试题目&#xff0c;并附上解析答案&#xff1a; 1. 查询一张表中重复的数据 题目&#xff1a; 给定一个表 employees&#xff0c;包含 id, name, salary 列。如何…

python基础题练习

1.可否定义一个sum函数呢&#xff1f;返回指定区间的值的和&#xff1f;例如&#xff0c;区间[1,4]的和为123410返回指定区间值的平方的和呢&#xff1f;立方呢&#xff1f; 代码&#xff1a; # 计算从start到end&#xff08;包括end&#xff09;的所有整数的和。 def sum_ra…

AutoX.js向后端传输二进制数据

android的JavaScript自动化软件用过Hamibot和AutoX.js 不过在向后端传输二进制数据时都有些限制&#xff0c;不如浏览器前端那么自由。Hamibot的http按文档应该时能支持传字节数组&#xff0c;但是实际上应该还没有支持。AutoX.js的http也是这样&#xff0c;但是AutoX.js还支持…

P7557 [USACO21OPEN] Acowdemia S题解

[USACO21OPEN] Acowdemia S 题目描述 由于对计算机科学的热爱&#xff0c;以及有朝一日成为 「Bessie 博士」的诱惑&#xff0c;奶牛 Bessie 开始攻读计算机科学博士学位。经过一段时间的学术研究&#xff0c;她已经发表了 N N N 篇论文&#xff08; 1 ≤ N ≤ 1 0 5 1 \leq…

SpringBoot框架之KOB项目 - 配置Mysql与注册登录模块(中)

修改Spring Security 登录验证模式 传统的验证登录模式 公开页面&#xff1a;输入url就可以直接访问授权页面&#xff1a;登录之后才可以访问 Jwt验证模式 容易实现跨域不需要在服务器端存储 对比于传统模式将所有的sessionId换成jwt token access token refresh token 过…