SpringCloud Gateway实现API接口加解密

网友投稿 326 2022-07-23


目录接口范围启用禁用/版本加密算法报文格式网关实现细节代码filter过滤器请求配置和请求方式分发Get请求参数解密包装 ServerHttpRequestDecoratorpost请求参数解密包装 ServerHttpRequestDecoratorGET/POST返回值加密处理CryptoServerHttpResponseDecorator完整CryptoFilter实现

接口范围

所有GET请求 白名单除外

body 体 是 application_json 和 application_json_utf8 的 POST请求 白名单除外

POST url传参也支持 白名单除外

启用禁用/版本

后端提供独立接口(或者现有接口)查询是否需要启用加密功能(如果后端启用了,前端请求被拦截修改为为启用,接口也无法访问回报解密错误),此接口明文传输

请求头增加一个加密版本字段,标识当前的加密算法版本:crypto-version: 1.0.0

加密算法

考虑到全局加密,使用AES加密方式性能更高

加密字符串:原始数据 > AES加密后的字节数组 > Base64编码处理

解密字符串:Base64密文 > AES密文 -> 原始字符串

AES加密细节:

aesKey:32/16 位由后端同一生成

iv:aesKey

mode:CBC

padding:pkcs7

js例子

//加密

static encryptAES(data, key) {

const dataBytes = CryptoJS.enc.Utf8.parse(data);

const keyBytes = CryptoJS.enc.Utf8.parse(key);

const encrypted = CryptoJS.AES.encrypt(dataBytes, keyBytes, {

iv: keyBytes,

mode: CryptoJS.mode.CBC,

padding: CryptoJS.pad.Pkcs7

});

return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);

}

报文格式

GET

url:/app/xx/xx?xx=1

加密处理

秘钥:xxxxxxxxxxxxxxxx

加密文本:{"xx":1}

密文:xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=

加密后的请求

/app/xx/xx?data=xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=

POST

url:/app/xx/xx/xxx

json body:

{"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}

加密处理

秘钥:xxxxxxxxxxxxxxxx

加密文本:

{"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}

密文:1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOC9L0aVpR863qWso5O8aG3

加密后的请求*

json body:

{

"data": "1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOjt4G9dK0WwhMGZofYuBKmdF27R8Qkr3VtZvjadtvBazJurITyE7hFcr43nlHSL5E="

}

POST url传参 和GET格式一致

网关实现细节代码

基于GlobalFilter 接口包装请求request和响应response,先列出关键代码,完整代码见文末

filter过滤器请求配置和请求方式分发

@Override

public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {

if (!cryptoProperties.isEnabled()) {

return chain.filter(exchange);

}

ServerHttpRequest request = exchange.getRequest();

//校验请求路径跳过加密

String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange);

String path = exchange.getRequest().getURI().getPath();

if (isSkip(path) || isSkip(originalRequestUrl)) {

return chain.filter(exchange);

}

HttpHeaders headers = request.getHeaders();

MediaType contentType = headers.getContentType();

//后期算法升级扩展,暂时只判断是否相等

if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) {

return Mono.error(new CryptoException("加密版本不支持"));

}

if (request.getMethod() == HttpMethod.GET) {

return this.handleGetReq(exchange, chain);

} else if (request.getMethod() == HttpMethod.POST &&

(contentType == null ||

MediaType.APPLICATION_JSON.equals(contentType) ||

MediaType.APPLICATION_JSON_UTF8.equals(contentType))) {

return this.handlePostReq(exchange, chain);

} else {

return chain.filter(exchange);

}

}

Get请求参数解密包装 ServerHttpRequestDecorator

//构造查询参数Map

MultiValueMap map = buildMultiValueMap(dataJson);

//新的解密后的uri

ServerHttpRequest newhttp://HttpRequest = this.buildNewServerHttpRequest(request, map);

//新的解密后的uri request

ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {

@Override

public MultiValueMap getQueryParams() {

return map;

}

};

post请求参数解密包装 ServerHttpRequestDecorator

