diff --git a/sgzb-auth/src/main/java/com/bonus/sgzb/auth/controller/ConfigController.java b/sgzb-auth/src/main/java/com/bonus/sgzb/auth/controller/ConfigController.java new file mode 100644 index 00000000..e6c29df1 --- /dev/null +++ b/sgzb-auth/src/main/java/com/bonus/sgzb/auth/controller/ConfigController.java @@ -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 getConfig() { + Map 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); + } +} diff --git a/sgzb-common/sgzb-common-core/pom.xml b/sgzb-common/sgzb-common-core/pom.xml index 36acdd3b..4e074f0b 100644 --- a/sgzb-common/sgzb-common-core/pom.xml +++ b/sgzb-common/sgzb-common-core/pom.xml @@ -123,6 +123,12 @@ org.apache.httpcomponents httpclient + + cn.hutool + hutool-all + 5.8.16 + compile + diff --git a/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm3Util.java b/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm3Util.java new file mode 100644 index 00000000..23a28dbf --- /dev/null +++ b/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm3Util.java @@ -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); + } + +} diff --git a/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm4Utils.java b/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm4Utils.java new file mode 100644 index 00000000..ef635fc0 --- /dev/null +++ b/sgzb-common/sgzb-common-core/src/main/java/com/bonus/sgzb/common/core/utils/encryption/Sm4Utils.java @@ -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); + } +} diff --git a/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/RequestCoverFilter.java b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/RequestCoverFilter.java new file mode 100644 index 00000000..e22b3dc7 --- /dev/null +++ b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/RequestCoverFilter.java @@ -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} 表示请求处理完成的指示 + */ + @Override + public Mono 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} 表示请求处理完成的指示 + */ + private Mono 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 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} 表示请求处理完成的指示 + */ + private Mono 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> 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("请求参数不正确"); + } + } + +} diff --git a/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/ResponseEncryptFilter.java b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/ResponseEncryptFilter.java new file mode 100644 index 00000000..a91209f2 --- /dev/null +++ b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/ResponseEncryptFilter.java @@ -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 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 writeWith(Publisher body) { + // 只处理状态码为200 OK的响应,并且响应体是Flux类型 + if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) { + Flux 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 writeAndFlushWith(Publisher> body) { + return writeWith(Flux.from(body).flatMapSequential(p -> p)); + } + }; + } + + /** + * 将多个DataBuffer连接成一个DataBuffer + * + * @param dataBuffers DataBuffer列表 + * @return 连接后的DataBuffer + */ + private DataBuffer joinDataBuffers(java.util.List 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; + } +} diff --git a/sgzb-ui/src/utils/request.js b/sgzb-ui/src/utils/request.js index 5a3a2ec2..600d37bf 100644 --- a/sgzb-ui/src/utils/request.js +++ b/sgzb-ui/src/utils/request.js @@ -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 // 是否需要防止数据重复提交