springboot中controller层实现通用的透明代理
# springboot中controller层实现通用的透明代理
代理的要求:
- 透明
- 可更改请求,响应头,体
- 支持流式请求和流式响应
# spring cloud gateway
首先想到api网关spring cloud gateway是否能用到controller层, 其实是有的:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-mvc</artifactId>
<version>3.1.6</version>
</dependency>
1
2
3
4
5
2
3
4
5
@PostMapping("/app-api/proxy/**")
public ResponseEntity<String> proxyPath(ProxyExchange<String> proxy) throws Exception {
System.out.println("proxyPath: "+proxy.path());
///app-api/chatgpt/chat-list/proxy/hello333
String path = proxy.path("/app-api/proxy/");
System.out.println("proxyPath2: "+path);
//hello333
//UnknownContentTypeException: no suitable HttpMessageConverter found for response type [?] and content type [text/html;charset=iso-8859-1]
//Could not extract response: no suitable HttpMessageConverter found for response type [?] and content type [text/html;charset=UTF-8]
//ResponseEntity.status()
// requestHeaders.remove("host");
// requestHeaders.remove("tenant-id");
//proxy.post().getHeaders();
return proxy.uri("https://xxx.yyy.zzz/" + path)
.header("host","")
//.headers(proxy.get().getHeaders())
.post();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面的代码泛型为string,即响应会解析为string然后返回.
ResponseEntity
但spring-cloud-gateway-mvc并不支持流式响应. 在官方issue里已有人咨询过.
ProxyExchange does not support streaming response (opens new window)
Streaming file download through spring-cloud-gateway can not open (opens new window)
# okhttp
直接使用okhttp进行代理转发,通过okhttp来支持流式请求和流式响应:
舒坦
okHttpClient = new OkHttpClient.Builder()
.readTimeout(Duration.ofSeconds(50))
.writeTimeout(Duration.ofSeconds(50))
.connectTimeout(Duration.ofSeconds(50))
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))
.build();
private void proxy(HttpServletRequest request,
Map<String, String> requestHeaders,
HttpServletResponse response, String url) throws IOException {
String queryString = request.getQueryString();
if (queryString != null) {
url += "?" + queryString;
}
//if(request.)
RequestBody requestBody = null;
InputStream inputStream0 = request.getInputStream();
if(inputStream0 != null){
String type = request.getHeader("content-type");
if(type ==null || type.isEmpty()){
type = "application/octet-stream";
}
requestBody = new InputStreamRequestBody(MediaType.parse(type),inputStream0);
//TODO inputStream.readAllBytes()是非stream操作, 如果是大文件上传,就会有性能问题和内存问题
/* byte[] bytes = inputStream.readAllBytes();
requestBody = RequestBody.create(bytes);*/
}
//todo 加上bare token
Request okHttpRequest = new Request.Builder()
.url(url)
.method(request.getMethod(),requestBody)
//.post(requestBody)
.headers(Headers.of(requestHeaders))
.build();
try (Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
// set response status code
response.setStatus(okHttpResponse.code());
// pass response headers
for (Map.Entry<String, List<String>> entry : okHttpResponse.headers().toMultimap().entrySet()) {
String key = entry.getKey();
List<String> value = entry.getValue();
////access-control-allow-origin: * : 会与外部重复,导致跨域问题,所以需要移除
//todo
if(!key.toLowerCase().startsWith("access-control")){
response.addHeader(key, value.get(0));
}
}
if(!okHttpResponse.isSuccessful()){
//todo
}
if(okHttpResponse.body() ==null){
return;
}
// set response content type
if( okHttpResponse.body().contentType() != null ){
response.setContentType(okHttpResponse.body().contentType().toString());
}
// streaming response body
try (InputStream inputStream = okHttpResponse.body().byteStream();
OutputStream outputStream = response.getOutputStream()) {
ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
// 一个inputStream往两个outputStream里写
OutputStream teeOutputStream = new TeeOutputStream(outputStream, outputStream2);
byte[] buffer = new byte[512];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
teeOutputStream.write(buffer, 0, bytesRead);
}
teeOutputStream.flush();
teeOutputStream.close();
byte[] bytes = outputStream2.toByteArray();
outputStream2.close();
String json = new String(bytes,0,bytes.length,"UTF-8");
//这里可以构建一个不影响流式响应的拦截/日志功能
StaticLog.info(json);
}
//ClassCastException: class java.util.LinkedHashMap cannot be cast to class [B (java.util.LinkedHashMap and
// [B are in module java.base of loader 'bootstrap')(String), java.lang.ClassCastException:
// class java.util.LinkedHashMap cannot be cast to class [B (java.util.LinkedHashMap and [B are in module java.base of loader 'bootstrap')
}catch (Throwable throwable){
StaticLog.error(throwable);
throw throwable;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
调用
注意header的添加和移除.以及跨域头的重复问题
@PostMapping(value = "/app-api/proxyStream/**")
@PreAuthenticated
public void proxy(HttpServletRequest request,
@RequestHeader Map<String, String> requestHeaders,
HttpServletResponse response ) throws IOException {
String url = request.getRequestURI();
String path = url.substring(url.indexOf("/app-api/proxyStream/")+"/app-api/proxyStream/".length());
StaticLog.debug("path---> "+path);
StaticLog.debug("headers---> "+requestHeaders.toString());
url = "https://yyy.xxx.zzz/"+ path;
requestHeaders.remove("host");
requestHeaders.remove("X-Real-IP");
requestHeaders.remove("X-Forwarded-For");
requestHeaders.remove("tenant-id");
requestHeaders.remove("Referer");
requestHeaders.remove("referer");
requestHeaders.remove("Origin");
requestHeaders.remove("origin");
requestHeaders.remove("Authorization");
requestHeaders.remove("authorization");
requestHeaders.remove("device-id");
requestHeaders.remove("language-id");
requestHeaders.remove("language-id");
requestHeaders.remove("version-name");
requestHeaders.remove("version-code");
requestHeaders.remove("device-type");
requestHeaders.remove("accept-language");
Iterator<String> iterator = requestHeaders.keySet().iterator();
while (iterator.hasNext()){
String next = iterator.next();
if(next.startsWith("sec-")){
iterator.remove();
}
}
requestHeaders.put("user-agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36");
StaticLog.info("headers after remove---> "+requestHeaders.toString());
//{user-agent=navigation/1.0.07.1007 Dalvik/2.1.0 (Linux; U; Android 10; HRY-AL00a Build/HONORHRY-AL00a),
// version-name=1.0.07, accept-encoding=gzip, device-id=eb791a8665fb5255,
// version-code=1007, content-type=application/json, language-id=123, accept=application/json,*/*,
// content-length=1802, app-version=1007, device-type=Android}
//web:
//{connection=keep-alive, content-length=99, sec-ch-ua="Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99",
// content-type=application/json, sec-ch-ua-mobile=?1, user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
// AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36, sec-ch-ua-platform="Android", accept=*/*,
// origin=http://localhost:57581, sec-fetch-site=same-site, sec-fetch-mode=cors, sec-fetch-dest=empty,
// referer=http://localhost:57581/, accept-encoding=gzip, deflate, br, accept-language=zh-CN,zh;q=0.9}
proxy(request, requestHeaders, response, url);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
编辑 (opens new window)
上次更新: 2023/05/04, 20:09:26