//构造一个请求包装

final MultiValueMap finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap);

ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {

@Override

public HttpHeaders getHeaders() {

HttpHeaders httpHeaders = new HttpHeaders();

httpHeaders.putAll(super.getHeaders());

httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);

httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");

return httpHeaders;

}

//处理post url传参解密

@Override

public MultiValueMap getQueryParams() {

if (queryParamsDecrypt) {

return finalQueryParamMap;

}

return super.getQueryParams();

}

@Override

public Flux getBody() {

//注意: 这里需要buffer一下,拿到完整报文后再map解密

return super.getBody().buffer().map(buffer -> {

DataBuffer joinDataBuffer = dataBufferFactory.join(buffer);

byte[] content = new byte[joinDataBuffer.readableByteCount()];

joinDataBuffer.read(content);

DataBufferUtils.release(joinDataBuffer);

String decryptData = new String(content, StandardCharsets.UTF_8);

log.info("post decryptData: {}", decryptData);

if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) {

throw new CryptoException("参数格式错误");

} else {

JSONObject dataJsonObj = JSON.parseObject(decryptData);

if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) {

throw new CryptoException("参数格式错误");

}

byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey());

return dataBufferFactory.wrap(Objects.requireNonNull(bytes));

}

});

GET/POST返回值加密处理CryptoServerHttpResponseDecorator

class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator {

final DataBufferFactory bufferFactory;

boolean isPass = false;

public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) {

super(delegate);

bufferFactory = delegate.bufferFactory();

}

@Override

public HttpHeaders getHeaders() {

HttpHeaders headers = super.getHeaders();

//同一个请求此处有可能调用多次,先重置为false

isPass = false;

if (headers.getContentType() != null &&

!MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&

!MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {

//相应体ContentType只处理json

isPass = true;

} else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {

//添加version响应头

headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion());

}

return headers;

}

//调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter

// application/json;charset=UTF-8 走这里

@Override

public Mono writeWith(Publisher extends DataBuffer> body) {

if (body instanceof Flux && !isPass) {

Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;

return super.writeWith(fluxBody.buffer().map(dataBuffer -> {

DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer);

byte[] content = new byte[joinDataBuffer.readableByteCount()];

joinDataBuffer.read(content);

DataBufferUtils.release(joinDataBuffer);

Map data = new HashMap<>(1);

data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey()));

return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8));

}));

}

return super.writeWith(body);

}

// StreamingMediaType类型:application/stream 和 application/stream+json 走这里

@Override

public Mono writeAndFlushWith(Publisher extends Publisher extends DataBuffer>> body) {

return super.writeAndFlushWith(body);

}

}

完整CryptoFilter实现

package org.xx.xx.gateway.filter;

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.JSONObject;

import com.google.common.collect.Lists;

import io.netty.buffer.ByteBufAllocator;

import lombok.RequiredArgsConstructor;

import lombok.SneakyThrows;

import lombok.extern.slf4j.Slf4j;

import org.reactivestreams.Publisher;

import org.xx.xx.gateway.props.CryptoProperties;

import org.xx.xx.gateway.provider.RequestProvider;

import org.xx.xx.gateway.provider.ResponseProvider;

import org.xx.xx.gateway.util.AesUtil;

import org.xx.xx.gateway.util.StringPool;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

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.NettyDataBufferFactory;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

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.http.server.reactive.ServerHttpResponse;

import org.springframework.http.server.reactive.ServerHttpResponseDecorator;

import org.springframework.util.AntPathMatcher;

import org.springframework.util.LinkedMultiValueMap;

import org.springframework.util.MultiValueMap;

import org.springframework.util.StringUtils;

import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;

import java.net.URI;

import java.net.URISyntaxException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;

import java.util.Map;

import java.util.Objects;

/**

* CryptoFilter

*

* @author lizheng

* @version 1.0

* @date 2022/3/11 上午10:57

*/

@Slf4j

@RequiredArgsConstructor

@Configuration

