工作原理
1、路由 注意:相同的配置文件前缀,路由的时候只需要改predicates: 后面的值
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: http://localhost:7070/ # 目标uri,路由到微服务地址
predicates: # 断言,也就是路由条件
1.1:Path
1.1.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: http://localhost:7070/ # 目标uri,路由到微服务地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/product/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
1.1.2:测试
http://localhost:9000/product/1
1.2:Query
1.2.1:配置文件
在这里插入代码片
- Query=token # 匹配请求参数中包含token的请求,将匹配的请求追加在目标uri之后
- Query=token, abc. # 匹配请求参数中包含token的请求满足正则表达式abc.,将匹配的请求追加在目标uri之后
1.2.2:测试
错误:http://localhost:9000/product/1
正确:http://localhost:9000/product/1?token=123 #含有token参数的请求
http://localhost:9000/product/1?token=abcc #含有token参数正则表达式的请求
1.3:Method
1.3.1:配置文件
- Method=GET # 匹配请求get的请求方式,将匹配的请求追加在目标uri之后
1.3.2:测试
错误:http://localhost:9000/product/1 #在postman用post请求访问
1.4:Datetime
1.4.1:配置文件
- After=2020-11-20T06:06:06+08:00[Asia/Shanghai] #匹配在2020年11月20号的请求,将匹配的请求追加在目标uri之后
1.4.2:测试
正确:http://localhost:9000/product/1
1.5:RemoteAddr
1.5.1:配置文件
- RemoteAddr=192.168.25.62/0 #匹配远程地址请求,0表示子网掩码,将匹配的请求追加在目标uri之后
1.5.2:测试
错误:http://localhost:9000/product/1
正确:http://192.168.25.62:9000/product/1
1.5:Header
1.5.1:配置文件
- Header=X-Request-Id, \d+ #匹配请求头包含X-Request-Id,\d+为正则表示为任意数值
1.5.2:测试
正确:http://localhost:9000/product/1 #用postman添加请求头参数:X-Request-Id, 值:123123
1.6:动态路由
1.6.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/product/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
1.6.2:测试
正确:http://localhost:9000/product/1
1.7:微服务名称转发
1.7.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
#是否与服务发现组建进行结合,通过serviceId转发到具体服务实例
enabled: true #开启根据微服务名称自动转发
lower-case-service-id: true #微服务名称以小写形式呈现
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
1.7.2:测试
正确:http://localhost:9000/order-service/order/1 #order-service是微服务名称
http://localhost:9000/product-service/product/1 #product-service是微服务名称
2、过滤
2.1 Path过滤器
2.1.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/product/**, /api-gateway/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
#将/api-gateway/product/1 重写为/product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.1.2:测试
正确:http://localhost:9000/api-gateway/product/1
2.2 PrefixPath前缀过滤器
2.2.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
- PrefixPath=/product #添加前缀路径
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.2.2:测试
正确:http://localhost:9000/1
2.3 StripPrefix分割过滤器
2.3.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
#将 /api/123/product/1 重写为 /product/1
- StripPrefix=2 #分割前缀
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.3.2:测试
正确:http://localhost:9000/abc/123/product/1
2.4 SetPath片断过滤器
2.4.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/api/product/{segment} # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
#将 /api/123/product/1 重写为 /product/1
- SetPath=/product/{segment} #片断
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.4.2:测试
正确:http://localhost:9000/api/product/1
2.5 AddRequestParameter参数过滤器
2.5.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/api-gateway/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
#将 /api-gateway/product/1 重写为 /product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
#下游请求中添加参数 flag=1
- AddRequestParameter=flag, 1
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.5.2:测试
正确: 正确:http://localhost:9000/api-gateway/product/1
2.6 SetStatus状态过滤器
2.6.1:配置文件
server:
port: 9000 #端口
spring:
application:
name: gateway-server
cloud:
gateway:
# 配置路由规则,可以配置多个
routes:
- id: product-service # id 自定义路由的id
uri: lb://product-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/api-gateway/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
filters: #网关过滤器
#将 /api-gateway/product/1 重写为 /product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
- SetStatus=404 #设置响应状态404
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
2.6.2:测试
正确:http://localhost:9000/api-gateway/product/1 #报404状态
2.7自定义网关过滤器
2.7.1:filter类
package com.yhd.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
public class CustomGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义过滤器开始了");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
@Override
public ShortcutType shortcutType() {
return null;
}
@Override
public List<String> shortcutFieldOrder() {
return null;
}
@Override
public String shortcutFieldPrefix() {
return null;
}
}
2.7.2:配置类
package com.yhd.config;
import com.yhd.filter.CustomGatewayFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayRoutesConfiguration {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route((r) ->
r.path("/product/**")
.uri("lb://product-service")
//注册自定义网关过滤器
.filters(new CustomGatewayFilter())
//路由ID
.id("product-service"))
.build();
}
}
2.7.2:测试
正确:http://localhost:9000/product/1
2.8自定义全局过滤器
2.8.1:filter类
package com.yhd.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("开始全局过滤");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
2.8.2:测试
正确:http://localhost:9000/product/1
结果: 开始全局过滤
自定义过滤器开始了
2.9统一权限过滤器
2.9.1:filter类
package com.yhd.filter;
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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AccessFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String message = "没有权限: "+HttpStatus.UNAUTHORIZED;
DataBuffer wrap = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(wrap));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
2.9.2:测试
错误:http://localhost:9000/product/1 #报401无权限的错误
正确:http://localhost:9000/product/1?token=123
3、网关限流
3.1常见算法:计数器算法,漏桶算法,令牌桶算法
3.2限流
3.2.1依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
3.2.2配置文件
server:
port: 9001 #端口
spring:
application:
name: gateway-server-sentinel
cloud:
sentinel:
filter:
enabled: false #将网关的资源不展示在sentinel上控制
gateway:
discovery:
locator:
# 是否与服务发现组建进行组合,通过serviceID转发到具体服务实例
enabled: true #是否开启基于服务发现的路由规则
lower-case-service-id: true #是否将服务名称转为小写
# 配置路由规则,可以配置多个
routes:
- id: order-service # id 自定义路由的id
uri: lb://order-service # lb:// 根据负载均衡去注册中心获取微服务请求地址
predicates: # 断言
- Path=/order/** # 匹配对应的url的请求,将匹配的请求追加在目标uri之后
#配置Eureka注册中心
eureka:
instance:
prefer-ip-address: true #是否使用ip地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
client:
service-url: #s设置注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
3.2.3配置类
package com.yhd.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit() {
initGatewayRules();
initBlockHandler();
}
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("order-service")
.setCount(3)
.setIntervalSec(60)
);
GatewayRuleManager.loadRules(rules);
}
private void initBlockHandler() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange,
Throwable throwable) {
Map<String, String> result = new HashMap<String, String>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
});
}
}
标题
http://localhost:9001/product/1
3.3分组限流
3.3.1配置类
package com.yhd.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit() {
initGatewayRules();
initBlockHandler();
}
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new
GatewayFlowRule("product_api")
.setCount(3)
.setIntervalSec(60));
rules.add(new
GatewayFlowRule("order_api")
.setCount(5)
.setIntervalSec(60));
GatewayRuleManager.loadRules(rules);
initCustomizedApis();
}
private void initBlockHandler() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange,
Throwable throwable) {
Map<String, String> result = new HashMap<String, String>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
});
}
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-service/order/index 完成的url路径匹配
add(new ApiPathPredicateItem().setPattern("/order-service/order/index"));
}});
definitions.add(api1);
definitions.add(api2);
//加载限流分组
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
3.3.2测试
http://localhost:9001/product-service/product/1
http://localhost:9001/order-service/order/index
4、高可用网关集群,用Nginx做