参数加解密

This commit is contained in:
mashuai 2025-04-25 09:11:45 +08:00
parent dd96c4bfe5
commit 4cca38069e
7 changed files with 561 additions and 1 deletions

View File

@ -0,0 +1,39 @@
package com.bonus.sgzb.auth.controller;
import com.bonus.sgzb.common.core.domain.R;
import com.bonus.sgzb.common.core.utils.SystemConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author ma_sh
* @create 2025/4/24 13:35
* 获取配置信息
*/
@RestController
@Slf4j
@RefreshScope
public class ConfigController {
@Resource
private SystemConfig systemConfig;
@GetMapping("getConfig")
public R<Object> getConfig() {
Map<String, Object> map = new HashMap<>();
map.put("loginConfig", systemConfig.getLoginConfig());
map.put("registersConfig", systemConfig.getRegistersConfig());
map.put("isAdmin", systemConfig.isAdmin());
map.put("webSocketurl", systemConfig.getWebsocketurl());
map.put("isAddRootCompany", systemConfig.isAddRootCompany());
map.put("requestConfig", systemConfig.getRequestConfig());
map.put("passwordConfig", systemConfig.getPasswordConfig());
map.put("addAddress", systemConfig.isAddAddress());
return R.ok(map);
}
}

View File

@ -123,6 +123,12 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -0,0 +1,32 @@
package com.bonus.sgzb.common.core.utils.encryption;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.digest.SM3;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
* @Author ma_sh
* @create 2025/4/24 11:10
* @Descr sm3加解密
*/
public class Sm3Util {
static SM3 sm3 = SmUtil.sm3WithSalt("2cc0c5f9f1749f1632efa9f63e902323".getBytes(StandardCharsets.UTF_8));
public static String encrypt(String data) {
return Sm3Util.sm3.digestHex(data);
}
public static String encrypt(InputStream data) {
return Sm3Util.sm3.digestHex(data);
}
public static String encrypt(File dataFile) {
return Sm3Util.sm3.digestHex(dataFile);
}
}

View File

@ -0,0 +1,69 @@
package com.bonus.sgzb.common.core.utils.encryption;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.SM4;
/**
* @Author ma_sh
* @create 2025/4/24 11:07
* @Descr sm4加解密
*/
public class Sm4Utils {
/**
* 必须是16字节
*/
private static final String KEY = "78d1295afa99449b99d6f83820e6965c";
private static final String IV = "f555adf6c01d0ab0761e626a2dae34a2";
/**
* 加密数据使用固定盐
*
* @param plainText 明文待加密的字符串
* @return 加密后的密文包含盐Hex 编码格式如果加密异常就返回传入的字符串
*/
public static String encrypt(String plainText) {
try {
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding, HexUtil.decodeHex(KEY),HexUtil.decodeHex(IV));
// 加密带盐的明文
byte[] encryptedData = sm4.encrypt(plainText);
// 返回带盐的加密结果Hex编码
return HexUtil.encodeHexStr(encryptedData);
} catch (Exception e) {
return plainText; // 发生异常时返回传入字符串
}
}
/**
* 解密数据使用固定盐
*
* @param cipherText 密文包含盐Hex 编码格式的字符串
* @return 解密后的明文字符串如果解密异常就返回传入的字符串
*/
public static String decrypt(String cipherText) {
try {
// 初始化SM4解密工具
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5Padding, HexUtil.decodeHex(KEY),HexUtil.decodeHex(IV));
// 解密数据
byte[] decryptedData = sm4.decrypt(cipherText);
return new String(decryptedData);
} catch (Exception e) {
return cipherText; // 发生异常时返回传入字符串
}
}
// 测试方法演示加密和解密过程
public static void main(String[] args) {
String plainText = "2ab792d986726b6023b1121da6dfebf2a0d3781530e05517cf7175139da04dfd5a26e3c055dfe6b630162e77095905997a86ac58af4dcba544103a7d487fe67793d4e78917c62d6980d1cbc58b4cc4597f69d992cc0736c08476211fc41053dad41718f29b9bd50df9ba99a5e354fd09f05b0cf32cfba639d22c59f704df055ed261fab5efa9f10fc29ed29b38144789bec6de4e3e864cc7c1d6c117beb1ad9a0876c5184d247cd35c49256fd1803ea67540e3433288808e2b798837c76143f6";
System.out.println("原文: " + plainText);
/*// 加密明文
String encryptedText = Sm4Utils.encrypt(plainText);
System.out.println("加密后: " + encryptedText);*/
// 解密密文
String decryptedText = Sm4Utils.decrypt(plainText);
System.out.println("解密后: " + decryptedText);
}
}

View File

