收到了把项目出入参加密解密的需求(POST请求加密,GET暂不加密),主框架环境是若依。这篇文章记录一下修改的过程。
加密类型选择AES加密(如有其他加密类型可以自由替换)首先选择在gateway模块中去统一进行加密解密的操作,先说前端传来参数的解密功能,在AuthFilter中想获取前端传来的参数。
一、获取入参
AuthFilter实现了GlobalFilter,也起到全局的过滤效果。实现方法中,有一个ServerWebExchange的入参,通过这个入参可以获得ServerHttpRequest对象。
按说这个ServerHttpRequest对象中应该是能拿到请求入参的,但是照着网上一些方法去获取入参获得的却是null。(这里涉及@requestbody修饰的入参的取值方法,使用流取值,且这个流默认只会取一次的相关问题等不在这篇文章里赘述)
如果这一步是可以获取参数的,请跳过这一步。
后面在网上找到这么一个方法,写一个过滤器,获得并处理入参,并且把这个过滤器放在最前面执行(设置HIGHEST_PRECEDENCE),把处理后的入参放在后面中过滤器执行链中,直接上代码。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerStrategies; 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.util.List; @Component public class ParamsEncryptionFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(ParamsEncryptionFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { /** * save request path and serviceId into gateway context */ ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); // 处理参数 MediaType contentType = headers.getContentType(); long contentLength = headers.getContentLength(); if (contentLength > 0) { if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) { readBody(exchange, chain); return readBody(exchange, chain); } } return chain.filter(exchange); } /** * default HttpMessageReader */ private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); /** * ReadJsonBody * * @param exchange * @param chain * @return */ private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain) { /** * join the body */ return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> { DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); DataBufferUtils.retain(buffer); return Mono.just(buffer); }); /** * repackage ServerHttpRequest */ ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return cachedFlux; } }; /** * mutate exchage with new ServerHttpRequest */ ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); /** * read body string with default messageReaders */ return ServerRequest.create(mutatedExchange, messageReaders).bodyToMono(String.class) .doOnNext(objectValue -> { log.debug("[GatewayContext]Read JsonBody:{}", objectValue); }).then(chain.filter(mutatedExchange)); }); } @Override public int getOrder() { return HIGHEST_PRECEDENCE; } }
添加上这个过滤器后,终于可以在AuthFilter中获取到入参了,获取入参的方法代码如下:
//主要代码如下 request = getServerHttpRequest(request);//获得入参然后解密 return chain.filter(exchange.mutate().request(request).build());//然后把解密后的参数继续传下去 private ServerHttpRequest getServerHttpRequest(ServerHttpRequest request) { String bodyStr = resolveBodyFromRequest(request); System.out.println("PUT OR POST bodyStr: " + bodyStr); String decode = ""; try { if(StringUtils.isEmpty(bodyStr)){ return request; } decode = AESUtil.aesDecodeByKey(bodyStr, "MP5v0^zee5Qlgq5V");//解密方法可以自由替换,这里使用AES为例。 } catch (Exception e) { e.printStackTrace(); } URI uri = request.getURI(); DataBuffer bodyDataBuffer = stringBuffer(decode); Flux<DataBuffer> bodyFlux1 = Flux.just(bodyDataBuffer); request = new ServerHttpRequestDecorator(request) { @Override public Flux<DataBuffer> getBody() { return bodyFlux1; } }; return request; } private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) { //获取请求体 Flux<DataBuffer> body = serverHttpRequest.getBody(); AtomicReference<String> bodyRef = new AtomicReference<>(); body.subscribe(buffer -> { CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); DataBufferUtils.release(buffer); bodyRef.set(charBuffer.toString()); }); //获取request body return bodyRef.get(); } private DataBuffer stringBuffer(String value) { this.value = value; byte[] bytes = value.getBytes(StandardCharsets.UTF_8); NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; }
二、给入参加密
把AES工具类顺手贴出来:
import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; /** * AES加解密工具类 * mjf on 2018/3/5. */ public class AESUtil { private static final String ENCODE_RULES = "mjfeng"; /** * 加密 * 1.构造密钥生成器 * 2.根据ecnodeRules规则初始化密钥生成器 * 3.产生密钥 * 4.创建和初始化密码器 * 5.内容加密 * 6.返回字符串 */ public static String aesEncode(String content) { try { //1.构造密钥生成器,指定为AES算法,不区分大小写 KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); //2.根据ecnodeRules规则初始化密钥生成器 //生成一个128位的随机源,根据传入的字节数组 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(ENCODE_RULES.getBytes()); keyGenerator.init(128, random); //3.产生原始对称密钥 SecretKey originalKey = keyGenerator.generateKey(); //4.获得原始对称密钥的字节数组 byte[] raw = originalKey.getEncoded(); //5.根据字节数组生成AES密钥 SecretKey key = new SecretKeySpec(raw, "AES"); //6.根据指定算法AES自成密码器 Cipher cipher = Cipher.getInstance("AES"); //7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY cipher.init(Cipher.ENCRYPT_MODE, key); //8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码 byte[] byteEncode = content.getBytes("utf-8"); //9.根据密码器的初始化方式--加密:将数据加密 byte[] byteAES = cipher.doFinal(byteEncode); //10.将加密后的数据转换为字符串 //这里用Base64Encoder中会找不到包 //解决办法: //在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。 String aesEncode = new String(new BASE64Encoder().encode(byteAES)); //11.将字符串返回 return aesEncode; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //如果有错就返加nulll return null; } /** * 解密 * 解密过程: * 1.同加密1-4步 * 2.将加密后的字符串反纺成byte[]数组 * 3.将加密内容解密 */ public static String aesDecode(String content) { try { //1.构造密钥生成器,指定为AES算法,不区分大小写 KeyGenerator keygen = KeyGenerator.getInstance("AES"); //2.根据ecnodeRules规则初始化密钥生成器 //生成一个128位的随机源,根据传入的字节数组 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(ENCODE_RULES.getBytes()); keygen.init(128, random); //3.产生原始对称密钥 SecretKey originalKey = keygen.generateKey(); //4.获得原始对称密钥的字节数组 byte[] raw = originalKey.getEncoded(); //5.根据字节数组生成AES密钥 SecretKey key = new SecretKeySpec(raw, "AES"); //6.根据指定算法AES自成密码器 Cipher cipher = Cipher.getInstance("AES"); //7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY cipher.init(Cipher.DECRYPT_MODE, key); //8.将加密并编码后的内容解码成字节数组 byte[] byteContent = new BASE64Decoder().decodeBuffer(content); /* * 解密 */ byte[] byteDecode = cipher.doFinal(byteContent); String aesDecode = new String(byteDecode, "utf-8"); return aesDecode; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } //如果有错就返加nulll return null; } /** * 解密 * 解密过程: * 1.同加密1-4步 * 2.将加密后的字符串反纺成byte[]数组 * 3.将加密内容解密 */ public static String aesDecodeByKey(String content, String aesKey) { try { byte[] byteContent = new BASE64Decoder().decodeBuffer(content); SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); /* * 解密 */ byte[] byteDecode = cipher.doFinal(byteContent); String aesDecode = new String(byteDecode, "utf-8"); return aesDecode; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } //如果有错就返加nulll return null; } /** * 加密 * 1.构造密钥生成器 * 2.根据ecnodeRules规则初始化密钥生成器 * 3.产生密钥 * 4.创建和初始化密码器 * 5.内容加密 * 6.返回字符串 */ public static String aesEncodeByKey(String content, String aesKey) { try { SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); //8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码 byte[] byteEncode = content.getBytes("utf-8"); //9.根据密码器的初始化方式--加密:将数据加密 byte[] byteAES = cipher.doFinal(byteEncode); //10.将加密后的数据转换为字符串 //这里用Base64Encoder中会找不到包 //解决办法: //在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。 String aesEncode = new String(new BASE64Encoder().encode(byteAES)); //11.将字符串返回 return aesEncode; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //如果有错就返加nulll return null; } }
三、出参加密并传给前端
出参加密也是使用继承了GlobalFilter的一个过滤器,但是中间遇到了byte数组转string丢失数据的问题。
在过滤器中获得的出参是byte[]格式的,想把byte[]转为string再进行加密操作,可是如果byte[]的长度比较大的话,转为string的时候会出现数据丢失的情况,试了几种byte[]转string的方法,要么参数前面不完整,要么后面不完整。这个问题是这么解决的,把出参加密的过滤器代码贴出来。
import com.alibaba.nacos.shaded.com.google.common.collect.Lists; import com.google.common.base.Joiner; import com.google.common.base.Throwables; import com.tbenefitofcloud.common.core.utils.StringUtils; import com.tbenefitofcloud.gateway.config.properties.IgnoreWhiteProperties; import org.apache.commons.io.IOUtils; import org.apache.poi.hssf.record.pivottable.StreamIDRecord; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.stereotype.Component; import org.springframework.util.Base64Utils; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartRequest; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.*; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; @Component public class ResponseFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(ResponseFilter.class); private static Joiner joiner = Joiner.on(""); @Autowired private IgnoreWhiteProperties ignoreWhite; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); String methodValue = request.getMethodValue(); ServerHttpResponse originalResponse = exchange.getResponse(); DataBufferFactory bufferFactory = originalResponse.bufferFactory(); log.info("进入响应拦截"); ServerHttpResponse response = exchange.getResponse(); DataBufferFactory dataBufferFactory = response.bufferFactory(); ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) { // 获取ContentType,判断是否返回JSON格式数据 String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); //(返回数据内如果字符串过大,默认会切割)解决返回体分段传输 return super.writeWith(fluxBody.buffer().map(dataBuffers -> { List<String> list = Lists.newArrayList(); dataBuffers.forEach(dataBuffer -> { try { byte[] content = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(content); DataBufferUtils.release(dataBuffer); list.add(new String(content, "utf-8")); } catch (Exception e) { log.info("加载Response字节流异常,失败原因:{}", Throwables.getStackTraceAsString(e)); } }); String responseData = joiner.join(list); System.out.println("responseData:"+responseData); String s = AESUtil.aesEncodeByKey(responseData, "MP5v0^zee5Qlgq5V"); s = s.replaceAll("\r\n", "").replaceAll("\n",""); byte[] uppedContent = new String(s.getBytes(), Charset.forName("UTF-8")).getBytes(); originalResponse.getHeaders().setContentLength(uppedContent.length); return bufferFactory.wrap(uppedContent); })); } } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decoratedResponse).build()); } @Override public int getOrder() { return -2; } private String Bytes2String(byte[] bytes){ Charset cs = Charset.forName("UTF-8"); ByteBuffer bb = ByteBuffer.allocate(bytes.length); bb.put(bytes).flip(); CharBuffer cb = cs.decode(bb); String res = new String(cb.array()); return res; } }
这样出入参加密解密的基础功能就实现了。
PS:前后端加密的方式方法一定要一致,同样加密方式的不同加密方法也不行。
白名单,加密开关等功能请自由添加。
入参解密支持POST请求的@RequestBody修饰的字符串和实体类。GET请求和其他的入参注解等后续会慢慢测试。