Spring cloud alibaba 组件gateway网关、配置管理 以及链路追踪、Jwt鉴权过滤

Spring cloud alibaba

一. Spring cloud Gateway网关

什么是网关?就是网络请求的统一入口.

为什么需要网关?

1.如果我们的有成千上万个服务,我们在请求每个服务的时候都需要进行认证,难度与工作量可想而知,要控制用户对于整个服务的访问次数的限制。

2.如果没有统一的入口,那么前端在与服务端交互的时候定位到各个服务,假设服务器端作服务的重构,那么前端也得跟着一起修改。

gateway是spring cloud的第二代网关,其性能是zuul的1.6倍左右,其内部是基于netty、reactor(多路复用)、webflux进行构建,性能强大。gateway需要从注册中心获取服务,然后通过网关来调用对应的服务。但是gateway不在web环境下运行,也就是说不能打成war包放在tomcat下运行

1.1 快速入门

1.创建springcloudalibaba-micro-service-gateway-9090子工程,导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloudalibaba-micro-service-manager</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloudalibaba-micro-service-gateway-9090</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>

            <exclusions>
                <!-- 排除掉springmvc相关的配置信息 -->
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-webmvc</artifactId>
                </exclusion>
                <!-- 排除掉tomcat相关的配置 -->
                <exclusion>
                    <groupId>org.springframework.bootk</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-el</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.tomcat.embed</groupId>
                    <artifactId>tomcat-embed-websocket</artifactId>
                </exclusion>
            </exclusions>

        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

    </dependencies>

</project>

2.创建 GateWayApplication 启动类

package com.qf;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MicroServiceGateWay {
    public static void main(String[] args) {

        SpringApplication.run(MicroServiceGateWay.class,args);
    }
}

3.创建application.yml文件

spring:
  application:
    name: micro-service-gateway # 网关名称

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        enabled: true
        register-enabled: false # 不注册到nacos上

    gateway:
      discovery:
        locator:
          enabled: true #开启网关端口并使用服务方的名称来访问
          #之前:http://localhost:8080/feign/getById?id=1002
          #现在:http://localhost:9090/micro-service-consumer/feign/getById?id=1002
      
server:
  port: 9090

4.启动服务提供方,消费方以及网关对应工程,进行测试

1.2 谓词配置

谓词(predicate)是gateway内置的的一下关于请求相关的处理,在application.yml中增加routes的配置

注意:谓词配置后,访问时就不需要再写服务名称了,因为谓词里面已经配置过了

配置谓词之前:http://localhost:9090/micro-service-consumer/feign/getUsers

配置谓词之后:http://localhost:9090/feign/getUsers

spring:
  application:
    name: micro-service-gateway # 网关

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        enabled: true
        register-enabled: false # 不注册到nacos上

    gateway:
      discovery:
        locator:
          enabled: true #开启之后通过网关访问:http://localhost:9090/micro-service-consumer/feign/getUsers
      routes:
        # id可以不是服务名,名字任意,但是不能重复,推荐使用服务名的方式
        - id: micro-service-consumer
          # uri才是控制着某个具体的访问到达我们特定的服务
          uri: lb://micro-service-consumer
          # 谓词: 就是满足的条件,可以在org.springframework.cloud.gateway.handler.predicate这个包下
          predicates:
            # 配置访问消费方controller的一级目录名称,这样就可以通过http://localhost:9090/feign/getUsers来访问了
            - Path=/feign/**
            # 请求的参数中必须携带origin这个参数名,参数值符合[a-zA-Z]+ 这个正则
            - Query=origin,[a-zA-Z]+
            # 请求的方式
            - Method=get,post
            # 设置时间区间内访问: 2020年12月31日 - 2030年12月31日,可以访问,+08:00表示时区
            - After=2020-12-31T00:00:00+08:00[Asia/Shanghai]
            - Before=2030-12-31T00:00:00+08:00[Asia/Shanghai]
            # 描述IP在10.8.13.1~10.8.13.255之间的地址才可以访问
            - RemoteAddr=10.8.13.0/24
            # 请求的头中必须得携带token, value值符合[a-zA-Z0-9]+ 这个正则
            - Header=token,[a-zA-Z0-9]+

server:
  port: 9090

1.3 过滤器配置

GateWay提供了很多内置的过滤器让我们使用,具体的过滤器在spring-cloud-gateway-core-2.1.2.RELEASE.jar下的org.springframework.cloud.gateway.filter.factory包下,接下来我们挑其中一个非常常用的过滤来讲解用法,在实际的开发过程中,有这样一种业务需求,就是限制同一个IP对服务器频繁的请求,例如我们限制每个IP在每秒只能访问3次,那么要怎么实现呢?其实spring-boot已经帮我们实现好了一个,只需要做一定的配置就可以了。

IP限制的原理就是令牌桶算法,随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token,如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。如下图所示:

1.在springcloudalibaba-micro-service-gateway-9090工程中添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.创建RedisHostKeyResovler类,实现获取IP的组件

package com.qf.resolver;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class RedisHostKeyResolver implements KeyResolver {

    /**
     * webflux:
     *      Mono: 用于返回单个值
     *      Flux: 用于返回集合数据
     */
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        //获取用户的访问的 ip
        String host = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        return Mono.just(host); //构建 Mono<String>
    }
}