@ -0,0 +1,260 @@
package com.bonus.sgzb.gateway.filter;
import com.bonus.sgzb.common.core.exception.CaptchaException;
import com.bonus.sgzb.common.core.utils.encryption.Sm3Util;
import com.bonus.sgzb.common.core.utils.encryption.Sm4Utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @Author ma_sh
* @create 2025/4/24 11:04
* 请求内容存储 处理请求内容 内容放在gatewayContext中
* 解决数据流被重复读取无数据的 问题
* 对formData 数据进行解密
*/
@Component
@Slf4j
public class RequestCoverFilter implements GlobalFilter, Ordered {
/**
* 数据加密标志
*/
public static final String ENCRYPT = "encryptRequest";
/**
* 数据完整性校验标志
*/
public static final String INTEGRALITY = "checkIntegrity";
/**
* 完整性校验哈希值
*/
public static final String HMAC_HEADER_NAME = "Params-Hash";
/**
* 处理Web请求可选地通过给定的 {@link GatewayFilterChain} 委托给下一个 {@code GatewayFilter}
*
* @param exchange 当前服务器交换
* @param chain 提供委托给下一个过滤器的方式
* @return {@code Mono<Void>} 表示请求处理完成的指示
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 如果解密和完整性校验均未启用则直接通过过滤链
ServerHttpRequest request = exchange.getRequest();
MediaType contentType = request.getHeaders().getContentType();
if (contentType == null) {
log.info("请求头中无Content-Type信息直接继续过滤链。");
return chain.filter(exchange);
// return handleUrlParams(exchange, chain);
} else if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return handleBodyRequest(exchange, chain);
} else {
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
/**
* 处理请求体里的参数
*
* @param exchange 当前服务器交换
* @param chain 提供委托给下一个过滤器的方式
* @return {@code Mono<Void>} 表示请求处理完成的指示
*/
private Mono<Void> handleBodyRequest(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
boolean integrality = "true".equalsIgnoreCase(request.getHeaders().getFirst(INTEGRALITY));
boolean encrypt = "true".equalsIgnoreCase(request.getHeaders().getFirst(ENCRYPT));
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
byte[] body = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(body);
DataBufferUtils.release(dataBuffer);
String requestBody = new String(body, StandardCharsets.UTF_8);
// 去掉多余的引号如果有
if (requestBody.startsWith("\"") && requestBody.endsWith("\"")) {
requestBody = requestBody.substring(1, requestBody.length() - 1);
}
if (ObjectUtils.isEmpty(requestBody)) {
return exchange.getResponse().setComplete();
}
// 解密请求体
if (encrypt) {
try {
requestBody = Sm4Utils.decrypt(requestBody);
} catch (Exception e) {
log.error("解密请求体时发生错误: {}", e.getMessage(), e);
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return exchange.getResponse().setComplete();
}
}
if (ObjectUtils.isEmpty(requestBody)) {
return exchange.getResponse().setComplete();
}
// 校验数据完整性
/*if (integrality) {
String providedHmac = requestBody.split("\\|")[1];
integrityVerification(providedHmac, requestBody.split("\\|")[0]);
}*/
requestBody = requestBody.split("\\|")[0];
if (ObjectUtils.isEmpty(requestBody)) {
return exchange.getResponse().setComplete();
}
// 创建新的请求体
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
DataBuffer newBody = bufferFactory.wrap(requestBody.getBytes(StandardCharsets.UTF_8));
ServerHttpRequest newRequest = createNewRequest(exchange, newBody);
return chain.filter(exchange.mutate().request(newRequest).build());
});
}
/**
* 创建包含新请求体的请求
*
* @param exchange 当前服务器交换
* @param newBody 新的请求体数据缓冲区
* @return 新的ServerHttpRequest对象
*/
private ServerHttpRequest createNewRequest(ServerWebExchange exchange, DataBuffer newBody) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return Flux.just(newBody);
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(newBody.readableByteCount());
return headers;
}
};
}
/**
* 处理url后拼接的参数
*
* @param exchange 当前服务器交换
* @param chain 提供委托给下一个过滤器的方式
* @return {@code Mono<Void>} 表示请求处理完成的指示
*/
private Mono<Void> handleUrlParams(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerWebExchange updatedExchange = updateRequestParam(exchange);
if (updatedExchange != null) {
return chain.filter(updatedExchange);
} else {
return chain.filter(exchange);
}
} catch (Exception e) {
log.error("处理 GET 请求时发生错误: {}", e.getMessage(), e);
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return exchange.getResponse().setComplete();
}
}
/**
* 更新查询参数解密和验证数据完整性
*
* @param exchange 当前服务器交换
* @return 更新后的ServerWebExchange对象如果无需更新则返回null
*/
private ServerWebExchange updateRequestParam(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
boolean integrality = "true".equalsIgnoreCase(request.getHeaders().getFirst(INTEGRALITY));
boolean encrypt = "true".equalsIgnoreCase(request.getHeaders().getFirst(ENCRYPT));
URI uri = request.getURI();
String query = uri.getQuery();
if (!ObjectUtils.isEmpty(query)) {
// 解密查询参数
if (encrypt) {
try {
query = Sm4Utils.decrypt(query);
} catch (Exception e) {
log.error("解密查询参数时发生错误: {}", e.getMessage(), e);
throw new CaptchaException("请求参数不正确");
}
}
// 校验数据完整性
// 校验数据完整性
if (integrality) {
integrityVerification(query.split("\\|")[1], query.split("\\|")[0]);
}
if (ObjectUtils.isEmpty(query)) {
return null;
}
// 更新查询参数
Map<String, List<String>> queryParams = Arrays.stream(query.split("&"))
.map(param -> param.split("="))
.collect(Collectors.toMap(param -> param[0], param -> Collections.singletonList(param[1])));
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQueryParams(CollectionUtils.toMultiValueMap(queryParams))
.build(true)
.toUri();
ServerHttpRequest newRequest = request.mutate().uri(newUri).build();
return exchange.mutate().request(newRequest).build();
}
return null;
}
/**
* 数据完整性校验
*
* @param providedHmac 请求头中的 HMAC
* @param query 请求参数
*/
private void integrityVerification(String providedHmac, String query) {
if (providedHmac == null) {
log.error("请求头中缺少 Params-Hash");
throw new CaptchaException("请求参数不正确");
}
String encrypt = Sm3Util.encrypt(query);
log.debug("加密后的参数: {}", encrypt);
log.debug("请求头中的 Params-Hash: {}", providedHmac);
if (!encrypt.equals(providedHmac)) {
log.error("参数校验失败");
throw new CaptchaException("请求参数不正确");
}
}
}

