畅购商城(八):微服务网关和JWT令牌

好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

微服务网关

介绍

网关是介于用户和微服务之前的中间层。说白了,网关就像是小区的保安,无论你想到小区的哪一户人家去,你都得先通过小区的大门。所以,小区的保安可以做人员统计,还可以控制某个时间段进去小区的人数,限制进入小区的资格等。保证了小区业主们的安全。微服务网关同样起着这些作用。

为什么要有微服务网关

不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 认证复杂,每个服务都需要独立认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
  • 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难

那么有了微服务网关之后,这些问题就可以得到解决。它有着以下优点。

  • 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数
  • 易于统一授权。

总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能

网关微服务

微服务搭建

一个项目中可能会用到不止一个网关,所以我们将网关微服务放在changgou-gateway父工程下。现在我们创建一个名为changou-gateway-web的微服务。有些依赖是所有网关微服务都要用到的,所以将这些依赖放在父工程下:

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

启动类和配置文件不能少,启动类就不贴了,配置文件如下👇

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: //127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

网关过滤配置

  • Host 路由
# 用户请求的域名规格配置,所有以robod.changgou.com开头的请求都将被路由到//localhost:18081微服务
# 例如  //robod.changgou.com:8001/brand ——> //localhost:18081/brand
# 但是首先得在hosts文件中配置一下:   127.0.0.1  robod.changgou.com
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route   # 唯一标识符
          uri: //localhost:18081
          predicates:
            - Host=robod.changgou.com**
  • - Path 路径匹配过滤配置