@ConditionalOnProperty(value = "gateway.crypto.enabled", havingValue = "true", matchIfMissing = true)

public class CryptoFilter implements GlobalFilter, Ordered {

private final AntPathMatcher antPathMatcher = new AntPathMatcher();

private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);

private final CryptoProperties cryptoProperties;

@Override

public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {

if (!cryptoProperties.isEnabled()) {

return chain.filter(exchange);

}

ServerHttpRequest request = exchange.getRequest();

//校验请求路径跳过加密

String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange);

String path = exchange.getRequest().getURI().getPath();

if (isSkip(path) || isSkip(originalRequestUrl)) {

return chain.filter(exchange);

}

HttpHeaders headers = request.getHeaders();

MediaType contentType = headers.getContentType();

//后期算法升级扩展,暂时只判断是否相等

if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) {

return Mono.error(new CryptoException("加密版本不支持"));

}

if (request.getMethod() == HttpMethod.GET) {

return this.handleGetReq(exchange, chain);

} else if (request.getMethod() == HttpMethod.POST &&

(contentType == null ||

MediaType.APPLICATION_JSON.equals(contentType) ||

MediaType.APPLICATION_JSON_UTF8.equals(contentType))) {

return this.handlePostReq(exchange, chain);

} else {

return chain.filter(exchange);

}

}

class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator {

final DataBufferFactory bufferFactory;

boolean isPass = false;

public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) {

super(delegate);

bufferFactory = delegate.bufferFactory();

}

@Override

public HttpHeaders getHeaders() {

HttpHeaders headers = super.getHeaders();

//同一个请求此处有可能调用多次,先重置为false

isPass = false;

if (headers.getContentType() != null &&

!MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&

!MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {vdLtenuaF

//相应体ContentType只处理json

isPass = true;

} else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {

//添加version响应头

headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion());

}

return headers;

}

//调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter

// application/json;charset=UTF-8 走这里

@Override

public Mono writeWith(Publisher extends DataBuffer> body) {

if (body instanceof Flux && !isPass) {

Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;

return super.writeWith(fluxBody.buffer().map(dataBuffer -> {

DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer);

byte[] content = new byte[joinDataBuffer.readableByteCount()];

joinDataBuffer.read(content);

DataBufferUtils.release(joinDataBuffer);

Map data = new HashMap<>(1);

data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey()));

return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8));

}));

}

return super.writeWith(body);

}

// StreamingMediaType类型:application/stream 和 application/stream+json 走这里

@Override

public Mono writeAndFlushWith(Publisher extends Publisher extends DataBuffer>> body) {

return super.writeAndFlushWith(body);

}

}

@SneakyThrows

private Mono handlePostReq(ServerWebExchange exchange, GatewayFilterChain chain) {

ServerHttpRequest request = exchange.getRequest();

String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName());

MultiValueMap queryParamMap = new LinkedMultiValueMap<>();

final boolean queryParamsDecrypt = !StringUtils.isEmpty(paramData);

if (queryParamsDecrypt) {

String dataJson;

try {

//AES解密

dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey());

} catch (Exception e) {

log.error("请求参数解密异常: ", e);

return cryptoError(exchange.getResponse(), "请求参数解密异常");

}

//构造查询参数Map

queryParamMap = buildMultiValueMap(dataJson);

//新的解密后的uri request

request = this.buildNewServerHttpRequest(request, queryParamMap);

}

//构造一个请求包装

final MultiValueMap finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap);

ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {

@Override

public HttpHeaders getHeaders() {

HttpHeaders httpHeaders = new HttpHeaders();

httpHeaders.putAll(super.getHeaders());

httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);

httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");

return httpHeaders;

}

@Override

public MultiValueMap getQueryParams() {

if (queryParamsDecrypt) {

return finalQueryParamMap;

}

return super.getQueryParams();

}

@Override

