Spring-cloud-gateway加密验签可以这么玩

 图片解释:

        图中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<BaseRequest,String> {

	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<String> 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<ModifyRequestBodyGatewayFilterFactory.Config> {

	private final List<HttpMessageReader<?>> 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<Void> 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<DataBuffer> getBody() {
				return outputMessage.getBody();
			}
		};
	}

	public static class Config {

		private Class inClass;

		private Class outClass;

		private String contentType;

		@Deprecated
		private Map<String, Object> inHints;

		@Deprecated
		private Map<String, Object> 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<String, Object> getInHints() {
			return inHints;
		}

		@Deprecated
		public Config setInHints(Map<String, Object> inHints) {
			this.inHints = inHints;
			return this;
		}

		@Deprecated
		public Map<String, Object> getOutHints() {
			return outHints;
		}

		@Deprecated
		public Config setOutHints(Map<String, Object> outHints) {
			this.outHints = outHints;
			return this;
		}

		public RewriteFunction getRewriteFunction() {
			return rewriteFunction;
		}

		public Config setRewriteFunction(RewriteFunction rewriteFunction) {
			this.rewriteFunction = rewriteFunction;
			return this;
		}

		public <T, R> Config setRewriteFunction(Class<T> inClass, Class<R> outClass,
				RewriteFunction<T, R> 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

公众号主要记录各种源码、面试题、微服务技术栈,帮忙关注一波,非常感谢

  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值