微服务day04

网关

网关路由

快速入门

创建新模块:hm-gateway继承hmall父项目。

引入依赖:引入网关依赖和nacos负载均衡的依赖

XML 复制代码
<?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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.heima</groupId>
        <artifactId>hmall</artifactId>
        <version>1.0.0</version>
    </parent>

    <groupId>com.wmmczk</groupId>
    <artifactId>hm-gateway</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos discovery-->
        <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-loadbalancer</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

编写配置文件:一个模块中有多个collection类就可以写多个判断路径。

server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.21.101:8848
    gateway:
      routes:
        - id: item # 路由规则id,自定义,唯一
          uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
          predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
        - id: cart
          uri: lb://cart-service
          predicates:
            - Path=/carts/**
        - id: user
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**
        - id: trade
          uri: lb://trade-service
          predicates:
            - Path=/orders/**
        - id: pay
          uri: lb://pay-service
          predicates:
            - Path=/pay-orders/**

路由属性

网关登陆校验

自定义过滤器

GlobaFilter自定义过滤器
java 复制代码
//Ordered接口为spring中的排序接口,为核心接口
//GlobalFilter要在NettyRoutingFilter之前,NettyRoutingFilter的Ordered的值为int的最大值,确保其最后执行
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取request
        ServerHttpRequest request = exchange.getRequest();
        //获取header头
        HttpHeaders headers = request.getHeaders();
        System.out.println("headers = " + headers);
        return chain.filter(exchange);//将exchange传给下一个过滤器
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}
GatewayFilter自定义过滤器

登录校验

JWT工具

登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在hm-service中已经有了,我们直接拷贝过来:

具体作用如下:

  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问

  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置

  • SecurityConfig:工具的自动装配

  • JwtTool:JWT工具,其中包含了校验和解析token的功能

  • hmall.jks:秘钥文件

其中AuthPropertiesJwtProperties所需的属性要在application.yaml中配置

hm:
  jwt:
    location: classpath:hmall.jks # 秘钥地址
    alias: hmall # 秘钥别名
    password: hmall123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**

创建自定义拦截器:

java 复制代码
package com.hmall.gateway.Filte;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
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.util.List;

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    //用于读取配置文件中要放行的路径
    private final AuthProperties authProperties;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    private final JwtTool jwtTool;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取requset
        ServerHttpRequest request = exchange.getRequest();
        //获取path进行判断是否需要拦截
        RequestPath path = request.getPath();
        //使工具类进行判断
        if (isPath(path)){
            //放行
            return chain.filter(exchange);
        }
        //获取token
        String token = null;
        HttpHeaders headers = request.getHeaders();
        List<String> list = headers.get("Authorization");
        if (!CollUtil.isEmpty(list)){
            //给token赋值
            token = list.get(0);
        }
        //校验token
        Long userId = null;
        try {
           userId =  jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            //token无效进行拦截
            //获取Response进行编辑
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();//该状态可以使后面的过滤器不在执行,返回错误
        }
        //TODO 传递用户信息
        System.out.println("userId = " + userId);
        return chain.filter(exchange);
    }

    private boolean isPath(RequestPath path) {
        //转为String类型
        String string = path.toString();
        //将配置文件中定义的路径进行遍历
        for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, string)){
                //使用工具类进行匹配 antPathMatcher.match ,匹配成功返回true
                return true;
            }
        }
        return false;
    }

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

网关传递用户信息

java 复制代码
        //TODO 传递用户信息
        String userinfo = userId.toString();
        //exchange提供了修改请求头信息的方法mutate
        ServerWebExchange build = exchange.mutate()
                .request(b -> b.header("user-info",userinfo))
                .build();
        System.out.println("userId = " + userId);
        return chain.filter(build);
java 复制代码
package com.hmall.gateway.Filte;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
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.util.List;

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    //用于读取配置文件中要放行的路径
    private final AuthProperties authProperties;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    private final JwtTool jwtTool;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取requset
        ServerHttpRequest request = exchange.getRequest();
        //获取path进行判断是否需要拦截
        RequestPath path = request.getPath();
        //使工具类进行判断
        if (isPath(path)){
            //放行
            return chain.filter(exchange);
        }
        //获取token
        String token = null;
        HttpHeaders headers = request.getHeaders();
        List<String> list = headers.get("Authorization");
        if (!CollUtil.isEmpty(list)){
            //给token赋值
            token = list.get(0);
        }
        //校验token
        Long userId = null;
        try {
           userId =  jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            //token无效进行拦截
            //获取Response进行编辑
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();//该状态可以使后面的过滤器不在执行,返回错误
        }
        //TODO 传递用户信息
        String userinfo = userId.toString();
        //exchange提供了修改请求头信息的方法mutate
        ServerWebExchange build = exchange.mutate()
                .request(b -> b.header("user-info",userinfo))
                .build();
        System.out.println("userId = " + userId);
        return chain.filter(build);
    }

    private boolean isPath(RequestPath path) {
        //转为String类型
        String string = path.toString();
        //将配置文件中定义的路径进行遍历
        for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, string)){
                //使用工具类进行匹配 antPathMatcher.match ,匹配成功返回true
                return true;
            }
        }
        return false;
    }

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

定义拦截器:

拦截器实现了HandlerInterceptor接口

java 复制代码
package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 拦截器需要进行注册
public class UserINfointerceptor implements HandlerInterceptor {
    //请求到达时执行获取用户信息
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
            UserContext.setUser(Long.valueOf(userInfo));
            System.out.println(userInfo);
        }
        // 3.放行
        return true;
    }

    //业务执行后的操作,删除线程存储中的数据
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

由于拦截器需要配置类进行注册才可以启用,创建一个配置类:

由于网关模块并没有使用springMVC进行编写,所以网关就会报错,因此使用该注解:@ConditionalOnClass(DispatcherServlet.class)//用于判断是否存在某个类,存在就加载,不存在就不加载,使得网关模块不在加载该配置类,避免报错。

java 复制代码
package com.hmall.common.config;

import com.hmall.common.interceptor.UserINfointerceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration//配置类的指定注释
@ConditionalOnClass(DispatcherServlet.class)//用于判断是否存在某个类,存在就加载,不存在就不加载
public class MyUserInfoConfig implements WebMvcConfigurer {
    //将拦截器添加到容器中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserINfointerceptor ());
    }
}

在hm-common模块下的文件 spring.factories 中添加路径,在启动时扫描该配置

添加该代码:

java 复制代码
  com.hmall.common.config.MyUserInfoConfig,\
java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MyUserInfoConfig,\
  com.hmall.common.config.JsonConfig

OpenFeign传递用户

在Api模块中的

复制代码
DefaultFeignConfig配置类中添加拦截器,拦截所有的okhttp请求即所有的微服务之间的请求

使用匿名内部类进行配置

由于需要获取用户信息,添加hm-common的依赖获取UserContext对象。

java 复制代码
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
java 复制代码
@Bean
    public RequestInterceptor MyRequestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long user = UserContext.getUser();
                if (user != null){
                    requestTemplate.header("user-info",user.toString());
                }
            }
        };
    }
java 复制代码
package com.hmall.api.config;

import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor MyRequestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long user = UserContext.getUser();
                if (user != null){
                    requestTemplate.header("user-info",user.toString());
                }
            }
        };
    }
}

微服务的登录解决方案:

配置管理

配置共享

我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:

  • 在Nacos中添加共享配置

  • 微服务拉取配置

添加共享配置:

1、在nacos的配置列表下进行新建共享配置

  • Data ID:表示共享配置文件的名称,即在项目中引入是的名字
  • 使用默认分组
  • 配置内容即要共享的配置,可以使用占位符进行动态的设置配置数据

jdbc的更共享配置:shared-jdbc.yaml ,有数据库设置和mp的配置使用占位符来进行不同的设置。

占位符后面的 :表示默认字符,即没有设置的默认数据

spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.21.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${hm.db.user:root}
    password: ${hm.db.pw:123}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto

日志的共享配置: shared-log.yaml

logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

swagger的共享配置;shared-swagger.yaml

knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:黑马商城接口文档}
    description: ${hm.swagger.description:黑马商城接口文档}
    email: ${hm.swagger.email:zhanghuyi@itcast.cn}
    concat: ${hm.swagger.concat:虎哥}
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${hm.swagger.package}

接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。

不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml

也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?

SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

/2、在项目中引入依赖:

XML 复制代码
  <!--nacos配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>

创建bootstrap.yml

XML 复制代码
spring:
  application:
    name: cart-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.21.101 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置

修改原有的配置文件,为占位符赋值 application.yaml

XML 复制代码
server:
  port: 8082
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
hm:
  swagger:
    title: 购物车服务接口文档
    package: com.hmall.cart.controller
  db:
    database: hm-cart

配置热更新

注意文件的dataId格式:

[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 服务名 :我们是购物车服务,所以是cart-service

  • spring.active.profile :就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置

  • 后缀名:例如yaml

设置nacos配置热更新文件:

名字为:cart-service

XML 复制代码
hm:
  cart:
    maxAmount: 10

在代码中创建配置类:,设置配置文件关联。

创建包: config

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
    private Integer maxAmount;
}

修改业务文件中判断购物车最大数量:

java 复制代码
    private void checkCartsFull(Long userId) {
        int count = Math.toIntExact(lambdaQuery().eq(Cart::getUserId, userId).count());
        if (count >= cartProperties.getMaxAmount()) {
            throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxAmount()));
        }
    }

动态路由

动态路由的相关操作

相关推荐
zfoo-framework6 分钟前
【jenkins插件】
java
风_流沙11 分钟前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
亽仒凣凣19 分钟前
Windows安装Redis图文教程
数据库·windows·redis
亦世凡华、27 分钟前
MySQL--》如何在MySQL中打造高效优化索引
数据库·经验分享·mysql·索引·性能分析
YashanDB30 分钟前
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
数据库·yashandb·崖山数据库
ProtonBase41 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法
suweijie7684 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿5 小时前
List深拷贝后,数据还是被串改
java