public Flux getBody() {

//注意: 这里需要buffer,拿到完整报文后再map解密

return super.getBody().buffer().map(buffer -> {

DataBuffer joinDataBuffer = dataBufferFactory.join(buffer);

byte[] content = new byte[joinDataBuffer.readableByteCount()];

joinDataBuffer.read(content);

DataBufferUtils.release(joinDataBuffer);

String decryptData = new String(content, StandardCharsets.UTF_8);

log.info("post decryptData: {}", decryptData);

if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) {

throw new CryptoException("参数格式错误");

} else {

JSONObject dataJsonObj = JSON.parseObject(decryptData);

if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) {

throw new CryptoException("参数格式错误");

}

byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey());

return dataBufferFactory.wrap(Objects.requireNonNull(bytes));

}

});

}

};

return chain.filter(exchange.mutate()

.request(decorator)

.response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))

.build());

}

@SneakyThrows

private Mono handleGetReq(ServerWebExchange exchange, GatewayFilterChain chain) {

ServerHttpRequest request = exchange.getRequest();

if (request.getQueryParams().isEmpty()) {

// get无参数 不走参数解密

return chain.filter(exchange.mutate()

.request(request)

.response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))

.build());

}

String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName());

if (StringUtils.isEmpty(paramData)) {

//有参数但是密文字段不存在

throw new CryptoException("参数格式错误");

}

String dataJson;

try {

//AES解密

dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey());

} catch (Exception e) {

log.error("请求参数解密异常: ", e);

return cryptoError(exchange.getResponse(), "请求参数解密异常");

}

//构造查询参数Map

MultiValueMap map = buildMultiValueMap(dataJson);

//新的解密后的uri

ServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map);

//新的解密后的uri request

ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {

@Override

public MultiValueMap getQueryParams() {

return map;

}

};

return chain.filter(exchange.mutate()

.request(decorator)

.response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))

.build());

}

private MultiValueMap buildMultiValueMap(String dataJson) {

vdLtenuaF JSONObject jsonObject = JSON.parseObject(dataJson);

MultiValueMap map = new LinkedMultiValueMap<>(jsonObject.size());

for (String key : jsonObject.keySet()) {

map.put(key, Lists.newArrayList(jsonObject.getString(key)));

}

return map;

}

private ServerHttpRequest buildNewServerHttpRequest(ServerHttpRequest request, MultiValueMap params) throws URISyntaxException {

StringBuilder queryBuilder = new StringBuilder();

for (String key : params.keySet()) {

queryBuilder.append(key);

queryBuilder.append(StringPool.EQUALS);

queryBuilder.append(params.getFirst(key));

queryBuilder.append(StringPool.AMPERSAND);

}

queryBuilder.deleteCharAt(queryBuilder.length() - 1);

//经过测试只覆盖 ServerHttpRequest的getQueryParams路由分发之后,无法携带过去新的参数,所以这里需要构造一个新的解密后的uri

URI uri = request.getURI();

URI newUri = new URI(uri.getScheme(),

uri.getUserInfo(),

uri.getHost(),

uri.getPort(),

uri.getPath(),

queryBuilder.toString(),

uri.getFragment());

//构造一个新的ServerHttpRequest

return request.mutate().uri(newUri).build();

}

private boolean isSkip(String path) {

for (String pattern : cryptoProperties.getSkipPathPattern()) {

if (antPathMatcher.match(pattern, path)) {

return true;

}

}

return false;

}

private Mono cryptoError(ServerHttpResponse resp, String msg) {

resp.setStatusCode(HttpStatus.UNAUTHORIZED);

resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

String result = JSON.toJSONString(ResponseProvider.unAuth(msg));

DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));

return resp.writeWith(Flux.just(buffer));

}

@Override

public int getOrder() {

return -200;

}

}

以上就是SpringCloud Gateway实现API接口加解密的详细内容,更多关于SpringCloud Gateway接口加解密的资料请关注我们其它相关文章!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:java实现后台返回base64图形编码
下一篇:Java内部类与匿名内部类
相关文章

 发表评论

暂时没有评论,来抢沙发吧~