图片解释:
图中api可以作为公司内部的集中入口统一做验签加解密,当然也可以公司的统一出口(假设api后面对接的是各家银行)针对对接不同的公司进行单独集中处理加解密
这也就是网关要干的事,统一对外暴露出入口,对无关业务(验签,加解密,容错,限流)的操作进行集中开发处理(不建议在网关搞业务,不建议操作数据库)
本节要实现的目标:
用最少的代码、配置来对接一个新的外部系统(需要验签,加解密)
现状:
为保证数据通信安全,我们在关系到外网通信时一般要将数据进行加密,对通话进行验签,今偶然间看了一下公司的网关(对外网关,对接了很多银行),主要实现是每接一个银行都是单独写了一个类,里边封装了所有接口调用包含加解密验签类的工作,将银行方出入参封装对象暴露给公司内部系统调用
优缺点:
优点:实现传统,开发人员易懂,新接银行复制修改即可,能够针对不同资方分别开发,互不影响(有些银行给提供jar包里边封装了调用,大部分还是自己http调用)
不足:网关的作用不应该关心业务代码(封装了业务字段,如果哪天要改,就得改两个系统),代码冗余度高
针对现状的思考:
1、大部分银行都是提供接口地址直接调用,能不能通过配置实现转发,只针对不同银行做加解密验签,其它都不做
2、针对特殊银行(给jar包调用的)特殊封装实现动态调用
针对第二种:可统一封装controller,字段包含银行标识,接口标识,通过配置映射调用,每次新增只需新增映射即可,本次不实现,只讲第一种(可包含大多数实现)
进入正题:
实现思路:
1、公司内部请求业务数据到网关
2、网关统一拦截将业务数据按要求进行加密转发,数据流入内网,流出外网
3、外网调用配置白名单,调用后进行拦截验签解密转发
代码实现思路:
1、请求转发可通过gateway配置文件配置
2、自定义过滤器解析requestBody将body数据进行加密
3、解析ResponseBody将数据解密转发请求
代码实现:
先看一个配置图
这里主要看id是user-server的配置(gateway端口8001)
1、拦截url中带有user-info的请求转发到http://localhost:8003
例如:请求http://localhost:8001/user-info/user会转发到http://localhost:8003/user-info/user
2、自定义ModifyRequestBody过滤器,解析body进行加密
3、自定义ModifyResponseBody过滤器,解析response进行验签解密
刚开始写的时候网上搜了一个解析body的代码,总体实现起来略显恶心,后来看了一下源码,原来源码里很多功能人家都实现好了,在这里提醒一下,开发某个功能时请一定要看看人家有没有帮你实现好,不要在闭门造车了
简单看一下基本都有,我们这里主要用了上面两个,注意这里配置的时候我们省略了后缀
GatewayFilterFactory
源码(文末)可以简单看看,意思就是解析bady然后重新生成了一个,主要看下面注释那一行(config.rewriteFunction.apply)也就是这个重写函数,我们做的就是取重写这个函数即可:
@Component
public class GongShangEncryptFunction implements RewriteFunction {
public static final String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANe2bmG2t11hweL4rd48JsBdeyZfRy8phAUqhs8sBi8lHFn8L3+VDkZH35BRPZPzDJ30a2lehBZjL+FXZCWIub16baGQgFjT2Jiod3xCa0uIvFDrpS28NAMb+gcw1VAVFJL1FVHtNKk2GYLQjZUR+oX884mKbtG4e49P9lMH7Z1vAgMBAAECgYALJEAVSfO0ngz+pSuN0/uIagunUrqBhBpujeDCqJp1KuyI9U6av18qYCH6+Uc98grPycUWfyxBX8QkVng0vBgjyfzeboPUbh0MFx5DA5lJ2yMv+oPLXIEjTcA4sVUxoRXLrSSETTGIuigFuNctc4vBx755hxGn1VnBhuEYsvu6MQJBAO51Br/xw3m5trlJN0ko4P7nlziFWhB0wopCGv6tLeQYvkcw98jz+EplARNcTP/0aQrggHM5UPP7cW7j6kp+AQkCQQDnlQwAG2gOJ30cJxMlQH23NE9ju3poUzHUiJ8qOXSHZOVScYP8VWtKfFMWSiQXriIgQ34LsyDLo/k6MJ3TG+C3AkBGfwR61I+0uenCR1n34AT8dx0m0Y251br5wudWKX6qs4H1bA2lNDNQUyIJRj1hYjF3zL1M00ISj2COpwTJ9wx5AkB9Q1CnaiuhpFh29ufTOYwGocPjhVATyBRnCrNVSpiud7PXIVGsFqQfORpULyxQpr8MxpUSTQULQZmYkR19SFIHAkEA4WW+WwuP7PGbwAyjrlcJo6e24zQ0N189zCfqULWnKGTYBm5LhMyJGEmVzjsPb148PD4Z7yGdhrWQtPlfWbCfTQ==";
public static final String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXtm5htrddYcHi+K3ePCbAXXsmX0cvKYQFKobPLAYvJRxZ/C9/lQ5GR9+QUT2T8wyd9GtpXoQWYy/hV2QliLm9em2hkIBY09iYqHd8QmtLiLxQ66UtvDQDG/oHMNVQFRSS9RVR7TSpNhmC0I2VEfqF/POJim7RuHuPT/ZTB+2dbwIDAQAB";
@Override
public Publisher apply(ServerWebExchange exchange, BaseRequest baseRequest) {
System.out.println("==========="+ JSON.toJSONString(baseRequest));
if (exchange.getRequest().getMethodValue().equals(HttpMethod.GET.name())){
return Mono.just(JSON.toJSONString(baseRequest));
}
try {
String key = SignUtil.getRandom();
String content = JSON.toJSONString(baseRequest);
String reqData = SignUtil.encrypt4Base64(content, key);
String randomKey = SignUtil.encryptByPublicKey4Pkcs5(key.getBytes(StandardCharsets.UTF_8), publicKey);
XdRequest xdRequest = new XdRequest();
xdRequest.setChannelId("100000");
xdRequest.setTime(String.valueOf(System.currentTimeMillis()));
xdRequest.setVersion("1.0");
xdRequest.setData(reqData);
xdRequest.setRandomKey(randomKey);
String signData = SignUtil.sign(JSON.toJSONString(xdRequest, SerializerFeature.MapSortField)
.getBytes(StandardCharsets.UTF_8), privateKey);
xdRequest.setSign(signData);
return Mono.just(JSON.toJSONString(xdRequest));
} catch (Exception e) {
e.printStackTrace();
}
return Mono.empty();
}
}
实现RewriteFunction接口,泛型第一个参数是入参(body)类型,二个是出参类型,可根据实际情况自定义,这里拿到body去做一些加密,然后返回一个新的body即可
然后回到刚开始的yml配置图
rewriteFunction,outClass,inClass是源码里Config里的三个字段,对应我们开发的funcfion,出入参类型,这样及结束了requestBody解析加密的开发,即每接一个银行只需要配置相应的过滤器及函数即可
responseBody也是一样的,后面需要用到什么自带的或者模仿写一个都可以这么来了
ModifyRequestBodyGatewayFilterFactory源码:
public class ModifyRequestBodyGatewayFilterFactory extends
AbstractGatewayFilterFactory {
private final List> messageReaders;
public ModifyRequestBodyGatewayFilterFactory() {
super(Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
@Deprecated
public ModifyRequestBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) {
this();
}
@Override
@SuppressWarnings("unchecked")
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
Class inClass = config.getInClass();
ServerRequest serverRequest = ServerRequest.create(exchange,
messageReaders);
// TODO: flux or mono
Mono> modifiedBody = serverRequest.bodyToMono(inClass)
// .log("modify_request_mono", Level.INFO)
.flatMap(o -> config.rewriteFunction.apply(exchange, o));
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
config.getOutClass());
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
// if the body is changing content types, set it here, to the bodyInserter
// will know about it
if (config.getContentType() != null) {
headers.set(HttpHeaders.CONTENT_TYPE, config.getContentType());
}
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
// .log("modify_request", Level.INFO)
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers,
outputMessage);
return chain
.filter(exchange.mutate().request(decorator).build());
}));
}
@Override
public String toString() {
return filterToStringCreator(ModifyRequestBodyGatewayFilterFactory.this)
.append("Content type", config.getContentType())
.append("In class", config.getInClass())
.append("Out class", config.getOutClass()).toString();
}
};
}
ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers,
CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
}
else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux getBody() {
return outputMessage.getBody();
}
};
}
public static class Config {
private Class inClass;
private Class outClass;
private String contentType;
@Deprecated
private Map inHints;
@Deprecated
private Map outHints;
private RewriteFunction rewriteFunction;
public Class getInClass() {
return inClass;
}
public Config setInClass(Class inClass) {
this.inClass = inClass;
return this;
}
public Class getOutClass() {
return outClass;
}
public Config setOutClass(Class outClass) {
this.outClass = outClass;
return this;
}
@Deprecated
public Map getInHints() {
return inHints;
}
@Deprecated
public Config setInHints(Map inHints) {
this.inHints = inHints;
return this;
}
@Deprecated
public Map getOutHints() {
return outHints;
}
@Deprecated
public Config setOutHints(Map outHints) {
this.outHints = outHints;
return this;
}
public RewriteFunction getRewriteFunction() {
return rewriteFunction;
}
public Config setRewriteFunction(RewriteFunction rewriteFunction) {
this.rewriteFunction = rewriteFunction;
return this;
}
public Config setRewriteFunction(Class inClass, Class outClass,
RewriteFunction rewriteFunction) {
setInClass(inClass);
setOutClass(outClass);
setRewriteFunction(rewriteFunction);
return this;
}
public String getContentType() {
return contentType;
}
public Config setContentType(String contentType) {
this.contentType = contentType;
return this;
}
}
}
文中源代码:https://gitee.com/carpentor/spring-cloud-example.git
公众号主要记录各种源码、面试题、微服务技术栈,帮忙关注一波,非常感谢