# 所有以/brand开头的请求都将路由到//localhost:18081
# 例如  localhost:8001/brand  ——>  localhost:18081/brand
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: //localhost:18081
          predicates:
            - Path=/brand/**
  • PrefixPath 过滤配置
# 自动加上某个前缀,用户请求/** ——>/brand/**
# 例如  localhost:8001/111  ——>  localhost:8001/brand/111  ——>  localhost:18081/brand/111
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: //localhost:18081
          predicates:
            - Path=/**
          filters:
            - PrefixPath=/brand
  • StripPrefix 过滤配置
# 将请求路径中的前n个路径去掉,请求路径以/区分,一个/代表一个路径
# 例如  localhost:8001/api/brand/111  ——>  localhost:8001/brand/111  ——>  localhost:18081/brand/111
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: //localhost:18081
          predicates:
            - Path=/**
          filters:
            - StripPrefix=1
  • LoadBalancerClient 路由过滤器(客户端负载均衡)
# 使用LoadBalancerClient实现负载均衡,后面的goods是微服务的名称,主要应用于集群环境
# 比如现在有5台服务器都是goods微服务,网关就会自动将请求发送给不同的服务器达到负载均衡的目的
spring:
  cloud:
    gateway:
      routes:
        - id: changgou_goods_route
          uri: lb://goods

网关限流

当访问量多大的时候,我们的服务就可能会挂掉,所以我们需要对每个微服务进行限流,但是这样比较麻烦。有了网关之后,我们可以对网关进行限流,因为所有的请求必须通过网关才能到达微服务,这样比较方便。

令牌桶算法

常见的限流算法有计数器,漏斗,令牌桶算法。令牌桶算法有以下几个特点:

  • 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
  • 根据限流大小,设置按照一定的速率往桶里添加令牌;
  • 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
  • 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
  • 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

使用令牌桶进行请求次数限流

spring cloud gateway 默认使用redis的RateLimter限流算法来实现。首先在changgou-gateway-web中添加Redis的依赖:

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

然后我们需要有限流的Key,这里用IP来当作限流的Key,限制某一个IP在一定时间段的访问次数,在启动类中定义一个Bean用于获取key

@Bean(name = "ipKeyResolver")
public KeyResolver userKeyResolver() {
    return exchange -> {
        String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName();
        return Mono.just(ip);
    };
}

我这里使用了Lamda去简化书写。接下来还得在配置文件中配置一下

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      routes:
          filters:
            - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的factory
              args:
                # 用户身份唯一标识符
                key-resolver: "#{@ipKeyResolver}"
                # 允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的容量,允许在一秒钟内完成的最大请求数
                redis-rate-limiter.burstCapacity: 1

既然是使用redis的RateLimter限流算法,那么Redis的配置自然不能少。

#Redis配置
spring:
  application:
  redis:
    host: 192.168.31.200
    port: 6379

限流的配置就配置好了,现在如果在1秒内请求超过1次的话就会被拒绝。

JWT

在实现用户登录功能之前,我们先来介绍一下JWT(JSON Web Token)。是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范。

JWT的构成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。为了能够直观的看到JWT的结构,我画了一张思维导图:

最终生成的JWT令牌就是下面这样,有三部分,用 . 分隔。

base64UrlEncode(JWT 头)+”.”+base64UrlEncode(载荷)+”.”+HMACSHA256(base64UrlEncode(JWT 头) + “.” + base64UrlEncode(有效载荷),密钥)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT的使用

  • 导入依赖:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
  • 创建Token
public String createToken() {
    JwtBuilder builder = Jwts.builder()
            .setId("test1")
            .setSubject("Robod")
            .setAudience("马化腾")
            .setIssuedAt(new Date());
            .signWith(SignatureAlgorithm.HS256,"robod666");
    Map<String,Object> map = new HashMap<>();
    map.put("ha","哈哈哈");
    builder.addClaims(map);
    return builder.compact();
}
  • 解析Token
public String parseToken() {
    String compactJwt="eyJhbGciOiJIUzI1NiJ9" +
            ".eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9" +
            ".RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
    Claims claims = Jwts.parser().
            setSigningKey("robod666").
            parseClaimsJws(compactJwt).
            getBody();
    return claims.toString();
}

用户登录与鉴权

介绍了JWT之后,我们就来用JWT实现用户登录与鉴权。流程如下:

首先我们需要准备一个JWT的工具类,JWTUtil,放在changgou-common下:

public class JwtUtil {
    //默认有效期,一个小时
    public static final Long JWT_TTL = 3600000L;

    //Jwt令牌信息
    public static final String JWT_KEY = "RobodLee";

    //密钥
    public static SecretKey secretKey = generalKey();

    //生成令牌
    public static String createJWT(String id, String subject, Long ttlMillis) {
        //指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //当前系统时间
        long nowMillis = System.currentTimeMillis();
        //令牌签发时间
        Date now = new Date(nowMillis);

        //如果令牌有效期为null,则默认设置有效期1小时
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }

        //令牌过期时间设置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        //封装Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    //唯一的ID
                .setSubject(subject)          // 主题  可以是JSON数据
                .setIssuer("robod")          // 签发者
                .setIssuedAt(now)             // 签发时间
                .signWith(signatureAlgorithm,secretKey) // 签名算法以及密匙
                .setExpiration(expDate);      // 设置过期时间
        return builder.compact();
    }

   	//生成加密 secretKey
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

	//解析令牌
    public static Claims parseJWT(String jwt) throws Exception {
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

我发现资料提供的代码中每次调用generalKey()、parseJWT()方法的时候都去调用generalKey()方法去生成SecretKey,但是generalKey()方法内容是不变的,所以可以将SecretKey单独提取出来,这样就不用每次都调用generalKey()去生成了。

然后创建一个用户微服务changou-service-user在UserController中编写登录逻辑👇

@RequestMapping("/login")
public Result<String> login(String username, String password, HttpServletResponse response) {
    User user = userService.findById(username);
    if (BCrypt.checkpw(password,user.getPassword())){
        Map<String,Object> tokenInfo = new HashMap<>(4);
        tokenInfo.put("role","USER");
        tokenInfo.put("success","SUCCESS");
        tokenInfo.put("username",username);
        String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenInfo), null);
        Cookie cookie = new Cookie("Authorization",token);
        cookie.setDomain("localhost");
        cookie.setPath("/");
        response.addCookie(cookie);
        return new Result<>(true,StatusCode.OK,"登录成功",token);
    }
    return new Result<>(false,StatusCode.LOGIN_ERROR,"登录失败");
}

在这段代码中,调用Service层从数据库中查出对应的User,然后比对password,看密码是否正确。如果正确,就调用JwtUtil创建一个JWT令牌,并放入一些简单的信息。然后将JWT令牌存入Cookie中,并返回给前端。如果登录失败就返回登录失败的信息。

然后就是在网关微服务中添加相应的逻辑了,在changgou-gateway-web中配置一下,配置一下User微服务的路由。

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      routes:
        - id: changgou_user_route   # 唯一标识符
          uri: //localhost:18088
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1

再添加一个过滤器:

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    private static final String AUTHORIZE_TOKEN = "Authorization";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String token;
        //从头中获取Token
        token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
        //请求头中没有Token就从参数中获取
        if (StringUtils.isEmpty(token)){
            token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }
        //参数中再没有Token就从Cookie中获取
        if (StringUtils.isEmpty(token)){
            HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if (cookie!=null){
                token = cookie.getValue();
            }
        }
        //还是没有Token就拦截
        if (StringUtils.isEmpty(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //Token不为空就校验Token
        try {
            JwtUtil.parseJWT(token);
        } catch (Exception e) {
            //报异常说明Token是错误的,拦截
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }

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

这段代码就是分别从Header,参数,Cookie中看有没有Token信息,没有的话就说明用户没有权限,拦截下来。有Token的话就解析一下Token有没有错,错误就拦截下来。如果都没有问题的话就放行,将请求路由到用户微服务中。

这是没有Token的情况下👆

当我们登陆后就会获取到Token👇

当我们携带着token去访问就没有问题了👇

小结

这篇文章中,首先介绍了微服务网关及网关的搭建及过滤配置和限流配置。然后介绍了JWT,最后使用了JWT去实现了用户登录与鉴权的操作。

如果我的文章对你有些帮助,不要忘了点赞收藏转发关注。要是有什么好的意见欢迎在下方留言。让我们下期再见!
微信公众号