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 # 是否允许携带cookiemaxAge: 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 {@Overridepublic 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");@AutowiredRedisTemplate redisTemplate;@AutowiredUserMapper 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();//getString 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校验keyUser 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");@AutowiredRedisTemplate redisTemplate;@AutowiredUserMapper 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();//getString 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校验keyUser 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.请求的模拟接口是否存在//todoreturn 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) {@Overridepublic 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);}}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}}