接口加解密及数据加解密
目录
一、 加解密方式介绍
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;}}
相关文章:
接口加解密及数据加解密
目录 一、 加解密方式介绍 1.1 Hash算法加密 1.2. 对称加密 1.3 非对称加密 二、 我们要讲什么? 三、 接口加解密 四、 数据加解密 一、 加解密方式介绍 所有的加密方式我们可以分为三类:对称加密、非对称加密、Hash算法加密。 算法内部的具体实现…...
开创远程就可以监测宠物健康新篇章
在宠物健康监测的新纪元,智能听诊器凭借其先进技术,正逐步改变我们对宠物健康监护的传统认知。这不仅是一款监测工具,而是宠物健康管理的得力助手,为宠物主人和兽医提供前所未有的洞察力和便捷性。 深度学习算法:智能…...
二叉树的基本概念(上)
文章目录 🍊自我介绍🍊简介🍊树的定义树中的专业术语树的分类 🍊二叉树的特性讲解 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞关注评论收藏(一键四连)哦~ 🍊自我介…...
aws s3 存储桶 前端组件上传简单案例
写一个vue3 上传aws oss存储的案例 使用到的插件 npm install aws-sdk/client-s3 注意事项 : 1. 本地调试 , 需要设置在官网设置跨域 必须!!! 否则调试不了 ,前端代理是不起作用的 ,因为是插…...
【开源免费】基于SpringBoot+Vue.JS墙绘产品展示交易平台(JAVA毕业设计)
本文项目编号 T 049 ,文末自助获取源码 \color{red}{T049,文末自助获取源码} T049,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…...
python爬虫初体验(四)—— 百度文库PPT的爬取
文章目录 1. 安装包2. 相关代码3. 说明4. 注意事项5. 扩展功能5.1 多页面下载5.2 输入地址下载 在Python 2中编写一个爬虫来大量下载图片,可以使用requests库来发送HTTP请求,并使用BeautifulSoup来解析HTML页面。此外,可以使用urllib2库来下载…...
下水道内缺陷识别检测数据集 yolo数据集 共2300张
下水道内缺陷识别检测数据集 yolo数据集 共2300张 下水道内部缺陷识别数据集(Sewer Interior Defect Recognition Dataset, SIDRD) 摘要 SIDRD 是一个专门针对下水道内部缺陷识别的数据集,旨在为城市基础设施维护和管理提供一个标准化的训练…...
年轻用户对Facebook的使用趋势分析
在社交媒体的蓬勃发展中,Facebook作为全球最大的社交平台之一,尽管面临着来自新兴平台的竞争,仍然在年轻用户中扮演着重要角色。然而,年轻用户对Facebook的使用方式和趋势却在不断变化。本文将探讨年轻用户对Facebook的使用趋势&a…...
EasyCVR全方位安全守护智慧电厂:构建高效视频监控系统优势分析
随着信息技术的飞速发展和数字化时代的到来,电厂作为能源供应的重要枢纽,其安全性和管理效率成为社会各界关注的焦点。为了满足电厂对高效、智能、可靠视频监控系统的需求,基于EasyCVR平台建设的电厂视频监控系统应运而生。 一、系统构成 基…...
基于深度学习的情感生成与交互
基于深度学习的情感生成与交互是一个新兴的研究领域,旨在通过深度学习技术生成具有情感的反应,以增强人机交互的自然性和有效性。该技术涉及情感识别、自然语言处理、计算机视觉等多个领域,并在多个应用场景中展现出潜力。 情感生成的主要方…...
JavaScript匿名函数
引言 JavaScript是一种广泛使用的脚本语言,用于Web开发和其他领域。在JavaScript中,函数是非常重要的组成部分,它们允许开发者组织代码、复用代码以及执行特定的任务。本文将探讨一种特殊的函数类型——匿名函数,并介绍如何使用它…...
线性判别分析(LDA)中计算两个类的中心点在投影方向w上的投影示例
通过一个具体的例子,详细说明 w T μ 0 w^T \mu_0 wTμ0 和 w T μ 1 w^T \mu_1 wTμ1 如何表示两个类的中心点在投影方向 w w w 上的投影。 假设: 我们有两个类的数据集,均值向量 μ 0 \mu_0 μ0 和 μ 1 \mu_1 μ1ÿ…...
前端知识——标签知识
1.p段落标签 ——一个p标签表示一个段落 单独占一行 >p标签里面不可以嵌套其它的块级标签(div h1~h6 p等) 会导致浏览器自动分裂成两个标签 不规范的写法 >但是可以包裹span标签 2.span标签 ——包裹文字标签 可以和span一行显示 3.文本格式化标签 ——给…...
使用Docker和cpolar在Linux服务器上搭建DashDot监控面板
使用Docker和cpolar在Linux服务器上搭建DashDot监控面板 前言环境准备安装Docker下载Dashdot镜像 部署DashDot应用本地访问DashDot服务安装cpolar内网穿透固定DashDot公网地址结语 前言 在这个数字化飞速发展的时代,服务器作为支撑各种应用和服务的基础设施…...
解决docker拉取镜像报错
报错信息如下: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)网上试了很多方式,有的需要配置DNS解析&…...
C++之STL—deque容器
双端数组 区别于 vector (单端数组), 构造函数 注意:读取数据时,const修饰保证函数内只能读取,不能修改数据 void print(const deque<int>& deq) {for (deque<int>::const iterator it deq.begin(); it ! deq.e…...
leveldb前缀匹配查找Seek
个人随笔 (Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考:https://github.com/google/leveldb/blob/main/include/leveldb/db.h 参考:百度AI 1. 背景 最近偶然发现了,leveldb前缀匹配查找的功能。 之前没有从这个角度去想过See…...
【自动驾驶】ros如何隔绝局域网内其他电脑播包
1.问题 可能碰到自己播包的时候,别人播包的传到我们电脑上,导致无法分析问题,或者出现一些奇怪的现象。 2.解决 export ROS_LOCALHOST_ONLY1 在终端加上这句话,或者在~/.bashrc中添加,通过source ~/.bashrc使其生…...
MySQL程序
目录 MySQL程序 常用的MySQL的程序 mysqld程序 mysql客户端 客户端命令的常用的选项 配置文件 配置文件语法 MySQL客户端命令 编辑 .sql 文件中执行SQL语句 mysqlcheck (表维护程序) Mysqldump(数据库备份程序) mysql…...
吉林省自闭症寄宿学校:提供个性化培养方案
在吉林省的怀抱中,隐藏着一片温馨而特殊的天地——星贝育园自闭症儿童寄宿制学校。这里,不是简单的教育场所,而是无数自闭症儿童梦想启航的港湾,是他们感受爱、学习成长、绽放自我光芒的温馨家园。 自闭症,一个逐渐被…...
铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...
消息队列系统设计与实践全解析
文章目录 🚀 消息队列系统设计与实践全解析🔍 一、消息队列选型1.1 业务场景匹配矩阵1.2 吞吐量/延迟/可靠性权衡💡 权衡决策框架 1.3 运维复杂度评估🔧 运维成本降低策略 🏗️ 二、典型架构设计2.1 分布式事务最终一致…...
02.运算符
目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&:逻辑与 ||:逻辑或 !:逻辑非 短路求值 位运算符 按位与&: 按位或 | 按位取反~ …...
