SpringCloud|SpringCloud Gateway实现API接口加解密

目录

  • 接口范围
  • 启用禁用/版本
  • 加密算法
  • 报文格式
  • 网关实现细节代码
    • filter过滤器请求配置和请求方式分发
    • Get请求参数解密包装 ServerHttpRequestDecorator
    • post请求参数解密包装 ServerHttpRequestDecorator
    • GET/POST返回值加密处理CryptoServerHttpResponseDecorator
  • 完整CryptoFilter实现
    【SpringCloud|SpringCloud Gateway实现API接口加解密】
    接口范围 所有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=https://www.it610.com/article/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过滤器请求配置和请求方式分发
    @Overridepublic 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
    //构造查询参数MapMultiValueMap map = buildMultiValueMap(dataJson); //新的解密后的uriServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map); //新的解密后的uri requestServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {@Overridepublic MultiValueMap getQueryParams() {return map; }};


    post请求参数解密包装 ServerHttpRequestDecorator
    //构造一个请求包装final MultiValueMap finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap); ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {@Overridepublic 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传参解密@Overridepublic MultiValueMap getQueryParams() {if (queryParamsDecrypt) {return finalQueryParamMap; }return super.getQueryParams(); }@Overridepublic 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 = https://www.it610.com/article/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(); }@Overridepublic HttpHeaders getHeaders() {HttpHeaders headers = super.getHeaders(); //同一个请求此处有可能调用多次,先重置为falseisPass = false; if (headers.getContentType() != null &&!MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&!MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {//相应体ContentType只处理jsonisPass = true; } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {//添加version响应头headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion()); }return headers; }//调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter// application/json; charset=UTF-8 走这里@Overridepublic Mono writeWith(Publisher body) {if (body instanceof Flux && !isPass) {Flux fluxBody = (Flux) 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 = https://www.it610.com/article/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 走这里@Overridepublic Mono writeAndFlushWith(Publisher> 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 = "https://www.it610.com/article/gateway.crypto.enabled", havingValue = "https://www.it610.com/article/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; @Overridepublic 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(); }@Overridepublic HttpHeaders getHeaders() {HttpHeaders headers = super.getHeaders(); //同一个请求此处有可能调用多次,先重置为falseisPass = false; if (headers.getContentType() != null &&!MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&!MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {//相应体ContentType只处理jsonisPass = true; } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {//添加version响应头headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion()); }return headers; }//调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter// application/json; charset=UTF-8 走这里@Overridepublic Mono writeWith(Publisher body) {if (body instanceof Flux && !isPass) {Flux fluxBody = (Flux) 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 = https://www.it610.com/article/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 走这里@Overridepublic Mono writeAndFlushWith(Publisher> body) {return super.writeAndFlushWith(body); }}@SneakyThrowsprivate Mono handlePostReq(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest(); String paramData = https://www.it610.com/article/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(), "请求参数解密异常"); }//构造查询参数MapqueryParamMap = buildMultiValueMap(dataJson); //新的解密后的uri requestrequest = this.buildNewServerHttpRequest(request, queryParamMap); }//构造一个请求包装final MultiValueMap finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap); ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {@Overridepublic HttpHeaders getHeaders() {HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); return httpHeaders; }@Overridepublic MultiValueMap getQueryParams() {if (queryParamsDecrypt) {return finalQueryParamMap; }return super.getQueryParams(); }@Overridepublic 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 = https://www.it610.com/article/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()); }@SneakyThrowsprivate 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 = https://www.it610.com/article/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(), "请求参数解密异常"); }//构造查询参数MapMultiValueMap map = buildMultiValueMap(dataJson); //新的解密后的uriServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map); //新的解密后的uri requestServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {@Overridepublic MultiValueMap getQueryParams() {return map; }}; return chain.filter(exchange.mutate().request(decorator).response(new CryptoServerHttpResponseDecorator(exchange.getResponse())).build()); }private MultiValueMap buildMultiValueMap(String dataJson) {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路由分发之后,无法携带过去新的参数,所以这里需要构造一个新的解密后的uriURI uri = request.getURI(); URI newUri = new URI(uri.getScheme(),uri.getUserInfo(),uri.getHost(),uri.getPort(),uri.getPath(),queryBuilder.toString(),uri.getFragment()); //构造一个新的ServerHttpRequestreturn 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)); }@Overridepublic int getOrder() {return -200; }}

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

      推荐阅读