本文前后端均使用国密SM2加密,后端在gateway中统一拦截,解密和加密。
流程步骤为:
1、前端对请求数据进行加密;
2、网关请求拦截器拦截前端请求,并对请求数据解密;
3、网关响应拦截器拦截后端响应数据,并对响应数据加密;
4、前端对返回的数据进行解密。
org.bouncycastle
bcprov-jdk15to18
1.69
org.bouncycastle
bcprov-jdk15on
1.56
cn.hutool
hutool-all
5.7.4
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.Test;
public class SM2Test {
@Test
public void createSM2Key(){
SM2 sm2 = SmUtil.sm2();
// sm2的加解密时有两种方式即 C1C2C3、 C1C3C2,
sm2.setMode(SM2Engine.Mode.C1C3C2);
// 生成私钥
String privateKey = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey()));
// 生成公钥
String publicKey = HexUtil.encodeHexStr(((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false));
}
}
生成2份公钥和私钥
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
@Component
public class RequestGlobalFilter implements GlobalFilter, Ordered {
@Value("${ht.publicKey2}")
private String publicKey2;
@Value("${ht.privateKey2}")
private String privateKey2;
static Logger logger = LoggerFactory.getLogger(RequestGlobalFilter.class);
@Autowired
private ServerCodecConfigurer configurer;
protected String decrypt(String body, SM2 sm2) {
String data;
if (StrUtil.isNotEmpty(body)) {
if (StrUtil.isNotEmpty(body)) {
String d = body.startsWith("04") ? body : "04" + body;
data = sm2.decryptStr(d, KeyType.PrivateKey, Charset.forName("utf-8"));
} else {
data = body;
}
} else {
data = body;
}
return data;
}
/**
* 全局过滤器 核心方法
* @param exchange
* @param chain
* @return
*/
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(exchange.getRequest().getHeaders().getContentType())) {
return chain.filter(exchange);
}
final HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
final ServerRequest serverRequest = ServerRequest.create(exchange, configurer.getReaders());
Mono modifiedBody;
Class dataClass;
dataClass = String.class;
modifiedBody = serverRequest
.bodyToMono(String.class)
.flatMap(body -> {
String newBody = null;
try {
if (body != null) {
String d = body.startsWith("04") ? body : "04" + body;
// 解密
SM2 sm2 = SmUtil.sm2(privateKey2, publicKey2);
// sm2.setMode(SM2Engine.Mode.C1C3C2);
// String newBody1 = StrUtil.utf8Str(sm2.decryptFromBcd(d, KeyType.PrivateKey));
newBody = sm2.decryptStr(d, KeyType.PrivateKey, Charset.forName("utf-8"));
// 去掉解密后字符串开始结尾多出的引号
if (newBody.startsWith("\"")) {
newBody = newBody.substring(1);
}
if (newBody.endsWith("\"")) {
newBody = newBody.substring(0, newBody.length() - 1);
}
}
} catch (Exception e) {
return Mono.error(e);
}
return Mono.just(newBody);
});
BodyInserter, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, dataClass);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter
.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// headers.set("Content-Type", "application/json");
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux getBody() {
return outputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
@Override
public int getOrder() {
return -200;
}
}
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Value;
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.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
@Component
@Slf4j
public class ResponseGlobalFilter implements GlobalFilter, Ordered {
@Value("${ht.publicKey1}")
private String publicKey1;
@Value("${ht.privateKey1}")
private String privateKey1;
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -2;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse()) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
ServerHttpResponse originalResponse = exchange.getResponse();
MediaType mediaType = originalResponse.getHeaders().getContentType();
if (body instanceof Flux && !MediaType.APPLICATION_OCTET_STREAM.isCompatibleWith(mediaType)) {
Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 释放掉内存
DataBufferUtils.release(join);
// 加密
SM2 sm2 = SmUtil.sm2(privateKey1, publicKey1);
// sm2.setMode(SM2Engine.Mode.C1C3C2);
// 返回值的字符串,需要注意此处加密后密文为16进制,否则前端解密前需要先转换格式
String encryptStr = sm2.encryptHex(new String(content), Charset.forName("utf-8"), KeyType.PublicKey);
// 加密后密文前有04,需要去掉(改为前端去除)
// String encrypt = encryptStr.startsWith("04") ? encryptStr.substring(2) : encryptStr;
return bufferFactory.wrap(encryptStr.getBytes());
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
}
npm install --save sm-crypto
main.js添加:
// 加密:
export function doEncrypt(msgString) {
let msg = msgString;
if (typeof (msgString) !== 'string') {
msg = JSON.stringify(msgString);
}
let sm2 = require('sm-crypto').sm2;
// 1 - C1C3C2; 0 - C1C2C3; 默认为1
let cipherMode = 1
let publicKey2 = '你的公钥2'
// 加密结果
let encryptData = sm2.doEncrypt(msg, publicKey2, cipherMode);
// 加密后的密文前需要添加04,后端才能正常解密
let encrypt = '04' + encryptData;
return encrypt;
};
// 解密
doDecryptStr(enStr) {
let msg = enStr;
if (typeof (enStr) !== 'string') {
msg = JSON.stringify(enStr);
}
let sm2 = require('sm-crypto').sm2;
// 1 - C1C3C2; 0 - C1C2C3; 默认为1
let cipherMode = 1
let privateKey1 = '你的私钥1'
// 加密后的密文,需要前去掉04。因为doDecrypt中自行添加了04,后端加密代码也自行添加了04
let en = enStr.data.substr(2)
// 解密结果
let doDecrypt = sm2.doDecrypt(en, privateKey1, cipherMode);
// 解密后类型转换
let objData = JSON.parse(doDecrypt)
return objData;
},