方案
原理:
1. 使用AES随机生成Key,加解密参数;
2. 使用RSA加解密Key;
具体操作:
1. 前端使用随机Key加密参数,使用固定RSA秘钥加密Key,请求到后端;
2. 后端收到加密Key,以及加密参数,先使用RSA解密Key,再用解密Key解密参数;
3. 返回使用原Key加密返回参数,使用RSA加密Key;
即:
1. 前端请求公钥加密,后端收到私钥解密;
2. 后端返回私钥加密,前端收到公钥解密。
ReqFilter
package cn.nocov.hospital.gateway.filter;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.nocov.hospital.gateway.config.redis.RedisUtil;
import cn.nocov.hospital.gateway.util.IpUtil;
import cn.nocov.hospital.gateway.util.NacosCfgUtil;
import cn.nocov.hospital.gateway.util.RequestUriUtil;
import cn.nocov.hospital.gateway.util.RsaAesUtil;
import cn.nocov.hospital.gateway.util.TokenUtil;
import cn.nocov.hospital.gateway.util.TokenUtil.ProductPlatformEnum;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest.Builder;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: Zek
* @date: 2020/8/13 on 4:36 下午
* @description: 请求过滤器,注意:@Order、Ordered虽然都是执行顺序,但是使用@Order注解会导致url少一位,不知道啥问题
*/
@Slf4j
@Configuration
public class ReqFilter implements GlobalFilter, Ordered {
@Resource private NacosCfgUtil nacosCfgUtil;
@Resource private RedisUtil redisUtil;
@Resource private RequestUriUtil requestUriUtil;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MDC.clear();
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (RequestMethod.OPTIONS.name().equals(request.getMethodValue())) {
return chain.filter(exchange);
}
URI requestUri = request.getURI();
String uri = requestUri.getPath();
String url = requestUri.toString();
HttpHeaders headers = request.getHeaders();
ProductPlatformEnum appPlatform =
ProductPlatformEnum.toEnum(headers.getFirst(Constant.HEADER_PLATFORM));
MDC.put(Constant.HEADER_PLATFORM, appPlatform == null ? "" : appPlatform.name());
String appVersion = headers.getFirst(Constant.HEADER_VERSION);
MDC.put(Constant.HEADER_VERSION, appVersion);
String requestId = headers.getFirst(Constant.HEADER_REQUEST_ID);
MDC.put(Constant.HEADER_REQUEST_ID, requestId);
HttpMethod method = request.getMethod();
log.info(
"{}--------------------》Send {} To {}\n{}",
requestId,
method,
url,
headers.toSingleValueMap());
boolean needEncrypt = requestUriUtil.noTokenNeedEncrypt(uri);
if (needEncrypt) {
// 判断请求方式是否需要加密
needEncrypt = needEncryptJudgeRequest(headers, method);
if (needEncrypt && !TokenUtil.checkHeader(appPlatform, appVersion, requestId)) {
return errorReturn(response, requestId, "512", "版本过低,请前往商店更新最新版本");
}
} else {
if (CharSequenceUtil.isNotBlank(requestId)) {
requestId = TokenUtil.getRequestId();
}
}
if (CharSequenceUtil.isNotBlank(requestId)) {
response.getHeaders().set(Constant.HEADER_REQUEST_ID, requestId);
}
String ip = IpUtil.getIp(request);
Builder mutate =
request
.mutate()
.header(Constant.HEADER_IP, ip)
.header(Constant.HEADER_REQUEST_ID, requestId);
// 需要校验token的接口
String token = "";
if (requestUriUtil.needToken(uri)) {
if (appPlatform == null) {
log.info("{}--------------------》登录校验失败:appPlatform == null,url:{}", requestId, url);
return errorReturn(response, requestId, "302", "登录验证失败,请重新登录");
}
token = TokenUtil.getToken(request, appPlatform);
if (CharSequenceUtil.isBlank(token)) {
log.info("{}--------------------》登录校验失败:cookie token == null/'',url:{}", requestId, url);
return errorReturn(response, requestId, "302", "登录验证失败,请重新登录");
}
String userInfoKey = redisUtil.get(loginRedisKey);
String redisPlatform = getPlatform(userInfoKey);
xxxxx;
String userId = getUserId(userInfoKey);
MDC.put(Constant.HEADER_USER_ID, userId);
mutate
.header(Constant.HEADER_USER_ID, userId)
.header(Constant.HEADER_VERSION, appVersion)
.header(Constant.HEADER_PLATFORM, appPlatform.name())
.header(
Constant.HEADER_USERNAME,
URLEncodeUtil.encode(redisUtil.hGet(userInfoKey, "username")));
}
if (hasRepeatRequest(token, ip, appPlatform, method + uri)) {
log.info("{}--------------------》重复请求{}", requestId, method + uri);
return errorReturn(response, requestId, "512", "请稍等一下喔(勿频繁点击)");
}
// post请求时,如果是文件上传之类的请求,不修改请求消息体
exchange.getAttributes().put(Constant.EXCHANGE_HAS_AES, needEncrypt);
if (needEncrypt) {
exchange.getAttributes().put(Constant.EXCHANGE_REQUEST_ID, requestId);
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
try {
if (HttpMethod.GET == method) {
return encryptParamsGet(
chain,
exchange,
request,
response,
mutate,
requestUri,
requestId,
appPlatform,
copyOfContextMap);
} else {
return encryptParamsPost(
chain, exchange, headers, requestId, appPlatform, copyOfContextMap);
}
} catch (Exception e) {
log.error("{}--------------------》加解密失败:", requestId, e);
return errorReturn(response, requestId, "512", "请求异常");
}
}
return chain.filter(exchange.mutate().request(request).build());
}
private Mono<Void> encryptParamsGet(
GatewayFilterChain chain,
ServerWebExchange exchange,
ServerHttpRequest request,
ServerHttpResponse response,
Builder mutate,
URI requestUri,
String requestIdValue,
ProductPlatformEnum appPlatform,
Map<String, String> copyOfContextMap) {
MDC.setContextMap(copyOfContextMap);
Map<String, String> queryParams = request.getQueryParams().toSingleValueMap();
if (!queryParams.isEmpty() && queryParams.containsKey("v")) {
String v = queryParams.get("v");
if (CharSequenceUtil.isBlank(v)) {
return errorReturn(response, requestIdValue, "512", "请求异常");
}
log.info("{}--------------------》原V:{}", requestIdValue, v);
v = RsaAesUtil.rsaDecryptAesKey(appPlatform, v);
exchange.getAttributes().put(Constant.EXCHANGE_AES_KEY, v);
URI uri;
if (queryParams.containsKey("d")) {
// 替换查询参数
String d = queryParams.get("d");
log.info("{}--------------------》原D:{}", requestIdValue, d);
String decryptParams =
CharSequenceUtil.isBlank(d)
? ""
: RsaAesUtil.aesDecryptParams(v.getBytes(StandardCharsets.UTF_8), d);
log.info("{}--------------------》解D:{}", requestIdValue, decryptParams);
uri =
UriComponentsBuilder.fromUri(requestUri)
.replaceQuery(URLEncodeUtil.encode(decryptParams))
.build(true)
.toUri();
} else {
uri = UriComponentsBuilder.fromUri(requestUri).replaceQuery("").build(true).toUri();
}
ServerHttpRequest build = mutate.uri(uri).build();
log.info(
"{}--------------------》重构 {} To {}\n{}",
requestIdValue,
build.getMethodValue(),
build.getURI(),
build.getHeaders().toSingleValueMap());
return chain.filter(exchange.mutate().request(build).build());
} else {
return errorReturn(response, requestIdValue, "512", "请求异常");
}
}
private Mono<Void> encryptParamsPost(
GatewayFilterChain chain,
ServerWebExchange exchange,
HttpHeaders httpHeaders,
String requestIdValue,
ProductPlatformEnum appPlatform,
Map<String, String> copyOfContextMap) {
// read & modify body
MDC.setContextMap(copyOfContextMap);
ServerRequest serverRequest =
ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
Mono<String> modifiedBody =
serverRequest
.bodyToMono(String.class)
.flatMap(
body -> {
MDC.setContextMap(copyOfContextMap);
// 对原先的body进行修改操作
String str = StrUtil.str(body, StandardCharsets.UTF_8);
log.info("{}--------------------》收参:{}", requestIdValue, str);
JSONObject jsonObject = JSONUtil.parseObj(str);
if (JSONUtil.isNull(jsonObject)) {
return Mono.error(new RuntimeException("请求异常"));
}
if (!jsonObject.containsKey("v")) {
return Mono.error(new RuntimeException("请求异常"));
}
String v = jsonObject.getStr("v");
if (CharSequenceUtil.isBlank(v)) {
return Mono.error(new RuntimeException("请求异常"));
}
v = RsaAesUtil.rsaDecryptAesKey(appPlatform, v);
exchange.getAttributes().put(Constant.EXCHANGE_AES_KEY, v);
if (jsonObject.containsKey("d")) {
String d = jsonObject.getStr("d");
d =
CharSequenceUtil.isBlank(d)
? JSONUtil.toJsonStr(JSONUtil.createObj())
: RsaAesUtil.aesDecryptParams(v.getBytes(StandardCharsets.UTF_8), d);
log.info("{}--------------------》解D:{}", requestIdValue, d);
return Mono.just(d);
} else {
return Mono.empty();
}
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(httpHeaders);
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter
.insert(outputMessage, new BodyInserterContext())
.then(
Mono.defer(
() -> {
MDC.setContextMap(copyOfContextMap);
ServerHttpRequestDecorator decorator =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0L) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
log.info(
"{}--------------------》重构 {} To {}\n{}",
requestIdValue,
decorator.getMethodValue(),
decorator.getURI(),
decorator.getHeaders().toSingleValueMap());
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
private Mono<Void> errorReturn(
ServerHttpResponse response, String requestId, String code, String msg) {
if ("302".equals(code)) {
response.addCookie(
ResponseCookie.from(Constant.TOKEN, "").path("/").maxAge(Duration.ofSeconds(0L)).build());
}
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
Map<String, String> map = Maps.newHashMapWithExpectedSize(3);
map.put("code", code);
map.put("msg", msg);
map.put("obj", null);
log.info("{}--------------------》异常返回:{}", requestId, map);
MDC.clear();
return response.writeWith(
Flux.just(response.bufferFactory().wrap(JSON.toJSONString(map).getBytes())));
}
/**
* 是否加解密
*
* @param headers
* @return
*/
private boolean needEncryptJudgeRequest(HttpHeaders headers, HttpMethod method) {
return method == HttpMethod.GET
|| (method == HttpMethod.POST
&& (CharSequenceUtil.containsAny(
headers.getFirst(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON_VALUE)));
}
/**
* 检查重复请求
*
* @param token 判断唯一请求
* @param ip 判断唯一请求
* @param appPlatform 判断唯一请求
* @param uri
* @return
*/
public boolean hasRepeatRequest(
String token, String ip, ProductPlatformEnum appPlatform, String uri) {
return xxx;
}
private String getPlatform(String userInfoKey) {
return xxx;
}
private String getUserId(String loginRedisValue) {
return xxx;
}
private void asyncUserAgentFun(HttpHeaders headers) {
log.info(xxx);
}
/** 顺序:数字越小,越先执行 */
@Override
public int getOrder() {
return -2;
}
}
RespFilter
package cn.nocov.hospital.gateway.filter;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.nocov.hospital.gateway.util.RsaAesUtil;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.slf4j.MDC;
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.DefaultDataBufferFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author: Zek
* @date: 2020/8/13 on 4:36 下午
* @description:
*/
@Slf4j
@Configuration
public class RespFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Object objHasAes = exchange.getAttributes().get(Constant.EXCHANGE_HAS_AES);
Boolean hasAes = objHasAes == null ? null : (Boolean) objHasAes;
String requestIdValue = (String) exchange.getAttributes().get(Constant.EXCHANGE_REQUEST_ID);
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse
.getHeaders()
.set(
Constant.HEADER_REQUEST_ID,
CharSequenceUtil.isBlank(requestIdValue) ? "" : requestIdValue);
if (hasAes == null || !hasAes) {
clearExchange(exchange);
return chain.filter(exchange.mutate().response(originalResponse).build());
}
String key = (String) exchange.getAttributes().get(Constant.EXCHANGE_AES_KEY);
log.info("{}--------------------》Exchange:{}", requestIdValue, key);
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse =
new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(
fluxBody
.buffer()
.map(
dataBuffer -> {
MDC.setContextMap(copyOfContextMap);
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 释放掉内存
DataBufferUtils.release(join);
String respBody = StrUtil.str(content, StandardCharsets.UTF_8);
if (!JSONUtil.isTypeJSON(respBody)) {
log.error(
"{}--------------------》非JSON:{}", requestIdValue, respBody);
return bufferFactory.wrap(content);
}
JSONObject resp = JSONUtil.parseObj(respBody);
String obj = resp.getStr("obj");
if (!JSONUtil.isNull(obj)) {
resp.set(
"obj",
RsaAesUtil.aesEncryptParams(
key.getBytes(StandardCharsets.UTF_8), obj));
}
log.info(
"{}--------------------》返D:{}",
requestIdValue,
(respBody.length() > 1000
? respBody.substring(0, 1000) + "......."
: respBody));
// 加密后的数据返回给客户端
return bufferFactory.wrap(JSONUtil.toJsonStr(resp).getBytes());
}));
}
return super.writeWith(body);
}
};
clearExchange(exchange);
MDC.clear();
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
/** 顺序:数字越小,越先执行 */
@Override
public int getOrder() {
return -1;
}
private void clearExchange(ServerWebExchange exchange) {
exchange.getAttributes().remove(Constant.EXCHANGE_AES_KEY);
exchange.getAttributes().remove(Constant.EXCHANGE_HAS_AES);
}
}
RsaAesUtil
package cn.nocov.hospital.gateway.util;
import cn.hutool.core.lang.Console;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.symmetric.AES;
import cn.nocov.hospital.gateway.util.TokenUtil.ProductPlatformEnum;
import javax.crypto.spec.IvParameterSpec;
/**
* @author: Zek
* @date: 2022/3/8 on 11:05 AM
* @desc:
*/
public class RsaAesUtil {
@Deprecated
private static final RSA XXXX =
SecureUtil.rsa(
"xx",
"xx");
/**
* AES 解密
*
* @param aesKey
* @param aesParams
* @return
*/
public static String aesDecryptParams(byte[] aesKey, String aesParams) {
AES aes = new AES("CBC", "PKCS7Padding", aesKey, new IvParameterSpec(aesKey, 0, 16).getIV());
return aes.decryptStr(aesParams);
}
/**
* AES 加密
*
* @param aesKey
* @param aesParams
* @return
*/
public static String aesEncryptParams(byte[] aesKey, String aesParams) {
return aes(aesKey).encryptBase64(aesParams);
}
/**
* 获取aes
*
* @param aesKey
* @return
*/
private static AES aes(byte[] aesKey) {
return new AES("CBC", "PKCS7Padding", aesKey, new IvParameterSpec(aesKey, 0, 16).getIV());
}
/**
* rsa 解密 aesKey
*
* @param appPlatform
* @param aesKey
* @return
*/
public static String rsaDecryptAesKey(ProductPlatformEnum appPlatform, String aesKey) {
return getRsa(appPlatform).decryptStr(aesKey, KeyType.PrivateKey);
}
/**
* 获取RSA
*
* @param appPlatform
* @return
*/
private static RSA getRsa(ProductPlatformEnum appPlatform) {
xxxxxxx
}
}
public static void main(String[] args) {
RSA rsa = SecureUtil.rsa();
Console.log(rsa.getPrivateKeyBase64());
Console.log(rsa.getPublicKeyBase64());
}
}
效果
问题:
会随机产生HTTP method names must be tokens,下篇文章解决(已解决但是不知道原因);