【GateWay】 微服务网关

目录

一、微服务网关介绍

1.1 存在的问题

1.2 微服务网关

1.2.1 概述

1.2.2 优点

1.2.3 常用技术

1.2.4 实际框架

二、微服务网关的搭建

2.1 创建工程

2.2 跨域和过滤配置

2.2.1 跨域路由配置

2.2.2 Host配置

2.2.3 Path 路径过滤

2.2.4 去掉请求前缀

2.2.5 增加前缀

2.2.6 负载均衡配置

2.3 网关限流

2.3.1 限流介绍

2.3.2 作用

2.3.3 使用

三、微服务网关结合 JWT 实现用户登录校验

3.1 整体框架流程

3.2 使用

3.2.1 加入工具类 JwtUtil.java

3.2.2 增加 login 接口

3.2.3 修改网关服务代码


一、微服务网关介绍

1.1 存在的问题

在分布式项目中,不同的微服务有不同的网络地址,而外部的客户端完成一个业务需求时,可能涉及到多个微服务接口,若让客户端直接与多个微服务通信,会带来以下问题:

  • 业务复杂,客户端会多次请求不同的微服务,增加了业务处理时间和业务复杂度
  • 跨域问题
  • 认证复杂,各个微服务都需要先对用户的请求进行身份认证
  • 安全问题,直接将各个微服务模块都暴露给客户端
  • 访问困难,根据实际情况,部分微服务设置了防火墙等方式,无法直接访问
  • 重构复杂,随着项目咖啡啊,若项目需要重新划分微服务,如将多个微服务合并成一个微服务,或是拆分微服务,若是由客户端直接与微服务通信,则会导致工作复杂度增加

 

1.2 微服务网关

1.2.1 概述

使用微服务网关即能解决上面的问题。

网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过网关,这就可以形成这样的功能架构:

  • 网关:处理安全、性能、监控方面的业务
  • 业务微服务:更专注于业务逻辑的实现

整体架构图如下:

       

1.2.2 优点

  • 降低了业务复杂度,让客户端只需发起一次请求到网关,由网关根据业务需求请求各个微服务处理,并将结果返回给客户端。
  • 可以统一解决跨域问题
  • 能够在网关服务中统一处理认证、日志、监控、限流
  • 提高了安全性,由统一的访问入口,降低了业务服务的暴露风险和服务器受攻击面积
  • 便于重构,客户端只负责向网关请求,实际的后端业务处理由后端自己完成,即若修改了后端业务逻辑,无需升级客户端,降低了前后端的耦合性。

 

1.2.3 常用技术

常见的实现微服务网关的技术有:

  • nginx:高性能的HTTP和方向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
  • zuul:由 Netflix 出品的基于JVM路由和服务器的负载均衡器
  • spring-cloud-gateway:由 spring 出品的基于spring 的网关项目,集成断路器、路径重写等

由于spring-cloud-gateway 的性能优于 zuul,且无缝兼容 springboot 等框架的项目,故市场大部分使用 spring-cloud-gateway

 

1.2.4 实际框架

在实际项目使用中,微服务网关通常负责路由功能,用于整合各大微服务,而在网关之前一般使用nginx等并发能力强的服务抵御第一波用户请求的冲击。

同时,由于微服务网关担任请求分发的功能,一旦网关挂了,用户就无法正常使用了。故微服务网关必须集群,且可以根据不同的系统搭建不同的微服务网关。

     

根据实际情况可以有多种微服务,如:

      

 

二、微服务网关的搭建

2.1 创建工程

工程创建... 忽略

加入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

创建配置文件

官方手册:https://cloud.spring.io/spring-cloud-gateway/spring-cloud-gateway.html#_stripprefix_gatewayfilter_factory

server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
spring:
  application:
    name: gateway-web
  redis:
    host: 192.168.47.142
    port: 6379
  cloud:
    gateway:
      globalcors:
        # 跨域请求配置
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
           # 唯一标识符
           - id: changgou_goods-route
             # 用户请求需要路由到该服务[指定要路由的服务]
#             uri: http://localhost:18081
             uri: lb://goods
             # 路由断言,路由规则配置
             # lb 使用LoadBalancerClient 实现负载均衡,后面的goods是微服务的名称
             predicates:
             # 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