View File

@ -0,0 +1,154 @@
package com.bonus.sgzb.gateway.filter;
import com.bonus.sgzb.common.core.utils.encryption.Sm4Utils;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @Author ma_sh
* @create 2025/4/24 11:11
* 对返回的 data数据进行加密
*/
@Configuration
@Slf4j
public class ResponseEncryptFilter implements GlobalFilter, Ordered {
/**
* 加密标识
*/
private static final String ENCRYPT_RESPONSE = "encryptResponse";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
// 检查请求头中是否包含加密标志并且系统是否启用了加密功能
if (shouldEncrypt(headers)) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 设置响应头
addResponseHeaders(originalResponse, headers);
// 创建自定义的响应装饰器用于处理响应数据
ServerHttpResponseDecorator responseDecorator = buildResponse(originalResponse, bufferFactory);
// 使用装饰器包装后的响应继续处理链
return chain.filter(exchange.mutate().response(responseDecorator).build());
}
// 如果不需要加密直接继续处理链
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -5;
}
/**
* 判断是否需要对响应数据进行加密
*
* @param headers 请求头
* @return 如果需要加密返回true否则返回false
*/
private boolean shouldEncrypt(HttpHeaders headers) {
return Boolean.parseBoolean(headers.getFirst(ENCRYPT_RESPONSE));
}
/**
* 添加自定义的响应头
*
* @param response 原始响应对象
* @param requestHeaders 请求头
*/
private void addResponseHeaders(ServerHttpResponse response, HttpHeaders requestHeaders) {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.getHeaders().add(ENCRYPT_RESPONSE, requestHeaders.getFirst(ENCRYPT_RESPONSE));
}
/**
* 创建自定义的响应装饰器用于加密响应数据
*
* @param originalResponse 原始响应对象
* @param bufferFactory 数据缓冲区工厂
* @return 自定义的响应装饰器
*/
private ServerHttpResponseDecorator buildResponse(ServerHttpResponse originalResponse, DataBufferFactory bufferFactory) {
return new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// 只处理状态码为200 OK的响应并且响应体是Flux类型
if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
// 对响应体进行加密处理
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBuffer joinedBuffer = joinDataBuffers(dataBuffers);
byte[] content = readContent(joinedBuffer);
DataBufferUtils.release(joinedBuffer);
// 将响应数据加密
String responseData = new String(content, StandardCharsets.UTF_8);
responseData = Sm4Utils.encrypt(responseData);
byte[] encryptedContent = responseData.getBytes(StandardCharsets.UTF_8);
// 设置加密后的内容长度
originalResponse.getHeaders().setContentLength(encryptedContent.length);
return bufferFactory.wrap(encryptedContent);
}));
} else {
log.error("Failed to retrieve response body. Status code: {}", getStatusCode());
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
}
/**
* 将多个DataBuffer连接成一个DataBuffer
*
* @param dataBuffers DataBuffer列表
* @return 连接后的DataBuffer
*/
private DataBuffer joinDataBuffers(java.util.List<? extends DataBuffer> dataBuffers) {
if (dataBuffers.size() > 1) {
return new DefaultDataBufferFactory().join(dataBuffers);
} else {
return dataBuffers.get(0);
}
}
/**
* 从DataBuffer中读取字节数组内容
*
* @param dataBuffer DataBuffer对象
* @return 读取到的字节数组
*/
private byte[] readContent(DataBuffer dataBuffer) {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
return content;
}
}

View File

@ -37,7 +37,7 @@ service.interceptors.request.use(config => {
//回参是否加密
config.headers['encryptResponse'] = systemConfig.requestConfig.encryptResponse && encryptResponse ? 'true' : 'false'
// console.log('🚀 ~ config:', config)
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交