Skip to content

Commit b6fc21d

Browse files
committed
feat: 网关请求参数验签、加解密
1 parent 2175ac3 commit b6fc21d

File tree

9 files changed

+314
-82
lines changed

9 files changed

+314
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright © 2019 collin (1634753825@qq.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.smartframework.cloud.examples.support.gateway.constants;
17+
18+
/**
19+
* 数据安全常量
20+
*
21+
* @author collin
22+
* @date 2024-04-11
23+
*/
24+
public class DataSecurityConstants {
25+
26+
/**
27+
* url参数名
28+
*/
29+
public static final String URL_PARAM_NAME = "q";
30+
31+
}

application-module/support-module/support-service-gateway/src/main/java/org/smartframework/cloud/examples/support/gateway/constants/GatewayReturnCodes.java

+4
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,9 @@ public interface GatewayReturnCodes {
8585
* 不支持数据安全
8686
*/
8787
String NOT_SUPPORT_DATA_SECURITY = "400017";
88+
/**
89+
* 请求nonce缺失
90+
*/
91+
String REQUEST_NONCE_MISSING = "400018";
8892

8993
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright © 2019 collin (1634753825@qq.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.smartframework.cloud.examples.support.gateway.dto;
17+
18+
import lombok.Getter;
19+
import lombok.Setter;
20+
import lombok.ToString;
21+
22+
@Getter
23+
@Setter
24+
@ToString
25+
public class DataSecurityParamDTO {
26+
27+
private String headers;
28+
private String urlParamsBase64;
29+
private String bodyBase64;
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright © 2019 collin (1634753825@qq.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.smartframework.cloud.examples.support.gateway.exception;
17+
18+
import io.github.smart.cloud.exception.BaseException;
19+
20+
/**
21+
* 请求nonce异常
22+
*
23+
* @author collin
24+
* @date 2024-04-11
25+
*/
26+
public class RequestNonceException extends BaseException {
27+
28+
public RequestNonceException(String code) {
29+
super(code);
30+
}
31+
32+
}

application-module/support-module/support-service-gateway/src/main/java/org/smartframework/cloud/examples/support/gateway/filter/access/core/datasecurity/DataSecurityServerHttpRequestDecorator.java

+95-79
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import io.github.smart.cloud.api.core.annotation.enums.SignType;
1919
import io.github.smart.cloud.common.web.constants.SmartHttpHeaders;
20-
import io.github.smart.cloud.constants.SymbolConstant;
2120
import io.github.smart.cloud.exception.DataValidateException;
2221
import io.github.smart.cloud.exception.ParamValidateException;
2322
import io.github.smart.cloud.starter.redis.adapter.IRedisAdapter;
@@ -26,32 +25,29 @@
2625
import lombok.extern.slf4j.Slf4j;
2726
import org.apache.commons.lang3.StringUtils;
2827
import org.smartframework.cloud.examples.support.gateway.cache.SecurityKeyCache;
29-
import org.smartframework.cloud.examples.support.gateway.constants.GatewayConstants;
3028
import org.smartframework.cloud.examples.support.gateway.constants.GatewayReturnCodes;
29+
import org.smartframework.cloud.examples.support.gateway.dto.DataSecurityParamDTO;
3130
import org.smartframework.cloud.examples.support.gateway.exception.AesKeyNotFoundException;
3231
import org.smartframework.cloud.examples.support.gateway.exception.RequestSignFailException;
3332
import org.smartframework.cloud.examples.support.gateway.exception.UnsupportedFunctionException;
34-
import org.smartframework.cloud.examples.support.gateway.filter.rewrite.RewriteServerHttpRequestDecorator;
3533
import org.smartframework.cloud.examples.support.gateway.util.RedisKeyHelper;
3634
import org.smartframework.cloud.examples.support.gateway.util.RewriteHttpUtil;
35+
import org.smartframework.cloud.examples.support.gateway.util.SignUtil;
3736
import org.smartframework.cloud.examples.support.gateway.util.WebUtil;
3837
import org.springframework.core.io.buffer.DataBuffer;
3938
import org.springframework.core.io.buffer.DataBufferFactory;
40-
import org.springframework.http.HttpMethod;
41-
import org.springframework.http.MediaType;
4239
import org.springframework.http.server.reactive.ServerHttpRequest;
4340
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
4441
import org.springframework.lang.NonNull;
42+
import org.springframework.util.Base64Utils;
4543
import org.springframework.util.MultiValueMap;
44+
import org.springframework.web.util.UriComponents;
45+
import org.springframework.web.util.UriComponentsBuilder;
4646
import reactor.core.publisher.Flux;
4747

48-
import java.io.UnsupportedEncodingException;
49-
import java.net.URLDecoder;
48+
import java.net.URI;
5049
import java.nio.charset.StandardCharsets;
5150
import java.security.interfaces.RSAPublicKey;
52-
import java.util.ArrayList;
53-
import java.util.Arrays;
54-
import java.util.List;
5551

5652
/**
5753
* 请求参数签名校验、解密
@@ -62,100 +58,117 @@
6258
@Slf4j
6359
public class DataSecurityServerHttpRequestDecorator extends ServerHttpRequestDecorator {
6460

65-
private transient Flux<DataBuffer> body;
6661
private final IRedisAdapter redisAdapter;
62+
63+
private transient Flux<DataBuffer> body;
64+
private transient URI uri;
65+
private transient MultiValueMap<String, String> queryParams;
66+
6767
private transient SecurityKeyCache securityKeyCache;
6868

6969
DataSecurityServerHttpRequestDecorator(ServerHttpRequest request, DataBufferFactory dataBufferFactory, String token, boolean requestDecrypt, byte signType, IRedisAdapter redisAdapter) {
7070
super(request);
7171

72-
if ((requestDecrypt || signType == SignType.REQUEST.getType() || signType == SignType.ALL.getType())
73-
&& !RewriteHttpUtil.isSupported(super.getHeaders().getContentType())) {
72+
this.redisAdapter = redisAdapter;
73+
74+
if (!requestDecrypt && signType == SignType.NONE.getType()) {
75+
this.body = super.getBody();
76+
this.uri = super.getURI();
77+
this.queryParams = super.getQueryParams();
78+
return;
79+
}
80+
81+
if ((requestDecrypt || signType == SignType.REQUEST.getType() || signType == SignType.ALL.getType()) && !RewriteHttpUtil.isSupported(super.getHeaders().getContentType())) {
7482
throw new UnsupportedFunctionException(GatewayReturnCodes.NOT_SUPPORT_DATA_SECURITY);
7583
}
7684

77-
Flux<DataBuffer> flux = super.getBody();
78-
this.redisAdapter = redisAdapter;
85+
DataSecurityParamDTO dataSecurityParam = SignUtil.getDataSecurityParams(request);
7986

80-
final String requestStr = getEncryptedRequestStr(request);
87+
// 1、请求参数验签
88+
checkRequestSign(request, signType, token, dataSecurityParam);
8189

82-
// 请求信息验签
83-
checkRequestSign(request, signType, requestStr, token);
90+
// 2、param base64 decode
91+
String base64DecodeUrlParams = StringUtils.isBlank(dataSecurityParam.getUrlParamsBase64()) ? null : new String(Base64Utils.decodeFromString(dataSecurityParam.getUrlParamsBase64()));
92+
String base64DecodeBody = StringUtils.isBlank(dataSecurityParam.getBodyBase64()) ? null : new String(Base64Utils.decodeFromString(dataSecurityParam.getBodyBase64()));
8493

94+
if (base64DecodeUrlParams == null && base64DecodeBody == null) {
95+
setRealUriData(base64DecodeUrlParams, requestDecrypt, null);
96+
setRealBody(dataBufferFactory, token, base64DecodeBody, requestDecrypt, null);
97+
98+
return;
99+
}
100+
101+
// 3、解密
85102
if (requestDecrypt) {
86-
flux.subscribe(buffer -> {
103+
super.getBody().subscribe(buffer -> {
87104
SecurityKeyCache securityKeyCache = getSecurityKeyCache(token);
88105
String aesKey = securityKeyCache.getAesKey();
89106
if (StringUtils.isBlank(aesKey)) {
90107
throw new AesKeyNotFoundException();
91108
}
92-
String decryptedRequestStr = AesUtil.decrypt(requestStr, aesKey);
93-
94-
HttpMethod httpMethod = request.getMethod();
95-
if (httpMethod == HttpMethod.GET) {
96-
decryptUrlParams(decryptedRequestStr, request.getQueryParams());
97-
} else if (httpMethod == HttpMethod.POST) {
98-
MediaType contentType = request.getHeaders().getContentType();
99-
if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(contentType.toString())) {
100-
decryptUrlParams(decryptedRequestStr, request.getQueryParams());
101-
} else if (MediaType.APPLICATION_JSON_VALUE.equals(contentType.toString())) {
102-
this.body = Flux.just(dataBufferFactory.wrap(decryptedRequestStr.getBytes(StandardCharsets.UTF_8)));
103-
}
104-
}
109+
110+
setRealUriData(base64DecodeUrlParams, true, aesKey);
111+
setRealBody(dataBufferFactory, token, base64DecodeBody, true, aesKey);
105112
});
106113
} else {
107-
this.body = flux;
114+
// 重写base64解密后的参数
115+
setRealUriData(base64DecodeUrlParams, requestDecrypt, null);
116+
setRealBody(dataBufferFactory, token, base64DecodeBody, requestDecrypt, null);
108117
}
109118
}
110119

111120
@Override
112121
public Flux<DataBuffer> getBody() {
113-
return body;
122+
return this.body;
114123
}
115124

116-
private void decryptUrlParams(String decryptedRequestStr, MultiValueMap<String, String> queryParams) {
117-
queryParams.remove(GatewayConstants.REQUEST_ENCRYPT_PARAM_NAME);
118-
Arrays.stream(decryptedRequestStr.split(SymbolConstant.AND)).forEach(param -> {
119-
if (param.contains(SymbolConstant.EQUAL)) {
120-
String[] entry = param.split(SymbolConstant.EQUAL);
121-
if (entry.length > 0) {
122-
String value = entry[1];
123-
if (StringUtils.isNotBlank(value)) {
124-
try {
125-
value = URLDecoder.decode(value, StandardCharsets.UTF_8.name());
126-
} catch (UnsupportedEncodingException e) {
127-
log.error("decode.error|value={}", value, e);
128-
}
129-
}
130-
List<String> values = new ArrayList<>(1);
131-
values.add(value);
132-
queryParams.put(entry[0], values);
133-
}
134-
}
135-
});
125+
@Override
126+
public URI getURI() {
127+
return this.uri;
128+
}
129+
130+
@Override
131+
public MultiValueMap<String, String> getQueryParams() {
132+
return this.queryParams;
136133
}
137134

138135
/**
139-
* 获取加密后的请求参数
136+
* 重写body参数
140137
*
141-
* @param request
142-
* @return
138+
* @param dataBufferFactory
139+
* @param token
140+
* @param base64DecodeBody
141+
* @param requestDecrypt
142+
* @param aesKey
143143
*/
144-
private String getEncryptedRequestStr(ServerHttpRequest request) {
145-
String requestStr = null;
146-
HttpMethod httpMethod = request.getMethod();
147-
if (httpMethod == HttpMethod.GET) {
148-
requestStr = request.getQueryParams().getFirst(GatewayConstants.REQUEST_ENCRYPT_PARAM_NAME);
149-
} else if (httpMethod == HttpMethod.POST) {
150-
MediaType contentType = request.getHeaders().getContentType();
151-
if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(contentType.toString())) {
152-
requestStr = request.getQueryParams().getFirst(GatewayConstants.REQUEST_ENCRYPT_PARAM_NAME);
153-
} else if (MediaType.APPLICATION_JSON_VALUE.equals(contentType.toString())) {
154-
RewriteServerHttpRequestDecorator rewriteServerHttpRequestDecorator = (RewriteServerHttpRequestDecorator) request;
155-
requestStr = rewriteServerHttpRequestDecorator.getBodyStr();
156-
}
144+
private void setRealBody(DataBufferFactory dataBufferFactory, String token, String base64DecodeBody, boolean requestDecrypt, String aesKey) {
145+
if (base64DecodeBody == null) {
146+
this.body = super.getBody();
157147
}
158-
return requestStr;
148+
149+
String realBody = requestDecrypt ? AesUtil.decrypt(base64DecodeBody, aesKey) : base64DecodeBody;
150+
this.body = Flux.just(dataBufferFactory.wrap(realBody.getBytes(StandardCharsets.UTF_8)));
151+
}
152+
153+
/**
154+
* 重写url参数
155+
*
156+
* @param base64DecodeUrlParams
157+
* @param requestDecrypt
158+
* @param aesKey
159+
*/
160+
private void setRealUriData(String base64DecodeUrlParams, boolean requestDecrypt, String aesKey) {
161+
if (base64DecodeUrlParams == null) {
162+
this.uri = super.getURI();
163+
this.queryParams = super.getQueryParams();
164+
return;
165+
}
166+
167+
String realUrlParamsStr = requestDecrypt ? AesUtil.decrypt(base64DecodeUrlParams, aesKey) : base64DecodeUrlParams;
168+
UriComponents uriComponents = UriComponentsBuilder.fromUri(super.getURI()).replaceQuery(realUrlParamsStr).build();
169+
170+
this.uri = uriComponents.toUri();
171+
this.queryParams = uriComponents.getQueryParams();
159172
}
160173

161174
/**
@@ -164,24 +177,25 @@ private String getEncryptedRequestStr(ServerHttpRequest request) {
164177
* @param request
165178
* @param signType
166179
* @param token
180+
* @param dataSecurityParam
167181
*/
168-
private void checkRequestSign(ServerHttpRequest request, byte signType, String requestStr, @NonNull String token) {
169-
if (StringUtils.isBlank(requestStr)) {
170-
return;
171-
}
182+
private void checkRequestSign(ServerHttpRequest request, byte signType, @NonNull String token, DataSecurityParamDTO dataSecurityParam) {
172183
if (SignType.REQUEST.getType() != signType && SignType.ALL.getType() != signType) {
173184
return;
174185
}
186+
175187
String sign = WebUtil.getFromRequestHeader(request, SmartHttpHeaders.SIGN);
176188
if (StringUtils.isBlank(sign)) {
177189
throw new ParamValidateException(GatewayReturnCodes.REQUEST_SIGN_MISSING);
178190
}
179191

192+
String requestSignContent = SignUtil.generateRequestSignContent(request.getMethod(), dataSecurityParam);
193+
180194
SecurityKeyCache securityKeyCache = getSecurityKeyCache(token);
181195
boolean signCheckResult = false;
182196
try {
183197
RSAPublicKey publicKey = RsaUtil.getRsaPublidKey(securityKeyCache.getCpubKeyModulus(), securityKeyCache.getCpubKeyExponent());
184-
signCheckResult = RsaUtil.checkSign(requestStr, sign, publicKey);
198+
signCheckResult = RsaUtil.checkSign(requestSignContent, sign, publicKey);
185199
} catch (Exception e) {
186200
log.error("sign.check.error", e);
187201
}
@@ -191,15 +205,17 @@ private void checkRequestSign(ServerHttpRequest request, byte signType, String r
191205
}
192206

193207
private SecurityKeyCache getSecurityKeyCache(@NonNull String token) {
194-
if (securityKeyCache != null) {
195-
return securityKeyCache;
208+
if (this.securityKeyCache != null) {
209+
return this.securityKeyCache;
196210
}
197211

198212
SecurityKeyCache securityKeyCache = (SecurityKeyCache) redisAdapter.get(RedisKeyHelper.getSecurityKey(token));
199213
if (securityKeyCache == null) {
200214
throw new DataValidateException(GatewayReturnCodes.SECURITY_KEY_EXPIRED);
201215
}
202-
return securityKeyCache;
216+
217+
this.securityKeyCache = securityKeyCache;
218+
return this.securityKeyCache;
203219
}
204220

205221
}

0 commit comments

Comments
 (0)