一, 原始需求
萌新小明,入职某网络科技公司,职位互联网开发工程狮.
最近接到一个需求,与某第三方系统对接. 对方提供了接口文档. 小明已经按照接口文档开发好了代码.
现在小明想通过httpbin来测试发送的请求是否正确,他该怎么做?
二, 需求梳理
1. 接口信息
接口地址 | 请求方式 | 参数 |
---|---|---|
https://test.00fly.online/get/method1 | get | param1、param2 |
https://test.00fly.online/post/method1 | post | param1、param2、file |
https://test.00fly.online/json/method1 | jsonbody post | param1、param2 |
2. httpbin信息
服务地址: https://http.00fly.online
3. 流程梳理
观察流程图,发现接口作为httpbin服务的反向代理方来对外提供服务.这样访问接口的时候,便能看到提交的详细请求信息。
一般情况下,实现反向代理常见有2种方案:
- 微服务的网关技术
- nginx
三, 网关实现
1. 准备工作
test.00fly.online域名ssl证书申请和本地host配置先行,具体请移步 玩转WEB接口之三 【HTTPS证书申请】
2. 源码传送
项目使用Spingcloud gateway 技术来实现
代码结构如下:
如何使用下面的备份文件恢复成原始的项目代码,请移步查阅:神奇代码恢复工具
注意:需要手工放置test.00fly.online.pfx
到 \src\main\resources\data\ssl
//goto pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.Springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath />
</parent>
<groupId>***.fly</groupId>
<artifactId>springcloud-gateway</artifactId>
<version>0.0.1</version>
<name>springcloud-gateway</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.apache.***mons</groupId>
<artifactId>***mons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
//goto src\main\java\***\fly\gateway\config\A***essLogGlobalFilter.java
package ***.fly.gateway.config;
import java.***.I***SocketAddress;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
/**
* 全局过滤器
*/
@Slf4j
@***ponent
@Order(value = Integer.MIN_VALUE)
public class A***essLogGlobalFilter implements GlobalFilter
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
// filter的前置处理
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
I***SocketAddress remoteAddress = request.getRemoteAddress();
return chain.filter(exchange) // filter的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode();
log.info("#### 请求路径:{}, 远程IP地址:{}, 响应码:{}", path, remoteAddress, statusCode);
}));
}
}
//goto src\main\java\***\fly\gateway\config\AuthorizeGatewayFilterFactory.java
package ***.fly.gateway.config;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.***ponent;
import org.springframework.util.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 局部过滤器 名称必须是xxxGatewayFilterFactory形式
*/
@Slf4j
@***ponent
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config>
{
private static final String AUTHORIZE_TOKEN = "token";
// 构造函数,加载Config
public AuthorizeGatewayFilterFactory()
{
// 固定写法
super(AuthorizeGatewayFilterFactory.Config.class);
log.info("--------Loaded GatewayFilterFactory [Authorize]");
}
// 读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder()
{
// Config.enabled
return Arrays.asList("enabled");
}
@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config)
{
return (exchange, chain) -> {
// 判断是否开启授权验证
if (!config.isEnabled())
{
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
// 从请求头中获取token
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null)
{
// 从请求头参数中获取token
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
ServerHttpResponse response = exchange.getResponse();
// 如果token为空,直接返回401,未授权
if (StringUtils.isEmpty(token))
{
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 处理完成,直接拦截,不再进行下去
return response.set***plete();
}
/**
* todo chain.filter(exchange) 之前的都是过滤器的前置处理
*
* chain.filter().then( 过滤器的后置处理........... )
*/
// 授权正常,继续下一个过滤器链的调用
return chain.filter(exchange);
};
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config
{
// 控制是否开启认证
private boolean enabled;
}
}
//goto src\main\java\***\fly\gateway\config\ResolverConfig.java
package ***.fly.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
/**
* 限流解析器器
*/
@Configuration
public class ResolverConfig
{
@Bean
@Primary
KeyResolver apiKeyResolver()
{
// 按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
@Bean
KeyResolver userKeyResolver()
{
// 按用户限流
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
@Bean
KeyResolver ipKeyResolver()
{
// 按IP来限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
//goto src\main\java\***\fly\gateway\web\IndexController.java
package ***.fly.gateway.web;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController
{
@RequestMapping("/show/test1")
public Object test1()
{
return "i am test1";
}
@RequestMapping("/show2/test2")
public Object test2()
{
return "i am test2";
}
}
//goto src\main\java\***\fly\GatewayApplication.java
package ***.fly;
import org.apache.***mons.lang3.StringUtils;
import org.apache.***mons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.***mandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.core.env.Environment;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication implements ***mandLineRunner
{
@Value("${server.port}")
Integer port;
@Autowired
Environment environment;
public static void main(String[] args)
{
SpringApplication.run(GatewayApplication.class, args);
}
@Override
public void run(String... args)
throws Exception
{
if (SystemUtils.IS_OS_WINDOWS && port > 0)
{
log.info("★★★★★★★★ now open Browser ★★★★★★★★ ");
log.info("请修改hosts: 127.0.0.1 test.00fly.online");
String url = "https://test.00fly.online:" + port;
String profile = StringUtils.join(environment.getActiveProfiles());
switch (profile)
{
case "httpbin":
Runtime.getRuntime().exec("cmd /c start " + url + "/user-agent");
Runtime.getRuntime().exec("cmd /c start " + url + "/headers");
Runtime.getRuntime().exec("cmd /c start " + url + "/get");
Runtime.getRuntime().exec("cmd /c start " + url);
break;
case "demo":
Runtime.getRuntime().exec("cmd /c start " + url + "/get/method1");
break;
default:
Runtime.getRuntime().exec("cmd /c start " + url + "/gateway/show/test1");
Runtime.getRuntime().exec("cmd /c start " + url + "/show2/test2");
break;
}
}
}
}
//goto src\main\resources\application-demo.yml
spring:
cloud:
gateway:
routes:
# 路由:method=get访问https://http.00fly.online/get
- id: get-route
uri: https://http.00fly.online
predicates:
- Method=GET
filters:
- SetPath=/get
- RewritePath=/(?<segment>/?.*), /get
# 路由:method=post访问https://http.00fly.online/post
- id: post-route
uri: https://http.00fly.online/post
predicates:
- Method=POST
filters:
- SetPath=/post
- RewritePath=/(?<segment>/?.*), /post
//goto src\main\resources\application-dev.yml
spring:
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: route-1
# 请求最终被转发到的目标地址
uri: https://test.00fly.online:443
# 设置断言
predicates:
- Path=/gateway/show/{segment}
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway
- StripPrefix=1
#局部过滤器
- AddResponseHeader=X-Response-Foo, Bar
## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
- Authorize=false
//goto src\main\resources\application-httpbin.yml
spring:
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一
- id: host_route
# 请求最终被转发到的目标地址
uri: https://http.00fly.online
# 设置断言
predicates:
- Host=test.00fly.online
//goto src\main\resources\application-test.yml
spring:
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: path-route
# 请求最终被转发到的目标地址
uri: https://www.baidu.***
# 设置断言
predicates:
- Path=/bd/**
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/bd
- StripPrefix=1
//goto src\main\resources\application.yml
server:
port: 443
ssl:
key-store: classpath:data/ssl/test.00fly.online.pfx
key-store-password: 12345asdfg
keyStoreType: PKCS12
servlet:
context-path: /
session:
timeout: 1800
spring:
application:
name: gateway
profiles:
active:
- demo
https://gitee.***/00fly/effict-side/tree/master/springcloud-gateway
3. 代码运行
直接运行 GatewayApplication
或者打包运行
mvn clean package
#直接运行
cd target
java -jar springcloud-gateway-0.0.1.jar
启动成功后系统自动打开浏览器访问: https://test.00fly.online/get/method1
返回json数据如下:
{
"args": {},
"headers": {
"A***ept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"A***ept-Encoding": "gzip, deflate, br",
"A***ept-Language": "zh-***,zh;q=0.9,en;q=0.8",
"Cache-Control": "max-age=0",
"Connection": "close",
"Content-Length": "0",
"Forwarded": "proto=https;host=test.00fly.online;for=\"127.0.0.1:55669\"",
"Host": "http.00fly.online",
"Sec-Ch-Ua": "\"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"121\", \"Chromium\";v=\"121\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "\"Windows\"",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"X-Forwarded-Host": "test.00fly.online"
},
"origin": "127.0.0.1, 61.169.67.18",
"url": "https://test.00fly.online/get"
}
4. PostMan测试
https://test.00fly.online/post/method1 测试结果
https://test.00fly.online/json/method1 测试结果
四, nginx实现(待调试)
nginx如何实现下面的功能?
get请求统一转发到https://http.00fly.online/get
post请求统一转发到https://http.00fly.online/post
目前还没找到具体的实现方法,哪位大佬有这方面的经验或想法,欢迎交流,不胜感激!
未完待续,有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-