Initial commit

This commit is contained in:
wzy-warehouse
2026-04-07 20:00:15 +08:00
committed by GitHub
commit 2962c3ff6b
32 changed files with 2178 additions and 0 deletions
@@ -0,0 +1,29 @@
package com.gis.basic_template_not_login_back;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
* 后台启动入口
* 启动时过滤DataSourceAutoConfiguration,避免数据源自动配置
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan("com.gis.basic_template_not_login_back.mapper") // 扫描MyBatis的Mapper接口
public class BasicTemplateNotLoginBackApplication {
// 使用非静态变量
@Value("${server.port}")
private Integer port;
public static void main(String[] args) {
// 启动Spring Boot应用并获取应用上下文
var context = SpringApplication.run(BasicTemplateNotLoginBackApplication.class, args);
// 从上下文中获取当前实例
BasicTemplateNotLoginBackApplication app = context.getBean(BasicTemplateNotLoginBackApplication.class);
System.out.println("后端服务启动成功!访问地址: http://localhost:" + app.port);
}
}
@@ -0,0 +1,28 @@
package com.gis.basic_template_not_login_back.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 加解密配置属性类
* 从application.yml中读取safety.crypto配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "safety.crypto")
public class CryptoProperties {
/**
* 响应无需加密的路径列表
*/
private List<String> noEncryptPaths = Collections.emptyList();
/**
* 请求无需解密的路径列表
*/
private List<String> noDecryptPaths = Collections.emptyList();
}
@@ -0,0 +1,20 @@
package com.gis.basic_template_not_login_back.config;
import java.lang.annotation.*;
/**
* 数据源切换注解
* 可用于类或方法上,指定使用的数据源
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 数据源名称
* 可选值:master(主库)、slave等
* @return 数据源名称
*/
String value() default "master";
}
@@ -0,0 +1,27 @@
package com.gis.basic_template_not_login_back.config;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1)
@Slf4j
public class DataSourceAspect {
@Around("@annotation(dataSource) || @within(dataSource)")
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
try {
String dsName = dataSource.value();
log.debug("切换数据源: {}", dsName);
DataSourceContextHolder.setDataSource(dsName);
return point.proceed();
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
@@ -0,0 +1,39 @@
package com.gis.basic_template_not_login_back.config;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource master() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slave() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dataSource() {
DynamicDataSource ds = new DynamicDataSource();
Map<Object, Object> map = new HashMap<>();
map.put("master", master());
map.put("slave", slave());
ds.setTargetDataSources(map);
ds.setDefaultTargetDataSource(master());
return ds;
}
}
@@ -0,0 +1,42 @@
package com.gis.basic_template_not_login_back.config;
/**
* 数据源上下文持有者
* 使用ThreadLocal存储当前线程使用的数据源名称
*/
public class DataSourceContextHolder {
/**
* 默认数据源名称
*/
private static final String DEFAULT_DATASOURCE = "master";
/**
* ThreadLocal存储数据源名称
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源名称
* @param dataSourceName 数据源名称
*/
public static void setDataSource(String dataSourceName) {
CONTEXT_HOLDER.set(dataSourceName);
}
/**
* 获取数据源名称
* @return 数据源名称
*/
public static String getDataSource() {
String dataSource = CONTEXT_HOLDER.get();
return dataSource == null ? DEFAULT_DATASOURCE : dataSource;
}
/**
* 清除数据源名称
*/
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
@@ -0,0 +1,15 @@
package com.gis.basic_template_not_login_back.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源路由
* 根据DataSourceContextHolder中设置的数据源名称,动态选择数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
@@ -0,0 +1,32 @@
package com.gis.basic_template_not_login_back.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 字符串序列化器(key 用字符串存储)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// JSON 序列化器(value 用 JSON 存储,支持对象)
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(new ObjectMapper(), Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
@@ -0,0 +1,23 @@
package com.gis.basic_template_not_login_back.controller;
import com.gis.basic_template_not_login_back.domain.ApiResponse;
import com.gis.basic_template_not_login_back.service.ex.ServiceException;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* 基础controller类,所有controller层对象必须继承这个类
*/
public class BaseController{
// 用于统一处理应用层抛出的异常
@ExceptionHandler(ServiceException.class)
public ApiResponse<Void> handleServiceException(Throwable e) {
return ApiResponse.error(e.getMessage());
}
// 用于统一处理其余抛出的异常
@ExceptionHandler(RuntimeException.class)
public ApiResponse<Void> handleException(Throwable e) {
return ApiResponse.error(e.getMessage());
}
}
@@ -0,0 +1,52 @@
package com.gis.basic_template_not_login_back.controller;
import com.gis.basic_template_not_login_back.domain.ApiResponse;
import com.gis.basic_template_not_login_back.utils.safety.SM2Utils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/crypto")
public class CryptoController extends BaseController {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Value("${safety.sm2.global}")
private String sm2KeyPairRedisKey;
/**
* 初始化SM2密钥对
*/
@PostConstruct
public void initSm2KeyPair() {
Object sm2KeyPairObj = redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
if (sm2KeyPairObj == null) {
Map<String, String> sm2KeyPair = SM2Utils.generateKeyPair();
redisTemplate.opsForValue().set(sm2KeyPairRedisKey, sm2KeyPair);
System.out.println("SM2密钥对已生成并存储到Redis");
}
}
/**
* 获取SM2公钥
*/
@GetMapping("/sm2/public-key")
@SuppressWarnings("unchecked")
public ApiResponse<Map<String, String>> getSm2PublicKey() {
Map<String, String> sm2KeyPair = (Map<String, String>) redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
if (sm2KeyPair == null) {
throw new RuntimeException("SM2密钥对未初始化");
}
Map<String, String> result = new HashMap<>();
result.put("publicKey", sm2KeyPair.get("publicKey"));
return ApiResponse.ok(result);
}
}
@@ -0,0 +1,65 @@
package com.gis.basic_template_not_login_back.domain;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class ApiResponse<T> implements Serializable {
@Serial
private static final long serialVersionUID = -7318963194081396360L;
private int code;
private String message;
private T data;
public ApiResponse() {}
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public ApiResponse(int code, String message) {
this.code = code;
this.message = message;
}
// 成功响应 - 无数据
public static ApiResponse<Void> ok() {
return new ApiResponse<>(200, "success");
}
// 成功响应 - 带数据
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(200, "success", data);
}
// 成功响应 - 自定义消息和数据
public static <T> ApiResponse<T> ok(String message, T data) {
return new ApiResponse<>(200, message, data);
}
// 错误响应 - 默认错误
public static ApiResponse<Void> error() {
return new ApiResponse<>(500, "error");
}
// 错误响应 - 自定义错误消息
public static ApiResponse<Void> error(String message) {
return new ApiResponse<>(500, message);
}
// 错误响应 - 自定义状态码和错误消息
public static ApiResponse<Void> error(Integer code, String message) {
return new ApiResponse<>(code, message);
}
// 错误相应,自定义所有信息
public static <T> ApiResponse<T> error(Integer code, String message, T data) {
return new ApiResponse<>(code, message, data);
}
}
@@ -0,0 +1,292 @@
package com.gis.basic_template_not_login_back.filter;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.gis.basic_template_not_login_back.config.CryptoProperties;
import com.gis.basic_template_not_login_back.utils.safety.SM2Utils;
import com.gis.basic_template_not_login_back.utils.safety.SM4Utils;
import com.gis.basic_template_not_login_back.wrapper.Sm4KeyHolder;
import jakarta.annotation.Resource;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 请求解密过滤器
*/
@WebFilter(urlPatterns = "/*")
@Order(1)
@Component
public class DecryptFilter implements Filter {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Value("${safety.sm2.global}")
private String sm2KeyPairRedisKey;
@Resource
private CryptoProperties cryptoProperties;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestUri = request.getRequestURI();
// 检查是否为无需解密的接口
if (isNoDecryptPath(requestUri)) {
chain.doFilter(request, servletResponse);
return;
}
try {
// 提取加密参数
String[] encryptedParams = extractEncryptedParams(request);
String encryptedData = encryptedParams[0];
String sm4KeyEncrypted = encryptedParams[1];
// 校验加密参数(仅校验sm4KeyEncrypted,确保必传)
if (!StringUtils.hasText(sm4KeyEncrypted)) {
throw new IllegalArgumentException("加密参数缺失(sm4KeyEncrypted");
}
// 解密SM4密钥
String sm2PrivateKey = getSm2PrivateKey();
String sm4Key = SM2Utils.decrypt(sm2PrivateKey, sm4KeyEncrypted);
// 存储SM4密钥
Sm4KeyHolder.setSm4Key(sm4Key);
// 仅当encryptedData有实际内容时才解密(排除null和空字符串)
String decryptedData = null;
if (StringUtils.hasText(encryptedData)) {
decryptedData = SM4Utils.decrypt(sm4Key, encryptedData);
}
// 包装解密后的请求(兼容空数据场景)
DecryptRequestWrapper wrappedRequest = wrapDecryptedRequest(request, decryptedData);
chain.doFilter(wrappedRequest, servletResponse);
} catch (Exception e) {
handleDecryptError(servletResponse, e);
}
}
/**
* 检查是否为无需解密的路径
*/
private boolean isNoDecryptPath(String requestUri) {
for (String path : cryptoProperties.getNoDecryptPaths()) {
if (requestUri.contains(path)) {
return true;
}
}
return false;
}
/**
* 提取加密参数
*/
private String[] extractEncryptedParams(HttpServletRequest request) throws IOException {
String encryptedData = null;
String sm4KeyEncrypted = null;
String method = request.getMethod();
String contentType = request.getContentType();
if ("GET".equalsIgnoreCase(method)) {
// GET请求从Query参数提取(允许参数为空字符串)
encryptedData = request.getParameter("encryptedData");
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
} else {
// POST/PUT请求从Body提取
if (contentType == null) {
throw new IllegalArgumentException("请求Content-Type不能为空");
}
if (contentType.contains("application/json")) {
String jsonBody = readRequestBody(request);
// 空JSON体处理(避免解析空字符串报错)
if (StringUtils.hasText(jsonBody)) {
Map<String, Object> bodyMap = JSON.parseObject(jsonBody, new TypeReference<Map<String, Object>>() {});
encryptedData = (String) bodyMap.get("encryptedData");
sm4KeyEncrypted = (String) bodyMap.get("sm4KeyEncrypted");
}
} else if (contentType.contains("multipart/form-data")) {
// FormData从表单参数提取
encryptedData = request.getParameter("encryptedData");
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
} else if (contentType.contains("application/x-www-form-urlencoded")) {
encryptedData = request.getParameter("encryptedData");
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
} else {
throw new IllegalArgumentException("不支持的Content-Type: " + contentType);
}
}
return new String[]{encryptedData, sm4KeyEncrypted};
}
/**
* 获取SM2私钥
*/
@SuppressWarnings("unchecked")
private String getSm2PrivateKey() {
Map<String, String> sm2KeyPair = (Map<String, String>) redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
if (sm2KeyPair == null || !sm2KeyPair.containsKey("privateKey")) {
throw new RuntimeException("Redis中未找到SM2私钥,请先初始化密钥对");
}
return sm2KeyPair.get("privateKey");
}
/**
* 包装解密后的请求(兼容空数据)
*/
private DecryptRequestWrapper wrapDecryptedRequest(HttpServletRequest request, String decryptedData) {
if ("GET".equalsIgnoreCase(request.getMethod())) {
// GET请求:解析为参数Map(空数据返回空Map)
Map<String, String[]> originalParams = parseGetParams(decryptedData);
DecryptRequestWrapper wrapper = new DecryptRequestWrapper(request, new byte[0]);
wrapper.setDecryptedParams(originalParams);
return wrapper;
} else {
// POST请求:空数据转换为空字节数组(避免null)
byte[] decryptedBody = StringUtils.hasText(decryptedData)
? decryptedData.getBytes(StandardCharsets.UTF_8)
: new byte[0];
return new DecryptRequestWrapper(request, decryptedBody);
}
}
/**
* 解析GET参数(兼容空数据)
*/
private Map<String, String[]> parseGetParams(String decryptedData) {
// 空数据直接返回空Map,避免JSON解析报错
if (!StringUtils.hasText(decryptedData)) {
return new HashMap<>();
}
Map<String, Object> paramMap = JSON.parseObject(decryptedData);
Map<String, String[]> result = new HashMap<>(paramMap.size());
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value == null) {
result.put(key, new String[0]);
} else if (value instanceof String) {
result.put(key, new String[]{(String) value});
} else {
result.put(key, new String[]{value.toString()});
}
}
return result;
}
/**
* 读取请求体
*/
private String readRequestBody(HttpServletRequest request) throws IOException {
try (ServletInputStream is = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
/**
* 处理解密错误
*/
private void handleDecryptError(ServletResponse servletResponse, Exception e) throws IOException {
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.setCharacterEncoding("UTF-8");
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("code", 500);
errorResult.put("msg", "请求解密失败: " + e.getMessage());
errorResult.put("success", false);
PrintWriter writer = servletResponse.getWriter();
writer.write(JSON.toJSONString(errorResult));
writer.flush();
}
/**
* 自定义请求包装类
*/
public static class DecryptRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {
private final byte[] decryptedBody;
@Setter
private Map<String, String[]> decryptedParams;
public DecryptRequestWrapper(HttpServletRequest request, byte[] decryptedBody) {
super(request);
this.decryptedBody = decryptedBody;
this.decryptedParams = new HashMap<>(request.getParameterMap());
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream bis = new ByteArrayInputStream(decryptedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bis.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
// 无需实现
}
@Override
public int read() {
return bis.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
@Override
public String getParameter(String name) {
String[] values = decryptedParams.get(name);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return decryptedParams;
}
@Override
public String[] getParameterValues(String name) {
return decryptedParams.get(name);
}
}
}
@@ -0,0 +1,25 @@
package com.gis.basic_template_not_login_back.service.ex;
/**
* 业务层所有自定义异常父类
*/
public class ServiceException extends RuntimeException {
public ServiceException() {
}
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -0,0 +1,106 @@
package com.gis.basic_template_not_login_back.utils.safety;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
/**
* SM2工具类(与前端gm-crypto兼容,加解密使用Hex编码)
*/
public class SM2Utils {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
private static final ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
/**
* 生成SM2密钥对
*/
public static Map<String, String> generateKeyPair() {
try {
ECKeyPairGenerator generator = new ECKeyPairGenerator();
ECKeyGenerationParameters genParams = new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom());
generator.init(genParams);
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
// 公钥:非压缩格式(64字节,包含0x04前缀共65字节)
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
String publicKeyHex = Hex.toHexString(publicKey.getQ().getEncoded(false));
// 私钥:32字节Hex字符串
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
String privateKeyHex = privateKey.getD().toString(16);
// 补零到64位
privateKeyHex = String.format("%64s", privateKeyHex).replace(' ', '0');
Map<String, String> keyMap = new HashMap<>(2);
keyMap.put("publicKey", publicKeyHex);
keyMap.put("privateKey", privateKeyHex);
return keyMap;
} catch (Exception e) {
throw new RuntimeException("生成SM2密钥对失败: " + e.getMessage(), e);
}
}
/**
* SM2公钥加密(返回Hex字符串)
*/
public static String encrypt(String publicKeyHex, String plaintext) {
try {
// 解析带0x04前缀的公钥(65字节)
byte[] publicKeyBytes = Hex.decode(publicKeyHex);
ECPublicKeyParameters publicKey = new ECPublicKeyParameters(x9ECParameters.getCurve().decodePoint(publicKeyBytes), ecDomainParameters);
// 初始化加密器(C1C3C2模式)
org.bouncycastle.crypto.engines.SM2Engine engine = new org.bouncycastle.crypto.engines.SM2Engine((org.bouncycastle.crypto.engines.SM2Engine.Mode.C1C3C2));
engine.init(true, new ParametersWithRandom(publicKey, new SecureRandom()));
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
byte[] encryptedBytes = engine.processBlock(plaintextBytes, 0, plaintextBytes.length);
// 加密结果转为Hex字符串
return Hex.toHexString(encryptedBytes);
} catch (Exception e) {
throw new RuntimeException("SM2加密失败: " + e.getMessage(), e);
}
}
/**
* SM2私钥解密(接收Hex字符串密文)
*/
public static String decrypt(String privateKeyHex, String ciphertextHex) {
try {
// 解析私钥
byte[] privateKeyBytes = Hex.decode(privateKeyHex);
ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(new BigInteger(1, privateKeyBytes), ecDomainParameters);
// 解码Hex格式密文
byte[] ciphertextBytes = Hex.decode(ciphertextHex);
// 初始化解密器
org.bouncycastle.crypto.engines.SM2Engine engine = new org.bouncycastle.crypto.engines.SM2Engine(org.bouncycastle.crypto.engines.SM2Engine.Mode.C1C3C2);
engine.init(false, privateKey);
byte[] decryptedBytes = engine.processBlock(ciphertextBytes, 0, ciphertextBytes.length);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM2解密失败: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,46 @@
package com.gis.basic_template_not_login_back.utils.safety;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
/**
* SM3算法工具类(纯算法实现,不依赖业务逻辑)
* 仅负责:计算字节数组的SM3哈希值、字节数组与十六进制转换
*/
public class SM3Utils {
/**
* 计算字节数组的SM3哈希值
* @param input 输入字节数组(可任意数据,如盐值+密码的组合)
* @return SM3哈希值(32字节)
*/
public static byte[] hash(byte[] input) {
if (input == null || input.length == 0) {
throw new IllegalArgumentException("输入字节数组不能为空");
}
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] hashBytes = new byte[digest.getDigestSize()];
digest.doFinal(hashBytes, 0);
return hashBytes;
}
/**
* 字节数组转16进制字符串(用于哈希值可视化)
*/
public static String toHex(byte[] bytes) {
if (bytes == null) {
return null;
}
return Hex.toHexString(bytes);
}
/**
* 16进制字符串转字节数组(反向操作)
*/
public static byte[] fromHex(String hexStr) {
if (hexStr == null || hexStr.isEmpty()) {
return null;
}
return Hex.decode(hexStr);
}
}
@@ -0,0 +1,82 @@
package com.gis.basic_template_not_login_back.utils.safety;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Hex;
import java.nio.charset.StandardCharsets;
/**
* SM4工具类(ECB模式,与前端兼容,加解密使用Hex编码)
*/
public class SM4Utils {
/**
* SM4加密(ECB模式,返回Hex字符串)
*/
public static String encrypt(String keyHex, String plaintext) {
try {
// 解析密钥(16字节=32位Hex字符串)
byte[] key = Hex.decode(keyHex);
if (key.length != 16) {
throw new IllegalArgumentException("SM4密钥必须是16字节(32位Hex字符串)");
}
// 初始化加密器(ECB模式 + PKCS7填充)
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
new SM4Engine(),
new PKCS7Padding()
);
CipherParameters params = new KeyParameter(key);
cipher.init(true, params);
// 处理明文
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
byte[] output = new byte[cipher.getOutputSize(plaintextBytes.length)];
int length = cipher.processBytes(plaintextBytes, 0, plaintextBytes.length, output, 0);
length += cipher.doFinal(output, length);
// 加密结果转为Hex字符串
return Hex.toHexString(output, 0, length);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败: " + e.getMessage(), e);
}
}
/**
* SM4解密(ECB模式,接收Hex字符串密文)
*/
public static String decrypt(String keyHex, String ciphertextHex) {
try {
// 解析密钥(16字节=32位Hex字符串)
byte[] key = Hex.decode(keyHex);
if (key.length != 16) {
throw new IllegalArgumentException("SM4密钥必须是16字节(32位Hex字符串)");
}
// 解码Hex格式密文
byte[] ciphertextBytes = Hex.decode(ciphertextHex);
// 初始化解密器
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
new SM4Engine(),
new PKCS7Padding()
);
CipherParameters params = new KeyParameter(key);
cipher.init(false, params);
// 处理密文
byte[] output = new byte[cipher.getOutputSize(ciphertextBytes.length)];
int length = cipher.processBytes(ciphertextBytes, 0, ciphertextBytes.length, output, 0);
length += cipher.doFinal(output, length);
// 解密结果转为字符串
return new String(output, 0, length, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4解密失败: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,68 @@
package com.gis.basic_template_not_login_back.wrapper;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.Setter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 解密拦截器
*/
public class DecryptRequestWrapper extends HttpServletRequestWrapper {
// 缓存解密后的请求体(用于POST等带Body的请求)
private final byte[] decryptedBody;
// 用于更新解密后的参数(GET场景)
// 缓存解密后的参数(用于GET等带Query参数的请求)
@Setter
private Map<String, String[]> decryptedParams;
public DecryptRequestWrapper(HttpServletRequest request, byte[] decryptedBody) {
super(request);
this.decryptedBody = decryptedBody;
this.decryptedParams = new HashMap<>(request.getParameterMap());
}
// 重写输入流,返回解密后的Body
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(decryptedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bis.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {}
@Override
public int read() throws IOException {
return bis.read();
}
};
}
// 重写参数获取方法,返回解密后的参数(用于GET)
@Override
public String getParameter(String name) {
String[] values = decryptedParams.get(name);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return decryptedParams;
}
}
@@ -0,0 +1,77 @@
package com.gis.basic_template_not_login_back.wrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gis.basic_template_not_login_back.config.CryptoProperties;
import com.gis.basic_template_not_login_back.utils.safety.SM4Utils;
import jakarta.annotation.Resource;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import jakarta.servlet.http.HttpServletRequest;
/**
* 响应数据加密拦截器
*/
@ControllerAdvice
public class EncryptResponseAdvice implements ResponseBodyAdvice<Object> {
@Resource
private ObjectMapper objectMapper;
@Resource
private CryptoProperties cryptoProperties;
/**
* 判断是否需要加密:排除特定路径,其余全部加密
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 获取当前请求的URI
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return false; // 非Web请求场景,不加密
}
HttpServletRequest request = attributes.getRequest();
String requestUri = request.getRequestURI();
// 检查是否为无需加密的路径
for (String path : cryptoProperties.getNoEncryptPaths()) {
if (requestUri.contains(path)) {
return false; // 排除路径,不加密
}
}
// 其余路径均需要加密
return true;
}
/**
* 响应体加密逻辑(保持不变)
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
String sm4Key = Sm4KeyHolder.getSm4Key();
if (sm4Key == null || sm4Key.length() != 32) {
throw new RuntimeException("SM4密钥不存在或格式错误,无法加密响应");
}
String plaintext = objectMapper.writeValueAsString(body);
String encryptedText = SM4Utils.encrypt(sm4Key, plaintext);
return encryptedText;
} catch (Exception e) {
throw new RuntimeException("响应数据加密失败: " + e.getMessage(), e);
} finally {
Sm4KeyHolder.clear(); // 清除线程本地存储,避免内存泄漏
}
}
}
@@ -0,0 +1,24 @@
package com.gis.basic_template_not_login_back.wrapper;
/**
* SM4密钥存储
*/
public class Sm4KeyHolder {
// 线程本地存储SM4密钥(Hex格式,32位字符串)
private static final ThreadLocal<String> SM4_KEY_HOLDER = new ThreadLocal<>();
// 设置当前线程的SM4密钥
public static void setSm4Key(String sm4Key) {
SM4_KEY_HOLDER.set(sm4Key);
}
// 获取当前线程的SM4密钥
public static String getSm4Key() {
return SM4_KEY_HOLDER.get();
}
// 清除线程本地存储(避免内存泄漏)
public static void clear() {
SM4_KEY_HOLDER.remove();
}
}
@@ -0,0 +1,38 @@
spring:
datasource:
# Druid 公共配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin
# 主库
master:
url: jdbc:postgresql://47.92.216.173:7654/xian?characterEncoding=utf8&TimeZone=Asia/Shanghai
username: postgres
password: zhangsan
driver-class-name: org.postgresql.Driver
# 从库
slave:
url: jdbc:postgresql://47.92.216.173:7654/assess_disaster?characterEncoding=utf8&TimeZone=Asia/Shanghai
username: postgres
password: zhangsan
driver-class-name: org.postgresql.Driver
@@ -0,0 +1,35 @@
spring:
datasource:
# Druid 公共配置
druid:
initial-size: 10
min-idle: 10
max-active: 50
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: false
# 主库
master:
url: jdbc:postgresql://10.22.245.138:54321/xianDC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: zaihailian
password: XAYJ@gis2603
driver-class-name: org.postgresql.Driver
# 从库
slave:
url: jdbc:postgresql://10.22.245.138:54321/xianDCAccess?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: zaihailian
password: XAYJ@gis2603
driver-class-name: org.postgresql.Driver
+42
View File
@@ -0,0 +1,42 @@
# 开发环境配置
spring:
config:
import: classpath:application-database-dev.yml
# redis
data:
redis:
host: 47.92.216.173
port: 7655
password: zhangsan
database: 0
connect-timeout: 3000ms
# 日志配置
logging:
level:
root: INFO
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
file:
name: ./logs/springboot-app.log
logback:
rollingpolicy:
file-name-pattern: ./logs/springboot-app-%d{yyyy-MM-dd}.%i.log
max-file-size: 100MB
max-history: 7
total-size-cap: 1GB
clean-history-on-start: true
# 安全配置
safety:
# 加解密过滤路径配置
crypto:
# 响应无需加密的路径
no-encrypt-paths:
- /crypto/sm2/public-key
- /druid
# 请求无需解密的路径
no-decrypt-paths:
- /crypto/sm2/public-key
- /druid
+41
View File
@@ -0,0 +1,41 @@
# 生产环境配置
spring:
config:
import: classpath:application-database-prod.yml
# redis
data:
redis:
host: 10.22.245.246
port: 6379
password: XAYJ@gis2603
database: 0
connect-timeout: 3000ms
# 日志配置
logging:
level:
root: WARN
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
file:
name: ./logs/springboot-app.log
logback:
rollingpolicy:
file-name-pattern: ./logs/springboot-app-%d{yyyy-MM-dd}.%i.log
max-file-size: 100MB
max-history: 7
total-size-cap: 1GB
clean-history-on-start: true
# 安全配置
safety:
# 加解密过滤路径配置
crypto:
# 响应无需加密的路径
no-encrypt-paths:
- /crypto/sm2/public-key
# 请求无需解密的路径
no-decrypt-paths:
- /crypto/sm2/public-key
+21
View File
@@ -0,0 +1,21 @@
# 端口
server:
port: 8080
# 激活的配置文件(默认dev,打包时会被Maven profile替换)
spring:
profiles:
active: @spring.profiles.active@
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.gis.basic_template_not_login_back.entity
configuration:
map-underscore-to-camel-case: true
# 安全配置
safety:
sm2:
# sm2公钥在redis存储名
global: 'sm2:keypair:global'