肥仔教程网

SEO 优化与 Web 开发技术学习分享平台

Spring Cloud 微服务架构&JWT认证&网关&熔断

总结

本文将通过Spring Cloud Alibaba和Spring Cloud相关基础组件,搭建一个分布式服务基础框架,使用nacos作为注册中心,gateway作为入口网关,通过JWT标准完成统一认证,使用sentinel进行流量控制与熔断降级,seata进行分布式事务(seata使用AT场景,也实践了RocketMQ事务消息),redisson进行分布式锁。整个项目对相关分布式技术进行覆盖使用,以达到速通和学习的目的。

下一篇:SpringCloud分布式框架&分布式事务&分布式锁

版本选择

这里边由于要使用到Spring Cloud Alibaba相关组件,故涉及到与Spring Cloud、Springboot的版本配套,由于我这边使用的还是JDK8,故选择的是2021.0.5.0相关版本。这里Alibaba的版本一般会在Spring Cloud版本的基础上再添加一个子版本。

版本地址:版本发布说明-阿里云Spring Cloud Alibaba官网

项目搭建

项目结构

springcloud-demo    // 父项目
-- gateway		// 网关项目
-- open-api		// 服务间开放接口
-- services			// 服务父项目
-- -- order-service		// 订单服务
-- -- product-service		// 商品服务
-- -- user-service				// 用户服务

springcloud-demo父项目pom引入的依赖,这里主要是确定Springboot、SpirngCloud和SpringCloud Alibaba相关版本,保证整个项目的相关版本一致。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.13</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-cloud.version>2021.0.5</spring-cloud.version>
        <spring-cloud.alibaba.version>2021.0.5.0</spring-cloud.alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

services父项目引入各服务相关的依赖

    <dependencies>
        <!-- springboot服务依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- springboot web服务依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springcloud alibaba nacos 依赖,用于服务的注册与发现 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Json序列化工具,按需引用  -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.47</version>
        </dependency>
        <!-- lombok 数据对象、日志工具包,按需引用 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- springcloud alibaba seata 依赖,用于分布式事务 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!-- mybatis plus依赖,用于数据库sql操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.11</version>
        </dependency>
        <!-- druid数据连接池依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.24</version>
        </dependency>
        <!-- mysql连接依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>

open-api项目依赖,这里服务间请求主要是feign客户端,以及负载均衡loadbalancer:

    <dependencies>
        <!-- lombok依赖,用于数据对象set\get方法,日志,按需引入 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- openfeign依赖,用于远程调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- loadbalancer依赖,用于远程调用时的负载均衡策略-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- fastjson依赖,对象序列化,按需依赖>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.47</version>
            <scope>compile</scope>
        </dependency>
        <!-- 工具包,用于hash或是加解密的工具包,按需依赖-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
    </dependencies>

gateway项目依赖:

    <dependencies>
        <!-- springboot-web服务依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- gateway依赖,用于网关业务-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- 负载均衡处理依赖,这里是因为我在gateway里边,使用了webClient进行了远程调用,进而引入, 按需依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- nacos依赖, 注册与发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 工具包,按需依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>
        <!-- 工具包,按需依赖-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.47</version>
            <scope>compile</scope>
        </dependency>
        <!-- 单元测试包,按需依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

服务注册与发现

注册中心

进行nacos下载,下载地址:Release 2.2.0 (Dec 14, 2022) · alibaba/nacos · GitHub,在页面下载压缩包。


下载包之后解压,进入bin目录,在当前目录执行cmd,运行命令“startup.cmd -m standalone”,进行单机下启动nacos。启动成功之后,控制台访问地址http://ip:8848/,账号密码默认都是nacos

服务注册与发现

在services项目下的各个服务模块,在引入nacos的包之后,服务的注册与发现也会非常简单,只需要添加相关注解以及配置项即可。

application.yml配置:

server:
  port: 8000 # 启动端口
spring:
  application:
    name: order-service # 当前服务名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # nacos的地址

application.class类上添加注解

@SpringBootApplication // 启动注解
@EnableDiscoveryClient // 服务注册与发现注解
// 启用feign客户端,以及需要调用的接口扫描路径
@EnableFeignClients(basePackages = {"cn.lonple.api.product.inter"}) 
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Feign客户端接口定义

