Skip to content

Commit

Permalink
SpringBoot初始化模板v2.1.9 设计加密模块。
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonyCheng committed Sep 2, 2024
1 parent a02d37f commit 8bad78b
Show file tree
Hide file tree
Showing 33 changed files with 582 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
Expand All @@ -23,6 +24,7 @@
@EnableConfigurationProperties
@ConfigurationPropertiesScan(basePackages = {"top.sharehome.springbootinittemplate.config.**"})
@EnableTransactionManagement
@ServletComponentScan(basePackages = {"top.sharehome.springbootinittemplate.config.**"})
public class MainApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package top.sharehome.springbootinittemplate.config.encrypt;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import top.sharehome.springbootinittemplate.config.encrypt.condition.EncryptCondition;
import top.sharehome.springbootinittemplate.config.encrypt.properties.EncryptProperties;
import top.sharehome.springbootinittemplate.utils.encrypt.AESUtils;
import top.sharehome.springbootinittemplate.utils.encrypt.RSAUtils;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.util.concurrent.CompletableFuture;
Expand All @@ -19,31 +23,33 @@
*/
@Configuration
@Conditional({EncryptCondition.class})
@EnableConfigurationProperties(EncryptProperties.class)
@Slf4j
@Getter
public class EncryptConfiguration {

public static String RSAPublicKey = null;
@Resource
private EncryptProperties encryptProperties;

public static String RSAPrivateKey = null;
private String rsaPublicKey = null;

public static String AESSecretKey = null;
private String rsaPrivateKey = null;

static {
CompletableFuture.runAsync(() -> {
KeyPair keyPair = RSAUtils.generateKeyPair(4096);
RSAPublicKey = RSAUtils.getStringFromPublicKey(keyPair.getPublic());
RSAPrivateKey = RSAUtils.getStringFromPrivateKey(keyPair.getPrivate());
SecretKey secretKey = AESUtils.generateKey(32);
AESSecretKey = AESUtils.getStringFromKey(secretKey);
log.info("############ Encryption Key is Generated");
});
}
private String aesSecretKey = null;

/**
* 依赖注入日志输出
*/
@PostConstruct
private void initDi() {
CompletableFuture.runAsync(() -> {
KeyPair keyPair = RSAUtils.generateKeyPair(encryptProperties.getRsaKeyLength().getKeyLength());
rsaPublicKey = RSAUtils.getStringFromPublicKey(keyPair.getPublic());
rsaPrivateKey = RSAUtils.getStringFromPrivateKey(keyPair.getPrivate());
SecretKey secretKey = AESUtils.generateKey(encryptProperties.getAesKeyLength().getKeyLength());
aesSecretKey = AESUtils.getStringFromKey(secretKey);
log.info("############ Encryption Key is Generated");
});
log.info("############ {} Configuration DI.", this.getClass().getSimpleName().split("\\$\\$")[0]);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package top.sharehome.springbootinittemplate.config.encrypt.controller;

import org.springframework.context.annotation.Conditional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.sharehome.springbootinittemplate.common.base.R;
import top.sharehome.springbootinittemplate.common.validate.PostGroup;
import top.sharehome.springbootinittemplate.config.encrypt.EncryptConfiguration;
import top.sharehome.springbootinittemplate.config.encrypt.annotation.RSADecrypt;
import top.sharehome.springbootinittemplate.config.encrypt.annotation.RSAEncrypt;
import top.sharehome.springbootinittemplate.config.encrypt.condition.EncryptCondition;
import top.sharehome.springbootinittemplate.config.encrypt.controller.model.TestEncryptDto;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
* 加密控制器
*
* @author AntonyCheng
*/
@RestController("/encrypt")
@Conditional(EncryptCondition.class)
public class EncryptController {

@Resource
private EncryptConfiguration encryptConfiguration;

/**
* 获取RSA公钥
*
* @return RSA公钥
*/
@GetMapping("/rsa/public/key")
public R<String> getRsaPublicKey() {
return R.ok(encryptConfiguration.getRsaPublicKey());
}

/**
* 加密测试控制器方法(仅测试使用,使用时取消注释GetMapping注解和RSADecrypt注解)
* 注意:想要运行测试方法,需要获取RSA公钥后自行前往支持在线RSA算法加密的网站进行内容加密,再传入该方法中
*/
@PostMapping("/test")
@RSADecrypt
public R<Map<String, Object>> test(@RequestBody @Validated({PostGroup.class}) TestEncryptDto testEncryptDto, @RequestParam @RSAEncrypt String test) {
return R.ok(new HashMap<>() {
{
put("testEncryptDto", testEncryptDto);
put("test", test);
}
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package top.sharehome.springbootinittemplate.config.encrypt.controller.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import top.sharehome.springbootinittemplate.common.validate.PostGroup;
import top.sharehome.springbootinittemplate.config.encrypt.annotation.RSAEncrypt;

import javax.validation.constraints.Size;
import java.io.Serializable;

/**
* 测试加密Dto类
*
* @author AntonyCheng
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class TestEncryptDto implements Serializable {

@Size(min = 5, max = 10, message = "账号长度介于5-10之间", groups = {PostGroup.class})
private String account;

@Size(min = 5, max = 10, message = "密码长度介于5-10之间", groups = {PostGroup.class})
@RSAEncrypt
private String password;

private static final long serialVersionUID = 1524087173502021198L;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package top.sharehome.springbootinittemplate.config.encrypt.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* AES密钥长度
*
* @author AntonyCheng
*/
@Getter
@AllArgsConstructor
public enum AESKeyLength {

LENGTH16(16),

LENGTH24(24),

LENGTH32(32);

private final Integer keyLength;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package top.sharehome.springbootinittemplate.config.encrypt.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* RSA密钥长度
*
* @author AntonyCheng
*/
@Getter
@AllArgsConstructor
public enum RSAKeyLength {

LENGTH512(512),

LENGTH1024(1024),

LENGTH2048(2048),

LENGTH4096(4096);

private final Integer keyLength;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package top.sharehome.springbootinittemplate.config.encrypt.filter;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import top.sharehome.springbootinittemplate.common.base.R;
import top.sharehome.springbootinittemplate.common.base.ReturnCode;
import top.sharehome.springbootinittemplate.config.bean.SpringContextHolder;
import top.sharehome.springbootinittemplate.config.encrypt.EncryptConfiguration;
import top.sharehome.springbootinittemplate.config.encrypt.annotation.RSADecrypt;
import top.sharehome.springbootinittemplate.config.encrypt.annotation.RSAEncrypt;
import top.sharehome.springbootinittemplate.config.encrypt.condition.EncryptCondition;
import top.sharehome.springbootinittemplate.exception.customize.CustomizeEncryptException;
import top.sharehome.springbootinittemplate.utils.encrypt.RSAUtils;
import top.sharehome.springbootinittemplate.utils.request.ParamsAndBodyRequestWrapper;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.*;

/**
* RSA加密过滤器
* 想要使用该过滤器,需要知道该过滤器对加密参数的识别方法:
* 即先过滤所有RSADecrypt注解的Controller的URI,然后再区分RequestBody和RequestParam注解,
* 如果是RequestBody,则默认是请求体,遍历其对象中所有字段,将带有RSAEncrypt注解且类型为String的字段值进行解密和更新,
* 如果是RequestParam,则默认是请求参数,将带有RSAEncrypt注解且类型为String的参数值进行解密和更新。
*
* @author AntonyCheng
*/
@Conditional(EncryptCondition.class)
@WebFilter(urlPatterns = "/*", filterName = "RSAEncryptFilter")
public class RSAEncryptFilter implements Filter {

@Value("${server.servlet.context-path}")
private String contextPath;

private final Map<String, HandlerMethod> uriMethodMap = new HashMap<>();

@Resource
private EncryptConfiguration encryptConfiguration;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 从 Spring Context 中获取 RequestMappingHandlerMapping
RequestMappingHandlerMapping requestMappingHandlerMapping = SpringContextHolder.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);

// 获取所有的 HandlerMethod 映射
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
HandlerMethod handlerMethod = entry.getValue();

// 检查方法或类是否带有 @RSADecrypt 注解
if (Objects.nonNull(AnnotationUtils.findAnnotation(handlerMethod.getMethod(), RSADecrypt.class)) ||
Objects.nonNull(AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RSADecrypt.class))) {

// 获取 URI pattern 并将其与 HandlerMethod 关联
Set<String> patterns = entry.getKey().getPatternsCondition().getPatterns();
for (String pattern : patterns) {
uriMethodMap.put(pattern, handlerMethod);
}
}
}
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 转换参数
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 判断本次请求是否包含在RSADecrypt注解方法URI列表中,如果在,那么根据请求体注解(RequestBody)和请求参数注解(RequestParam)进行区分操作
HandlerMethod handlerMethod = uriMethodMap.get(request.getRequestURI().replace(contextPath, ""));
if (Objects.nonNull(handlerMethod)) {
ParamsAndBodyRequestWrapper paramsAndBodyRequestWrapper = new ParamsAndBodyRequestWrapper(request);
Parameter[] parameters = handlerMethod.getMethod().getParameters();
for (Parameter parameter : parameters) {
Annotation[] annotations = parameter.getAnnotations();
if (Arrays.stream(annotations).anyMatch(annotation -> annotation instanceof RequestBody)) {
// 如果是 RequestBody 注解,则遍历该对象的所有 String 属性
Class<?> parameterType = parameter.getType();
Field[] declaredFields = parameterType.getDeclaredFields();
ObjectMapper objectMapper = new ObjectMapper();
Object requestBodyObject = objectMapper.readValue(paramsAndBodyRequestWrapper.getInputStream(), parameterType);
for (Field declaredField : declaredFields) {
RSAEncrypt annotation = declaredField.getDeclaredAnnotation(RSAEncrypt.class);
if (!Objects.equals(declaredField.getType(), String.class) || Objects.isNull(annotation)) {
continue;
}
try {
declaredField.setAccessible(true);
String value = (String) declaredField.get(requestBodyObject);
String decrypt = RSAUtils.decrypt(value, encryptConfiguration.getRsaPrivateKey());
declaredField.set(requestBodyObject, decrypt);
} catch (CustomizeEncryptException e) {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(R.fail(e.getReturnCode().getCode(), e.getMessage())));
writer.flush();
return;
} catch (IllegalAccessException e) {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(R.fail(ReturnCode.FAIL.getCode(), e.getMessage())));
writer.flush();
return;
} catch (Exception ignore) {
}
}
paramsAndBodyRequestWrapper.updateOrAddRequestBody(requestBodyObject);
} else if (Arrays.stream(annotations).anyMatch(annotation -> annotation instanceof RequestParam)) {
RSAEncrypt annotation = parameter.getDeclaredAnnotation(RSAEncrypt.class);
if (!Objects.equals(parameter.getType(), String.class) || Objects.isNull(annotation)) {
continue;
}
String key = parameter.getName();
String value = paramsAndBodyRequestWrapper.getParameter(key);
try {
String decrypt = RSAUtils.decrypt(value, encryptConfiguration.getRsaPrivateKey());
paramsAndBodyRequestWrapper.updateOrAddParams(key, decrypt);
} catch (CustomizeEncryptException e) {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(R.fail(e.getReturnCode().getCode(), e.getMessage())));
writer.flush();
return;
}
}
}
filterChain.doFilter(paramsAndBodyRequestWrapper, response);
} else {
filterChain.doFilter(request, response);
}
}

}
Loading

0 comments on commit 8bad78b

Please sign in to comment.