3.在application.yml中增加filters和redis的配置

server:
  port: 9090

spring:
  application:
    name: micro-service-gateway #服务名称,在nacos上显示

  cloud:
    nacos:
      discovery:
        enabled: true #注册服务
        server-addr: 127.0.0.1:8848 #nacos访问地址
        register-enabled: false #不注册到nacos
    gateway:
      discovery:
        locator:
          enabled: true #开启网关访问
          #访问方式:
                  #原来:http://localhost:8080/feign-user-consumer/findById?id=1&origin=123
           #重点掌握现在:http://localhost:9090/micro-service-consumer/feign-user-consumer/findById?id=1&origin=123
           #一般不这样用:http://localhost:9090/micro-service-provider/user-provider/findById?id=1
      routes:
      # id可以不是服务名,名字任意,但是不能重复,推荐使用服务名的方式
      - id: micro-service-consumer
        # uri才是控制着某个具体的访问到达我们特定的服务
        uri: lb://micro-service-consumer
        # 谓词: 就是满足的条件,可以在org.springframework.cloud.gateway.handler.predicate这个包下
        predicates:
        # 配置访问消费方controller的一级目录名称,这样就可以通过http://localhost:9090/feign-user-consumer/getUsers来访问了
        - Path=/feign-user-consumer/**
        # 请求的参数中必须携带origin这个参数名,参数值符合[a-zA-Z]+ 这个正则
        - Query=origin,[a-zA-Z]+
        # 请求的方式
        - Method=get,post
        # 设置时间区间内访问: 2020年12月31日 - 2030年12月31日,可以访问,+08:00表示时区
        - After=2020-12-31T00:00:00+08:00[Asia/Shanghai]
        - Before=2030-12-31T00:00:00+08:00[Asia/Shanghai]
        # 描述IP在10.8.13.1~10.8.13.255之间的地址才可以访问
        #- RemoteAddr=10.8.162.0/24
        # 请求的头中必须得携带token, value值符合[a-zA-Z0-9]+ 这个正则
        #- Header=token,[a-zA-Z0-9]+

        filters:
          # RequestRateLimiter是固定值
          - name: RequestRateLimiter
            args:
              # key-resolver是用于限流的bean对象,通过SpEL的方式 #{@XXX} 取出spring容器中的bean
              keyResolver: '#{@redisHostKeyResolver}'
              # 每秒往令牌桶中存放的数量
              redis-rate-limiter.replenishRate: 1
              # 令牌桶中最多的令牌的数量
              redis-rate-limiter.burstCapacity: 3

  redis:
    host: 127.0.0.1
    port: 6379

4.可以让局域网内的其他用户访问本机:http://本机ip:9090/feign/getUsers?origin=abc

1.4 自定义全局过滤器

1.导入依赖

<dependency>
    <groupId>com.qf</groupId>
    <artifactId>springcloudalibaba-micro-service-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.79</version>
</dependency>

2.创建全局过滤器类

package com.qf.filters;

import com.alibaba.fastjson2.JSON;
import com.qf.utils.JsonResult;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

//创建多个全局过滤器
@Configuration
public class FilterConfig {

    @Bean
    @Order(-100)//正数值越小,负数的绝对值越大,优先级越高
    public GlobalFilter loginFilter(){
        return new GlobalFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("-100");

                //获取传入的用户名和密码
                ServerHttpRequest request = exchange.getRequest();
                //获取所有参数
                MultiValueMap<String, String> queryParams = request.getQueryParams();
                //获取前端传过来的name以及password
                List<String> nameList = queryParams.get("name");
                List<String> passwordList = queryParams.get("password");
                //判断
                if(nameList !=null && nameList.size() > 0 && passwordList !=null && passwordList.size() > 0){
                    //模拟数据库查询
                    if("jack".equals(nameList.get(0)) && "123".equals(passwordList.get(0))){
                        //放行
                        return chain.filter(exchange);
                    }else{
                        //参数输入错误
                        JsonResult jsonResult = JsonResult.fail();
                        jsonResult.setData("name or password is error");

                        //返回
                        ServerHttpResponse response = exchange.getResponse();
                        String jsonString = JSON.toJSONString(jsonResult);
                        DataBuffer dataBuffer = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
                        return response.writeWith(Mono.just(dataBuffer));
                    }

                }else{
                    //参数输入错误
                    JsonResult jsonResult = JsonResult.fail();
                    jsonResult.setData("name or password is null");
                    //返回
                    ServerHttpResponse response = exchange.getResponse();
                    String jsonString = JSON.toJSONString(jsonResult);
                    DataBuffer dataBuffer = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
                    return response.writeWith(Mono.just(dataBuffer));
                }

            }
        };
    }

    @Bean
    @Order(-90)//正数值越小,负数的绝对值越大,优先级越高
    public GlobalFilter otherFilter(){
        return new GlobalFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("-90");
                return chain.filter(exchange);
            }
        };
    }

}

3.浏览器访问:http://localhost:9090/feign/getUsers?origin=qwer&username=jack&password=123 进行测试

二. nacos配置管理

配置管理,就是将所有的微服务的配置统一进行管理,这样做的好处是我们的配置不用写在项目中,实现集中化的管理,方便环境的变更。

2.1 基本配置

1.在springcloud-alibaba-microservice-gateway-9090(网关)和springcloudalibaba-micro-service-consumer-8080(服务消费方)工程中都导入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2.手动在桌面创建文件夹,命名为:micro-service-gateway,然后把springcloud-alibaba-microservice-gateway-9090网关工程中的application.yml中的中文注释都删了再拷贝过去,修改名称为:micro-service-gateway-dev.yml,然后再复制一份,命名改为:micro-service-gateway-test.yml,修改micro-service-gateway-test.yml中的内容,使其和micro-service-gateway-dev.yml文件的内容略有不同即可,比如:修改micro-service-gateway-test.yml 端口号为9091。

同样类似的方式,对springcloudalibaba-micro-service-consumer-8080工程中的application.yml也执行一遍。

最后,分别对micro-service-gateway和micro-service-consumer两个文件夹打成zip压缩包文件,一会要导入到nacos中使用。

3.在springcloud-alibaba-microservice-gateway-9090(网关)和springcloudalibaba-micro-service-consumer-8080(服务消费方)工程中都配置bootstrap.yml,内容如下:(group和name对应的值最好和刚才桌面上创建文件夹的名称一致),然后把两个工程中原来的application.yml命名为application.yml.bak

spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        # 配置文件的后缀名
        file-extension: yml
        # 配置在nacos上的组名
        group: micro-service-gateway
  application:
    # 服务名称
    name: micro-service-gateway
  profiles:
    # 配置文件环境(生产环境,开发环境,测试环境等等,对应不同的application-*.yml文件)
    active: dev
spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        # 配置文件的后缀名
        file-extension: yml
        # 配置在nacos上的组名
        group: micro-service-consumer
  application:
    # 服务名称
    name: micro-service-consumer
  profiles:
    # 配置文件环境(生产环境,开发环境,测试环境等等,对应不同的application-*.yml文件)
    active: dev

4.在nacos中导入新建配置文件(zip压缩包形式),参考课件提供的配置文件

5.启动工程进行测试:

配置 active: dev 则显示nacos中对应 *-dev.yml配置文件中的内容;

配置 active: test则显示nacos中对应 *-test.yml配置文件中的内容;

2.2 配置的实时刷新

1.在Controller中添加方法并测试,修改springcloudalibaba-micro-consumer-8080中FeignUserController,在FeignUserController上添加实时刷新注解@RefreshScope

@RefreshScope//实时刷新
@RestController
@RequestMapping("feign")
public class UserFeignController {

    @Value("${user.username}")
    private String username;

    @RequestMapping("getUserName")
    public String getUserName(){
        return username;
    }

2.在nacos面板中修改对应加载的microservice-consumer-test.yml,设置user.username的值为张三

3.启动springcloudalibaba-micro-consumer-8080工程,访问getUsername方法测试即可

4.然后把user.username的值改为李四,再次访问测试即可

三. 链路追踪

微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。

举个例子,在微服务系统中,一个来自用户的请求,请求先达到前端A(如前端界面),然后通过远程调用,达到系统的中间件B、C(如负载均衡、网关等),最后达到后端服务D、E,后端经过一系列的业务逻辑计算最后将数据返回给用户。对于这样一个请求,经历了这么多个服务,怎么样将它的请求过程的数据记录下来呢?这就需要用到服务链路追踪。

在本章中我们主要会讲解spring cloud sleuth整合Zipkin来实现链路的追踪,整合起来非常的简单,只需要引入相关的依赖,再加上相关的配置即可。

3.1 Sleuth基本术语

Sleuth负责记录微服务数据,Zipkin是Twitter开源的分布式跟踪系统,主要用来收集系统的时序数据,从而跟踪系统的调用问题。

span: 基本工作单元,发送一个远程调度任务 就会产生一个Span,Span是一个64位ID唯一标识的,Trace是用另一个64位ID唯一标识的,Span还有其他数据信息,比如摘要、时间戳事件、Span的ID、以及进度ID。

**Trace: ** 一系列Span组成的一个树状结构。请求一个微服务系统的API接口,这个API接口,需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。

**Annotation: **用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束 。这些注解包括以下:

  • cs - Client Sent -客户端发送一个请求,这个注解描述了这个Span的开始
  • sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络传输的时间。
  • ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户端),如果ss的时间戳减去sr时间戳,就可以得到服务器请求的时间。
  • cr - Client Received (客户端接收响应)-此时Span的结束,如果cr的时间戳减去cs时间戳便可以得到整个请求所消耗的时间。

3.2 链路追踪的搭建

Zipkin Server的下载地址:https://github.com/openzipkin/zipkin

1.启动Zipkin Server,命令如下:java -jar zipkin-server-2.19.1-exec.jar,访问:http://localhost:9411/

2.在需要追踪的服务中添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

3.在nacos面板中分别修改micro-service-consumer-dev.yml和micro-service-gateway-dev.yml中配置如下内容,并分别在各自工程中的bootstrap.yml中指定调用该dev配置文件

spring:
  zipkin:
    base-url: http://localhost:9411/
    discovery-client-enabled: false
  sleuth:
    sampler:
    # 抽样率 100%, 默认10%, 如果服务流量较大,全部采集对存储造成的压力也会很大
    probability: 0.2

D. 查看结果

四.Jwt鉴权过滤

JJWT的是在JVM上创建和验证JSON Web Token(JWTs)的库,基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。

4.1 引入工具类

4.1.1在springcloudalibaba-micro-service-common中导入依赖
<!-- jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.8</version>
</dependency>
4.1.2在springcloudalibaba-micro-service-commons中创建JsonResult和JwtHelper工具类

JsonResult

package com.qf.utils;

import lombok.Data;

@Data
public class JsonResult<T> {

    private String msg;
    private Integer code;
    private T data;


    public static JsonResult ok(){
        JsonResult jsonResult = new JsonResult<>();
        jsonResult.setCode(200);
        jsonResult.setMsg("success");

        return jsonResult;
    }

    public static JsonResult fail(){
        JsonResult jsonResult = new JsonResult<>();
        jsonResult.setCode(-20000);
        jsonResult.setMsg("error");

        return jsonResult;
    }
}

JwtHelper

package utils;


import io.jsonwebtoken.*;
import org.apache.commons.lang3.time.DateUtils;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

//jwt工具类
public class JwtHelper {

    // 生成Jwt
    public static String jwsWithHS(SignatureAlgorithm signatureAlgorithm, String userInfo, int num, String secret) {
        Key key = new SecretKeySpec(secret.getBytes(), signatureAlgorithm.getJcaName());
        Claims claims = Jwts.claims();
        claims.setSubject(userInfo);  // jwt的信息的本身
        claims.setExpiration(DateUtils.addSeconds(new Date(), num)); //设置jwt多少秒过期
        String jws = Jwts.builder()
                .setClaims(claims).signWith(key, signatureAlgorithm).compact();
        return jws;
    }

    // 校验Jwt
    public static Jwt verifySign(String jws, String secret, SignatureAlgorithm signatureAlgorithm) {
        Key key = new SecretKeySpec(secret.getBytes(), signatureAlgorithm.getJcaName());
        Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(jws);
        return jwt;
    }

    public static void main(String[] args) {
        //设置,算法:HS256,用户信息:jack,过期时间:10分钟,密码:202cb962ac59075b964b07152d234b70
        //System.out.println(jwsWithHS(SignatureAlgorithm.HS256, "jack", 600, "202cb962ac59075b964b07152d234b70"));

        //校验jwt
        String jwtstring = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYWNrIiwiZXhwIjoxNjQwOTMzMTk4fQ.igm6YSD1nBO4hT4rywa5ZPB3FXvD9gcYHEWqL8Rvk18";

        Jwt jwt = verifySign(jwtstring, "202cb962ac59075b964b07152d234b70", SignatureAlgorithm.HS256);
        Claims claims = (Claims)jwt.getBody();
        System.out.println(claims);
        //获取用户信息
        System.out.println(claims.get("sub"));

    }
}

4.2创建认证模块

4.2.1创建springcloudalibaba-micro-service-authentic-6060子模块,导入依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>com.qf</groupId>
            <artifactId>springcloudalibaba-micro-service-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 均衡负载接口调用Feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

        <!-- 熔断降级sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- nacos配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- 链路追踪 -->
<!--                <dependency>-->
<!--                    <groupId>org.springframework.cloud</groupId>-->
<!--                    <artifactId>spring-cloud-starter-zipkin</artifactId>-->
<!--                    <version>2.2.6.RELEASE</version>-->
<!--                </dependency>-->

    </dependencies>
4.2.2配置application.yml
server:
  port: 6060

spring:
  application:
    name: micro-service-authentic #服务名称,在nacos上显示

  cloud:
    nacos:
      discovery:
        enabled: true #注册服务
        server-addr: 127.0.0.1:8848 #nacos访问地址

    sentinel:
      transport:
        port: 8719 #内部传输的端口
        dashboard: localhost:8888 #sentinel控制面板访问端口
      web-context-unify: false #false表示针对调用同一接口的不同url进行链路限制

#feign配合sentinel使用
feign:
  sentinel:
    enabled: true

#配置签名和算法
jwt:
  secret: 21232f297a57a5a743894a0e4a801fc3
  signature-algorithm: HS256
4.2.3编写启动类
package com.qf;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient//注册服务
@EnableFeignClients//feign
public class MicroServiceAuthenticApplication {

    public static void main(String[] args) {
        SpringApplication.run(MicroServiceAuthenticApplication.class,args);
    }
}
4.2.4编写Controller
package com.qf.controller;

import com.qf.utils.JsonResult;
import com.qf.utils.JwtHelper;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/authentic")
public class AuthenticController {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.signature-algorithm}")
    private String algorithm;

    @RequestMapping("login")
    public JsonResult login(String usercode,String password){

        //判断传入的参数
        if((usercode != null && usercode.length() > 0) && (password != null && password.length() > 0) ){
            //模拟数据库进行比对
            if("jack".equals(usercode) && "123".equals(password)){
                //登录成功,生成jwt
                String jwt = JwtHelper.jwsWithHS(SignatureAlgorithm.forName(algorithm), usercode, 600, secret);
                //返回
                JsonResult jsonResult = JsonResult.ok();
                jsonResult.setData(jwt);

                return jsonResult;
            }else {
                JsonResult jsonResult = JsonResult.fail();
                jsonResult.setData("login is fail");
                return jsonResult;
            }

        }else{
            JsonResult jsonResult = JsonResult.fail();
            jsonResult.setData("usercode or password is null");
            return jsonResult;
        }

    }
}

4.3 配置网关

4.3.1在springcloudalibaba-micro-service-gateway-9090导入公共模块依赖
<dependency>
    <groupId>com.qf</groupId>
    <artifactId>springcloudalibaba-micro-service-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
4.3.2在springcloudalibaba-micro-service-gateway-9090的application.yml中配置谓词和jwt
server:
  port: 9090

spring:
  application:
    name: micro-service-gateway #服务名称,在nacos上显示

  cloud:
    nacos:
      discovery:
        enabled: true #注册服务
        server-addr: 127.0.0.1:8848 #nacos访问地址
        register-enabled: false #不注册到nacos
    gateway:
      discovery:
        locator:
          enabled: true #开启网关访问
          #访问方式:
               #原来: http://localhost:8080/feign/findById?id=1&origin=123
          #重点掌握现在:http://localhost:9090/micro-service-consumer/feign/findById?id=1&origin=123
 
      routes:
      # 配置认证服务
      - id: micro-service-authentic
        uri: lb://micro-service-authentic
        predicates:
        - Path=/authentic/**
      # id可以不是服务名,名字任意,但是不能重复,推荐使用服务名的方式
      - id: micro-service-consumer
        # uri才是控制着某个具体的访问到达我们特定的服务
        uri: lb://micro-service-consumer
        # 谓词: 就是满足的条件,可以在org.springframework.cloud.gateway.handler.predicate这个包下
        predicates:
        # 配置访问消费方controller的一级目录名称,这样就可以通过http://localhost:9090/feign/getUsers来访问了
        - Path=/feign/**
        # 请求的参数中必须携带origin这个参数名,参数值符合[a-zA-Z]+ 这个正则
        - Query=origin,[a-zA-Z]+
        # 请求的方式
        - Method=get,post
        # 设置时间区间内访问: 2020年12月31日 - 2030年12月31日,可以访问,+08:00表示时区
        - After=2020-12-31T00:00:00+08:00[Asia/Shanghai]
        - Before=2030-12-31T00:00:00+08:00[Asia/Shanghai]
        # 描述IP在10.8.13.1~10.8.13.255之间的地址才可以访问
        #- RemoteAddr=10.8.162.0/24
        # 请求的头中必须得携带token, value值符合[a-zA-Z0-9]+ 这个正则
        #- Header=token,[a-zA-Z0-9]+

#        filters:
#          # RequestRateLimiter是固定值
#          - name: RequestRateLimiter
#            args:
#              # key-resolver是用于限流的bean对象,通过SpEL的方式 #{@XXX} 取出spring容器中的bean
#              keyResolver: '#{@redisHostKeyResolver}'
#              # 每秒往令牌桶中存放的数量
#              redis-rate-limiter.replenishRate: 1
#              # 令牌桶中最多的令牌的数量
#              redis-rate-limiter.burstCapacity: 3
#
#  redis:
#    host: 127.0.0.1
#    port: 6379

#配置签名和算法
jwt:
  secret: 21232f297a57a5a743894a0e4a801fc3
  signature-algorithm: HS256
4.3.3在springcloudalibaba-micro-service-gateway-9090创建AuthFilter过滤器
package com.qf.filters;

import com.alibaba.fastjson2.JSON;
import com.qf.utils.JsonResult;
import com.qf.utils.JwtHelper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.signature-algorithm}")
    private String algorithm;

    //校验token
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //请求路径
        String requestPath = exchange.getRequest().getPath().toString();
        System.out.println(requestPath);
        //判断,如果是登录,直接放行
        if(requestPath.contains("/authentic/login")){
            return chain.filter(exchange);
        }
        
        //访问的是其他服务
        //获取token进行校验
        String token = exchange.getRequest().getHeaders().getFirst("token");

        //判断
        if(token == null){
            
            JsonResult jsonResult = JsonResult.fail();
            jsonResult.setData("token is null");

            //返回
            ServerHttpResponse response = exchange.getResponse();
            String jsonString = JSON.toJSONString(jsonResult);
            DataBuffer dataBuffer = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(dataBuffer));
        }else{

            try {

                //校验
                Jwt jwt = JwtHelper.verifySign(token, secret, SignatureAlgorithm.forName(algorithm));

                Claims claims = (Claims)jwt.getBody();
                String sub = claims.get("sub").toString();
                System.out.println("sub:" + sub);

                if("jack".equals(sub)){
                    return chain.filter(exchange);
                }else{
                    JsonResult jsonResult = JsonResult.fail();
                    jsonResult.setData("usercode is error");

                    //返回
                    ServerHttpResponse response = exchange.getResponse();
                    String jsonString = JSON.toJSONString(jsonResult);
                    DataBuffer dataBuffer = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
                    return response.writeWith(Mono.just(dataBuffer));
                }

            }catch (Exception e){
                JsonResult jsonResult = JsonResult.fail();
                jsonResult.setData(e.getMessage());

                //返回
                ServerHttpResponse response = exchange.getResponse();
                String jsonString = JSON.toJSONString(jsonResult);
                DataBuffer dataBuffer = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(dataBuffer));
            }

        }
    }

    @Override
    public int getOrder() {
        //正数值越小,负数的绝对值越大,优先级越高
        return 1;
    }
}

4.4测试

4.4.1启动并测试:http://localhost:9090/feign/getUsers?origin=qwer 由于Header中未携带tonken则无法访问
4.4.2再次访问:http://localhost:9090/authentic/login?usercode=jack&password=123 获取token后再次携带token访问
posted @ 2022-07-10 23:28  qtyanan  阅读(1475)  评论(0编辑  收藏  举报