#             - Host=cloud.tom.com**
#             - Path=/api/brand/**
             - Path=/**
             filters:
               # 将请求路径中的第一个路径去掉,请求路径以/区分
               - StripPrefix=1
#                - PrefixPath=/brand
               - name: RequestRateLimiter #局部显示流过滤,名字不能随便写
                 args:
                   key-resolver: "#{@ipKeyResolver}" # 用户身份唯一识别标识符
                   redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
                   redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

 

2.2 跨域和过滤配置

2.2.1 跨域路由配置

由于网关是给所有用户访问的,故没有nginx代理的情况下需要做跨域路由配置

     globalcors:
        # 跨域请求配置
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE

 

2.2.2 Host配置

使用 host 配置可以让外部访问到网关

主要配置内容为:

      # HOST 配置
      routes:
           # 唯一标识符
           - id: changgou_goods-route
             # 用户请求需要路由到该服务[指定要路由的服务]
             uri: http://localhost:18081
             # 路由断言,路由规则配置
             predicates:
             # 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
             - Host=cloud.tom.com**

由于是在本机上测试,需要修改本地host

# 打开 hosts 文件
C:\Windows\System32\drivers\etc

# 添加内容
127.0.0.1 cloud.tom.com

  

测试:

http://cloud.tom.com:8001/brand

 

2.2.3 Path 路径过滤

Path 路径过滤与 host 配置不能共用

routes:
           # 唯一标识符
           - id: changgou_goods-route
             # 用户请求需要路由到该服务[指定要路由的服务]
             uri: http://localhost:18081
             # 路由断言,路由规则配置
             predicates:
             # 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
#             - Host=cloud.tom.com**
             - Path=/brand/**

测试:

http://localhost:8001/brand

 

2.2.4 去掉请求前缀

通过 StripPrefix 可以指定去掉第几个前缀路径

        routes:
           # 唯一标识符
           - id: changgou_goods-route
             # 用户请求需要路由到该服务[指定要路由的服务]
             uri: http://localhost:18081
             # 路由断言,路由规则配置
             predicates:
             # 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
             - Path=/api/brand/**
             filters:
               # 将请求路径中的第一个路径去掉,请求路径以/区分
               - StripPrefix=1

测试:

http://localhost:8001/api/brand

  

2.2.5 增加前缀

主动给所有的请求增加前缀

        routes:
           # 唯一标识符
           - id: changgou_goods-route
             # 用户请求需要路由到该服务[指定要路由的服务]
             uri: http://localhost:18081
             # 路由断言,路由规则配置
             predicates:
             # 用户请求的域名规则配置,所有以 cloud.tom.com 的请求都会被路由到上面uri所指的为止去
             - Path=/**
             filters:
               # 将请求路径中的第一个路径去掉,请求路径以/区分
                - PrefixPath=/brand

测试:

http://localhost:8001/

 

2.2.6 负载均衡配置

在并发量较大的时候,我们需要根据服务的名称判断来做负载均衡,可以使用 LoadBalancerClientFilter 实现负载均衡调用。

LoadBalancerClientFilter 会作用在 url 中以 lb 开头的路由,利用 loadBalancer 来获取服务实例,构造目标的 requestUrl,并设置到 GATEWAY_REQUEST_URL_ATTR 属性中供 NettyRoutingFilter 使用

修改uri

 

2.3 网关限流

2.3.1 限流介绍

Nginx 限流:

由于 Nginx 的访问中带有大量的静态资源访问和各大微服务的访问,故经过 Nginx 限流后请求数目可能依旧很大。故这时候可以通过网关限流的方式处理。内部实现的算法为漏桶算法。

网关限流:

网关限流可以对各个微服务设定不同的限流速率,防止由于大量并发导致服务崩溃。其内部是使用了 令牌桶算法 实现的。常见的实现令牌桶算法的技术有 Guaua、Redis。

模拟场景:

攻击者通过某个接口一直访问文件上传微服务,若 Nginx 的限流策略为 100/r,则文件上传服务可能同时面对 50/s 个并发请求。这时候可能导致文件服务难以支撑。这就要通过网关限流的方式,防止微服务负载过高。且通过约定好的识别方式,直接拦截住恶意请求。

 

2.3.2 作用

保护业务微服务,防止雪崩效应。

 

2.3.3 使用

引入依赖

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

 

在网关的项目启动类增加方法:

/**
 * @File: GatewayWebApplication
 * @Description:
 * @Author: tom
 * @Create: 2020-06-10 11:36
 **/
@SpringBootApplication
@EnableEurekaClient
@Slf4j
public class GatewayWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayWebApplication.class);
    }

    /**
     * 创建用户唯一标识,使用 IP 作为用户的唯一标识,根据 IP 进行限流操作
     * @return
     */
    @Bean("ipKeyResolver")
    public KeyResolver userKeyResolver() {
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
//                return Mono.just("需要使用的用户身份识别唯一标识");
                String ip = exchange.getRequest().getRemoteAddress().getHostName();
                log.info("用户请求的IP为:" + ip);
                return Mono.just(ip);
            }
        };
    }
}

 