// 添加feign客户端,以及指定要访问的服务名,该服务名为application.yml中注册的spring.application.name
@FeignClient(value = "product-service")
public interface ProductAPI {
    @GetMapping("/product/detail/{id}")
    ProductInfo getProductDetail(@PathVariable("id") Long id);
}

自定义负载均衡算法

在SpringCloud高版本里,针对feign客户端调用接口时的负载均衡,已经弃用了ribbon改为使用LoadBalancer,其中支持的负载均衡算法为随机、轮询,以及由nocas提供的基于IP的权重算法,其中默认为轮询,支持自定义。

  1. 在application.class上添加负载均衡自定义算法
@LoadBalancerClients({
        @LoadBalancerClient(name = "order-service", configuration = LoadBalanceConfig.class)
})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 定义LoadBalanceConfig类,初始化负载均衡算法类,这里这个类上边不要添加注解。
public class LoadBalanceConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> reactorLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty("loadbalancer.client.name");
        // 初始化自定义负载均衡算法
        return new CustomLoadBalance(
                loadBalancerClientFactory.getLazyProvider(name,
                        ServiceInstanceListSupplier.class), name);
    }
}
  1. 实现自定义负载均衡算法,可以直接参考轮询负载均衡算法类的实现,完全复制过来之后,修改主要的负载逻辑即可。
// 实现ReactorServiceInstanceLoadBalancer接口,完全参考RoundRobinLoadBalancer类的实现
public class CustomLoadBalance implements ReactorServiceInstanceLoadBalancer {
     
    // ...... 省略部分代码
    // 负载均衡逻辑实现方法
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }
            return new EmptyResponse();
        } else {
            // 这里是轮询负载均衡算法逻辑,修改此处进行自定义即可。
            int index = ThreadLocalRandom.current().nextInt(instances.size());
            ServiceInstance instance = (ServiceInstance) instances.get(index);
            return new DefaultResponse(instance);
        }
    }
}

网关

在分布式服务场景下,由于后端服务众多,为了统一对外提供服务而不用暴露所有服务应用,可以使用网关进行代理以及请求转发。在pom文件中引入依赖之后,只需要添加相关注解和配置即可完成。

更多使用可以参考:Spring Cloud Gateway 中文文档

网关配置

  1. application.class添加注解
@SpringBootApplication // 启动服务
@EnableDiscoveryClient // 服务注册与发现
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. application.yml添加路由配置
server:
  port: 7000 # 网关端口
