1.创建springBoot项目
可以将Server URL换成start.aliyun.com
2.配置路由与跨域处理
路由:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
跨域处理:
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
3.自定义过滤器
package cn.itcast.gateway.filters;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
过滤器的执行顺序:
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
排序的规则是什么呢?
每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
4.实现定义过滤器中的业务逻辑
//首先配置一个全局异常处理器来处理异常
梳理业务逻辑
@Order(-1)
@Component
@Slf4j
public class AynuFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
//3.黑白名单
//4.用户鉴权
//5.请求的模拟接口是否存在
//6.请求转发,调用模拟接口
//7.响应日志
//8.调用成功,接口调用次数+1
//9.调用失败,返回一个规范的错误码
return exchange.getResponse().setComplete();
}
}
配置请求日志
exchange(路由交换机):我们所有的请求的信息、响应的信息、响应体、请求体都能从这里拿到。
chain(责任链模式):因为我们的所有过滤器是按照从上到下的顺序依次执行,形成了一个链条。所以这里用了一个 chain ,如果当前过滤器对请求进行了过滤后发现可以放行,就要调用责任链中的 next 方法,相当于直接找到下一个过滤器,这里称为 filter 。有时候我们需要在责任链中使用 next,而在这里它使用了filter 来找到下一个过滤器,从而正常地放行请求。
@Order(-1)
@Component
@Slf4j
public class AynuFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
//3.黑白名单
//4.用户鉴权
//5.请求的模拟接口是否存在
//6.请求转发,调用模拟接口
//7.响应日志
//8.调用成功,接口调用次数+1
//9.调用失败,返回一个规范的错误码
return chain.filter(exchange);
}
}
使用exchange获取request,并输出日志
@Order(-1)
@Component
@Slf4j
public class AynuFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:{}",request.getId());
log.info("请求路径:{}",request.getPath().value());
log.info("请求方法:{}",request.getMethod());
log.info("请求参数:{}",request.getQueryParams());
log.info("请求来源地址:{}",request.getRemoteAddress());
String hostString = request.getLocalAddress().getHostName();
log.info("请求来源地址:{}",hostString);
//3.黑白名单
//4.用户鉴权
//5.请求的模拟接口是否存在
//6.请求转发,调用模拟接口
//7.响应日志
//8.调用成功,接口调用次数+1
//9.调用失败,返回一个规范的错误码
return chain.filter(exchange);
}
}
配置白名单
通常情况下,G经常使用的是封禁IP。例如,如果某个远程地址频繁访问,我们可以将其添加到黑名单并拒绝访问。现在我们来试试设置一个规则,如果请求的来源地址不是 127.0.0.1,就拒绝它的访问。先写一个全局的常量。在这里我们用一个白名单,通常建议在权限管理中尽量使用白名单,少用黑名单。白名单的原则是只允许特定的调用,这样可能会更加安全,或者你可以默认情况下全禁止。
@Order(-1)
@Component
@Slf4j
public class AynuFilter implements GlobalFilter {
private static final List<String> IP_WHITE_LIST = Arrays.asList("127.0.0.1");
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:{}",request.getId());
log.info("请求路径:{}",request.getPath().value());
log.info("请求方法:{}",request.getMethod());
log.info("请求参数:{}",request.getQueryParams());
log.info("请求来源地址:{}",request.getRemoteAddress());
String hostString = request.getLocalAddress().getHostName();
log.info("请求来源地址:{}",hostString);
//3.黑白名单
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
if (!IP_WHITE_LIST.contains(hostString)){
//设置响应状态码为403(禁止访问)
response.setStatusCode(HttpStatus.FORBIDDEN);
}
//4.用户鉴权
//5.请求的模拟接口是否存在
//6.请求转发,调用模拟接口
//7.响应日志
//8.调用成功,接口调用次数+1
//9.调用失败,返回一个规范的错误码
return chain.filter(exchange);
}
}
用户鉴权
这里举个例子,具体实现请参考自己的业务需求
@Order(-1)
@Component
@Slf4j
public class AynuFilter implements GlobalFilter {
private static final List<String> IP_WHITE_LIST = Arrays.asList("127.0.0.1");
@Autowired
RedisTemplate redisTemplate;
@Autowired
UserMapper userMapper;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:{}",request.getId());
log.info("请求路径:{}",request.getPath().value());
log.info("请求方法:{}",request.getMethod());
log.info("请求参数:{}",request.getQueryParams());
log.info("请求来源地址:{}",request.getRemoteAddress());
String hostString = request.getLocalAddress().getHostString();
log.info("请求来源地址:{}",hostString);
//3.黑白名单
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
if (!IP_WHITE_LIST.contains(hostString)){
//设置响应状态码为403(禁止访问)
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
//4.用户鉴权
HttpHeaders headers = request.getHeaders();
//调用者传过来的参数
String accessKey = headers.getFirst("accessKey");
String body = headers.getFirst("body");
String timestamp = headers.getFirst("timestamp");
String random = headers.getFirst("random");
String sign = headers.getFirst("sign");
//验证随机数,使用redis存储,以sign为键名
//操作字符串数据对象
ValueOperations valueOperations = redisTemplate.opsForValue();
//get
String randomDB = (String) valueOperations.get(sign);
if (randomDB==null){
//setex TimeUnit是一个枚举类,里面列举了时间单位
valueOperations.set(sign,random,120, TimeUnit.HOURS);
}else {
if (!randomDB.equals(random)){
throw new RuntimeException("无权限");
}
}
//调用mapper校验key
User userDB = userMapper.getUserByAccessKey(accessKey);
if (userDB==null){
throw new RuntimeException("无权限");
}
//时间戳验证,时间戳不能和当前时间超过5分钟
if (TimeUtils.checkTimesTamp(timestamp)){
throw new RuntimeException("无权限");
}
//秘钥验证,使用传过来的数据生成sign,查询与用户的sign是否一致
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey",accessKey);
hashMap.put("body",body);
hashMap.put("random", random);
hashMap.put("timestamp",timestamp);
String newSign = SignUtils.getSign(hashMap, userDB.getSecretKey());
if (!newSign.equals(sign)){
throw new RuntimeException("无权限");
}
//5.请求的模拟接口是否存在
//todo
//6.请求转发,调用模拟接口
//7.响应日志
//8.调用成功,接口调用次数+1
//9.调用失败,返回一个规范的错误码
return chain.filter(exchange);
}
}
自定义响应处理
问题:
预期是等模拟接口调用完成,才记录响应日志、统计调用次数。
但现实是 chain.filter 方法立刻返回了,直到 filter 过滤器全部 return 后才调用了模拟接口。
原因是:chain.filter 是个异步操作。
解决方案:利用 response 装饰者,增强原有response 的处理能力
@Component
@Slf4j
public class AynuFilter implements GlobalFilter ,Ordered{
private static final List<String> IP_WHITE_LIST = Arrays.asList("127.0.0.1");
@Autowired
RedisTemplate redisTemplate;
@Autowired
UserMapper userMapper;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.用户发送请求到api网关
log.info("进入网关过滤器");
//2.配置请求日志
ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:{}",request.getId());
log.info("请求路径:{}",request.getPath().value());
log.info("请求方法:{}",request.getMethod());
log.info("请求参数:{}",request.getQueryParams());
log.info("请求来源地址:{}",request.getRemoteAddress());
String hostString = request.getLocalAddress().getHostString();
log.info("请求来源地址:{}",hostString);
//3.黑白名单
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
if (!IP_WHITE_LIST.contains(hostString)){
//设置响应状态码为403(禁止访问)
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
//4.用户鉴权
HttpHeaders headers = request.getHeaders();
//调用者传过来的参数
String accessKey = headers.getFirst("accessKey");
String body = headers.getFirst("body");
String timestamp = headers.getFirst("timestamp");
String random = headers.getFirst("random");
String sign = headers.getFirst("sign");
//验证随机数,使用redis存储,以sign为键名
//操作字符串数据对象
ValueOperations valueOperations = redisTemplate.opsForValue();
//get
String randomDB = (String) valueOperations.get(sign);
if (randomDB==null){
//setex TimeUnit是一个枚举类,里面列举了时间单位
valueOperations.set(sign,random,120, TimeUnit.HOURS);
}else {
if (!randomDB.equals(random)){
throw new RuntimeException("无权限");
}
}
//调用mapper校验key
User userDB = userMapper.getUserByAccessKey(accessKey);
if (userDB==null){
throw new RuntimeException("无权限");
}
//时间戳验证,时间戳不能和当前时间超过5分钟
if (TimeUtils.checkTimesTamp(timestamp)){
throw new RuntimeException("无权限");
}
//秘钥验证,使用传过来的数据生成sign,查询与用户的sign是否一致
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey",accessKey);
hashMap.put("body",body);
hashMap.put("random", random);
hashMap.put("timestamp",timestamp);
String newSign = SignUtils.getSign(hashMap, userDB.getSecretKey());
if (!newSign.equals(sign)){
throw new RuntimeException("无权限");
}
//5.请求的模拟接口是否存在
//todo
return handleResponse(exchange,chain);
}
public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
HttpStatus statusCode = originalResponse.getStatusCode();
if (statusCode != HttpStatus.OK) {
return chain.filter(exchange);//降级处理返回数据
}
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
//添加调用接口后的处理逻辑
//8.调用成功,接口调用次数+1
// 合并多个流集合,解决返回体分段传输
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer buff = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[buff.readableByteCount()];
buff.read(content);
DataBufferUtils.release(buff);//释放掉内存
//构建日志
StringBuilder stringBuilder = new StringBuilder(200);
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(originalResponse.getStatusCode());
String s = new String(content, StandardCharsets.UTF_8);
stringBuilder.append(s);
log.info("响应结果:{}", arrayList.toArray());
return bufferFactory.wrap(content);
}));
} else {
log.error("<-- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
} catch (Exception e) {
log.error("gateway log exception.\n" + e);
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}