修改配置文件:

spring:
  application:
    name: gateway-web
  redis:
    host: 192.168.47.142
    port: 6379
  cloud:
    gateway:
      globalcors:
        # 跨域请求配置
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
           # 唯一标识符
           - id: changgou_goods-route
             uri: lb://goods
             - Path=/**
             filters:
               - StripPrefix=1
               ############## 配置令牌桶 ##############
               # 默认 redis 为127.0.0.1:6379,需根据实际情况配置
               - name: RequestRateLimiter #局部显示流过滤,名字不能随便写
                 args:
                   key-resolver: "#{@ipKeyResolver}" # 用户身份唯一识别标识符
                   redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
                   redis-rate-limiter.burstCapacity: 1 #令牌桶总容量

使用JMeter 测试

 

三、微服务网关结合 JWT 实现用户登录校验

3.1 整体框架流程

  • 用户通过用户中心网关访问用户微服务,进行登录
  • 用户微服务利用 JWT 生成 token 令牌,并在 Header 中返回给用户
  • 用户之后的请求到要带上这个 token 令牌,在请求到达用户中心网关
  • 用户中心网关会对令牌的正确性和实效性进行验证,成功且有效则会继续向下分发,若令牌错误或过期则会拒绝请求

3.2 使用

3.2.1 加入工具类 JwtUtil.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;

/**
 * JWT工具类
 */
public class JwtUtil {

    // 有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时
    // 设置秘钥明文
    public static final String JWT_KEY = "tomcast";
    // 设置颁发者
    public static final String JWT_ISS = "tom";

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        SecretKey secretKey = generalKey();

        JwtBuilder builder = Jwts.builder()
                .setId(id)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer(JWT_ISS)     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析令牌数据
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        SecretKey secretKey = generalKey();
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
    }
}

 

3.2.2 增加 login 接口

/**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @GetMapping("/login")
    public Result login(String username, String password, HttpServletResponse response) {
        // 查询用户信息
        User user = userService.findById(username);
        // 对比密码
        if (BCrypt.checkpw(password, user.getPassword())) {
            // 创建用户令牌信息
            Map<String, Object> tokenMap = new HashMap<String, Object>();
            tokenMap.put("role","USER");
            tokenMap.put("success", "SUCCESS");
            tokenMap.put("username", username);
            String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenMap), null);

            // 将令牌信息存入到Cookie
            Cookie cookie = new Cookie("Authorization", token);
            cookie.setDomain("localhost");
            cookie.setPath("/");
            response.addCookie(cookie);

            // 将令牌作为参数给用户
            return Result.ok("登录成功", token);
        }
        // 密码匹配失败,登录失败
        return Result.error("账号或密码有误");
    }

 

3.2.3 修改网关服务代码

配置启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @File: GatewayWebApplication
 * @Description:
 * @Author: tom
 **/
@SpringBootApplication
@EnableEurekaClient
public class GatewayWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayWebApplication.class);
    }

    /**
     * 创建用户唯一标识,使用 IP 作为用户的唯一标识,根据 IP 进行限流操作
     * @return
     */
    @Bean("ipKeyResolver")
    public KeyResolver userKeyResolver() {
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
//                return Mono.just("需要使用的用户身份识别唯一标识");
                String ip = exchange.getRequest().getRemoteAddress().getHostName();
                return Mono.just(ip);
            }
        };
    }
}

 

配置过滤器 AuthorizeFilter

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @File: AuthorizeFilter
 * @Author: tom
 * @Create: 2020-06-11 13:57
 * @Description: 全局过滤器,实现用户权限鉴别(校验)
 **/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    private static final String AUTHORIZE_TOKEN = "Authorization";

    /**
     * 全局拦截
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 获取用户令牌信息
        // 1. 从 Header 中获取
        String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
        Boolean isInHeader = true;

        // 2. 从 参数 中获取
        if (StringUtils.isEmpty(token)) {
            token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
            isInHeader = false;
        }
        // 3. 从 Cookie 中获取
        if (StringUtils.isEmpty(token)) {
            HttpCookie httpCookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if(httpCookie != null) {
                token = httpCookie.getValue();
                isInHeader = false;
            }
        }

        // 若没有令牌,则拦截
        if (StringUtils.isEmpty(token)) {
            // 传入空数据
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        // 若有令牌,则校验令牌是否有效
        try {
            // 解析成功,则放行
            JwtUtil.parseJWT(token);
        } catch (Exception e) {
            // 无效则拦截
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        // 将令牌封装到头文件中
        if (isInHeader) {
            request.mutate().header(AUTHORIZE_TOKEN, token);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值