spring:
  application:
    name: gateway # 网关注册名称
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # nacos注册地址,这里都是在默认的命名空间public和分组DEFAULT_GROUP
    gateway:
      routes:
        - id: order # 唯一id
          uri: lb://order-service # 需要路由的服务名,lb表示使用负载均衡算法
          predicates: 
            - Path=/order/**  # 断言,这里只配置了url匹配路基,即/order开头的请求,全部转发给order-service服务
        - id: product
          uri: lb://product-service
          predicates:
            - Path=/product/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/user/**
  1. 网关启动之后,就可以通过访问网络,完成请求转发。

统一认证

在实际项目中,对每一个接口的请求,都会要求进行访问认证,即单点登录之后,处处访问,这里我们通过JWT协议,在网关侧进行校验。

JWT协议

JSON Web Token即一个Json数据格式的web请求token,基于对token的发放与认证,完成单点登录与服务请求放行。JWT分为三个部分,Header(标头)、Payload(载体)和Signature(签名),其中Header和Payload都为JSON数据格式,Signature为Header、Payload进行base64运算之后,进行Hash的结果。整个组装过程如下:

  1. Header部分内容,一般存储数据格式JWT和签名的算法。
{
	"alg" : "SHA-256",
	"type" : "JWT"
}
  1. Payload部分,主要携带一些简单信息,比如当前用户,过期时间等,不可携带隐私信息,因为该部分只是做Base64处理,可以被转换成明文。
{
	"createTime" : 1744704728709,
  "expireTime": 1744704728709,
	"name" : "John Do",
	"admin" : true
}
  1. 签名部分,先将Header和Payload进行base64处理,然后使用“.”进行拼接,在加载盐值(需要严格保管盐值,一旦泄露,token就可以被任意伪造了)进行Hash处理,后续以同样的方式进行签名校验。(这里只是我处理的方式,比较简单,可以自行定义更安全的处理方式)
Signature = sha256Hex(base64(Header) + "." + base64(Payload) + "." + salt)
  1. 用户通过账号密码登录成功之后,返回token,完整格式如下。
base64(Header).base64(Payload).Signature

网关认证

用户完成登录认证之后,后续的请求就会携带token,这时需要在网关侧进行token认证,如果只是简单的JWT认证,在网关侧即可以编码完成验证(需要维护签名的盐值),我这边考虑是将token的生成和验证,都放在了user-service服务,这样的话网关需要发送http请求到用户中心完成token认证。

  1. 首先创建一个全局的拦截器,对所有请求进行拦截处理。由于gateway底层使用的是响应式编程,这里请求远程服务使用RestTemplate会报错,需要使用webClient。
@Component
// 这里实现GlobalFilter,所有请求都会拦截
// Ordered 对拦截器的执行顺序进行调整,很重要
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private UserService userServiceClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        if (uri.getPath().contains("/user/auth")) {
            return chain.filter(exchange);
        } else {
            HttpHeaders headers = request.getHeaders();
            String token = headers.getFirst("token");
            if (token == null || token.isEmpty()) {
                return Result.response(exchange, "请先登录", HttpStatus.UNAUTHORIZED.value());
            } else {
                // 调用用户中心服务,进行token校验
                return userServiceClient.userVerify(token, exchange, chain);
            }
        }
    }

    @Override
    // 这里很重要,这个全局拦截器需要在gateway自身路由的拦截器之前执行,不然通过webClient进行负载均衡请求时,会拿不到远程服务的实例
    public int getOrder() {
        return -1;  
    }

}
  1. 初始化一个WebClient的Builder
@Configuration
public class ClientConfig {

    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
}
  1. 基于WebClient进行远程请求,这里注意WebClient使用的是响应式编程,在获取结果时不可以使用block方法。
    private final WebClient userServiceClient;

    public UserService(WebClient.Builder builder) {
        userServiceClient = builder.baseUrl("http://user-service").build();
    }
    public Mono<Void> userVerify(String token, ServerWebExchange exchange, GatewayFilterChain chain) {
        return userServiceClient
                .get().uri("/user/verify")
                .header("token", token)
                .retrieve() // 获取响应体
                .bodyToMono(new ParameterizedTypeReference<Result<Boolean>>() {
                }).doOnSuccess(response -> {
                    // 记录日志(非阻塞操作)
                    log.info("权限校验结果: {}", response);
                })
                .flatMap(response -> {
                    // 业务逻辑
                    if (response != null && response.getCode() == 200 && response.getData() != null && response.getData()) {
                        return chain.filter(exchange);
                    } else {
                        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                        exchange.getResponse().setComplete();
                        return Result.response(exchange, "校验失败", HttpStatus.UNAUTHORIZED.value());
                    }
                }).onErrorResume(e -> {
                    // 处理异常(如503、超时等)
                    return Result.response(exchange, "校验服务请求失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
                });
    }

限流与熔断

sentinel安装

下载sentinel包:Release v1.8.6 · alibaba/Sentinel · GitHub ,直接下jar包,通过java -jar即可运行,启动之后的访问地址http://127.0.0.1:8080,账号密码都为sentinel。

接口限流与熔断

1.引入依赖包

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2021.0.5.0</version>
        </dependency>
  1. application.yml添加sentinel的配置信息。
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080

3.添加接口资源注释,进行接口注册。

    // 添加@SentinelResource注解
    @SentinelResource(value = "order-detail", fallback = "orderDetailFallback", blockHandler = "orderDetailBlockHandler")
    @GetMapping("/detail/{id}")
    public OrderInfo getOrderDetail(@PathVariable("id") Long id) {
        log.info("accept request : {} , get order : {}", port, id);
        return orderService.getOrderDetail(id);
    }

    public Result<String> orderDetailFallback(Long id) {
        return Result.failed("id: " + id + " fallback");
    }

    public Result<String> orderDetailBlockHandler(Long id, Throwable e) {
        return Result.failed("id: " + id + " block");
    }
  1. 进行一次请求之后,在sentinel控制台可以看到请求接口,针对请求接口可以进行流控和熔断配置。

何以解忧,唯有杜康!

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言