SpringCloud学习五-GateWay网关

14 篇文章 0 订阅
5 篇文章 0 订阅

1.GateWay是什么?

GateWay 是SpringCloud 生态系统中的网关,目标是替代Zuul,同样提供了限流,监控,路由转发、权限校验等功能。

相关名词:

  • Route(路由):这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
  • Predicate(断言):这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。
  • Filter(过滤器):这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它修改请求和响应。

工作流程:
GateWay.png

客户端向 Spring Cloud Gateway 发出请求,如果HandlerMapping中找到了请求相匹配的路由,将其发送到Web Handler。Handler再通过指定过滤器链将请求发送到实际服务之星业务逻辑,然后返回。虚线是过滤器可能会在发送代理请求之前pre活之后post执行业务逻辑。

2.创建工程

2.1依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

注意:springcloud gateway使用的web框架为webflux,和springMVC不兼容。

2.2代码

@SpringBootApplication
public class GateWayApplication {
	public static void main(String[] args) {
		SpringApplication.run(GateWayApplication.class, args);
	}
	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				.route("test_route", r -> r.path("/test")
						.uri("http://baidu.com"))
				.build();
	}
}

路由有两种配置方式,一种是像上面这样配置一个id名为test_route的路由,当访问项目地址/test的时候,会自动转发到baidu.com。
另一种是在配置文件中配置,下面介绍。

2.3配置文件

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: http://www.baidu.com
        predicates:
        - Path=/foo/**
        filters:
            - StripPrefix=1

字段含义如下:

  • id: 自定有路由id,唯一
  • uri: 跳转的目标地址
  • predicates: 路由条件
  • filters: 过滤规则

上面配置的意思是配置了一个id为test_route的路由规则,当访问项目地址/foo/hello的时候自动转发到地址baidu.com。如果上面例子中没有加一个StripPrefix=1过滤器,则目标uri为http://localhost:8000/foo/bar,StripPrefix过滤器是去掉一个路径。
routes配置和代码配置保留一处即可。

3.路由规则详解

Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。

spring-cloud-gateway3.png

3.1时间匹配

spring:
  cloud:
    gateway:
      routes:
       - id: time_route
        uri: http://ityouknow.com
        predicates:
         - After=2018-01-20T06:06:06+08:00[Asia/Shanghai]

Spring 是通过 ZonedDateTime 来对时间进行的对比,ZonedDateTime 是 Java 8
中日期时间功能里,用于表示带时区的日期与时间信息的类,ZonedDateTime
支持通过时区来设置时间,中国的时区是:Asia/Shanghai。 After Route Predicate
是指在这个时间之后的请求都转发到目标地址。上面的示例是指,请求时间在
2018年1月20日6点6分6秒之后的所有请求都转发到地址http://ityouknow.com。+08:00是指时间和UTC时间相差八个小时,时间地区为Asia/Shanghai。

3.2 Cookie匹配

Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

spring:
  cloud:
    gateway:
      routes:
       - id: cookie_route
         uri: http://ityouknow.com
         predicates:
         - Cookie=ityouknow, kee.e

3.3 通过请求路径匹配(常用)

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: http://baidu.com
        predicates:
        - Path=/foo/{segment}

如果请求路径符合要求,则此路由将匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 测试,命令行输入:
curl http://localhost:8080/foo/1

3.4通过请求参数匹配

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: http://ityouknow.com
        predicates:
        - Query=smile

这样配置,只要请求中包含 smile 属性的参数即可匹配路由。
使用 curl 测试,命令行输入:
curl localhost:8080?smile=x&id=2

4.利用过滤器修改接口的返回报文(后置过滤)

各个子服务返回的报文各异,需要在网关对返回报文进行包装统一返回格式。

@Component
@Slf4j
public class ResponseFilter  implements GlobalFilter, Ordered {
    //白名单
    @Value("${filter.url.white.list.rsp}")
    private String[] skipAuthUrls ;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        String url = exchange.getRequest().getURI().getPath();
        //跳过不需要验证的路径
        if(Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        // probably should reuse buffers
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        // 释放掉内存
                        DataBufferUtils.release(dataBuffer);
                        String rs = new String(content, Charset.forName("UTF-8"));
                        //默认失败
                        CommonResult commonResult = CommonResult.failed();
                        try {
                            if(StringUtils.isNotBlank(rs)){
                                JsonResult jsonResult= JSONObject.parseObject(rs,JsonResult.class);
                                if(null != jsonResult ){
                                    if(0 == jsonResult.getCode()){
                                        commonResult = CommonResult.success(jsonResult.getData(),jsonResult.getMessage());
                                    }else{
                                        commonResult = CommonResult.failed(jsonResult.getMessage());
                                    }
                                }
                            }
                        }catch (Exception e){
                            log.error("转换异常,异常报文:{}",rs);
                            log.error(e.getMessage(),e);
                        }
                        byte[] newRs = JSON.toJSONString(commonResult).getBytes(Charset.forName("UTF-8"));
                        originalResponse.getHeaders().setContentLength(newRs.length);//如果不重新设置长度则收不到消息。
                        return bufferFactory.wrap(newRs);
                    }));
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    /**
     *
     * 功能描述: 执行优先级
     * 此处order需要小于-1,需要先于NettyWriteResponseFilter过滤器执行
     * @param:
     * @return:
     * @auther: lfc
     * @date: 2019/8/25 18:56
     */
    @Override
    public int getOrder() {
        return -99;
    }

需要注意的是order需要小于-1,需要先于NettyWriteResponseFilter过滤器执行。

5.过滤器拦截权限认证(前置过滤)

@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {

    @Value("${filter.url.white.list.req}")
    private String[] skipAuthUrls;

    private String jwtBlacklistKeyFormat;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        //跳过不需要验证的路径
        if(Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }
        //从请求头中取出token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        //未携带token或token在黑名单内
        if (token == null ||
                token.isEmpty() ||
                isBlackToken(token)) {
            if(log.isDebugEnabled() && isBlackToken(token)){
                log.debug("**********此token已加入黑名单**********");
            }
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.unauthorized(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //取出token包含的身份
        String data = JWTUtil.getData(token, Constant.JWT_TYPE);
        if(data.isEmpty()){
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.forbidden(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        UserDTO ud = JSONObject.parseObject(data,UserDTO.class);
        // 1.解析判断是否被修改 2.根据token 获取redis中的数据以此判断是否超时或有效
        if(!JWTUtil.verify(token) || StringUtils.isBlank(JedisUtil.getJson(ud.getJti()))){
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = JSONObject.toJSONString(CommonResult.tamperToken(null)).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //redis 更新过期时间
        JedisUtil.setJson(ud.getJti(),token,Constant.EXRP_HOUR);
        data = JWTUtil.sign(JSONObject.toJSONString(ud));

        //将现在的request,添加当前身份
        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization", data).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();

        return chain.filter(mutableExchange);
    }
    @Override
    public int getOrder() {
        return -100;
    }
    
    /**
     *
     * 功能描述: 判断token是否在黑名单内
     * @param token
     * @return
     * @auther: lfc
     * @date: 2019/9/9 0:34
     */
    private boolean isBlackToken(String token){
//        assert token != null;
//        return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
        return false;
    }

GateWay中区分前置过滤还是后置过滤取决于动作在chain.filter方法前还是之后,之后回调的是后置,之前调用的是前置。

参考资源:纯洁微笑
方志鹏-深入理解Spring Cloud与微服务构建

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值