gateway

1:实现gateway动态路由

配置文件

server:
  port: 9001
  servlet:
    context-path: /imooc

spring:
  application:
    name: e-commerce-gateway
  cloud:
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 192.168.5.253:8848 # Nacos 服务器地址
        namespace: d8fa475f-a1df-4cd3-acbf-e9d6df0ca59f
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
    # 静态路由,本项目使用的是动态路由
  #    gateway:
  #      routes:
  #        - id: path_route # 路由的ID
  #          uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
  #          predicates: # 断言, 路径相匹配的进行路由
  #            - Path=/user/{id}
    kafka:
      bootstrap-servers: 192.168.5.253:9092
      producer:
        retries: 3
      consumer:
        auto-offset-reset: latest
  #  zipkin:
  #    sender:
  #      type: kafka # 默认是 web
  #    base-url: http://localhost:9411/
  main:
    allow-bean-definition-overriding: true  # 因为将来会引入很多依赖, 难免有重名的 bean

# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:
  gateway:
    route:
      config:
        data-id: e-commerce-gateway-router
        group: e-commerce

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

gateway配置类

/**
 * ProjectName xyc-commerce-springcloud
 * 读取nacos相关的配置项,用户配置监听器
 * @author xieyucan
 * <br>CreateDate 2022/9/5 13:50
 */
@Configuration
public class GatewayConfig {


    /**
     * 获取读取超时时间
     */
    public static final long DEFAULT_TIMEOUT=30000;

    /**
     * 服务器地址
     */
    public static String NACOS_SERVER_ADDR;

    /**
     * 命名空间
     */
    public static String NACOS_NAMESPACE;

    /**
     * data_id
     */
    public static String NACOS_ROUTE_DATA_ID;

    /**
     * nacos分组id
     */
    public static String NACOS_GROUP_ID;


    /**
     * 这里的意思是,将@Value("${spring.cloud.nacos.discovery.server-addr}") 赋值给nacosServerAddr
     * 然后 NACOS_SERVER_ADDR=nacosServerAddr;
     * @param nacosServerAddr
     */
    @Value("${spring.cloud.nacos.discovery.server-addr}")
    public void setNacosServerAddr(String nacosServerAddr)
    {
        NACOS_SERVER_ADDR=nacosServerAddr;
    }


    @Value("${spring.cloud.nacos.discovery.namespace}")
    public void setNacosNamespace(String nacosNamespace)
    {
        NACOS_NAMESPACE=nacosNamespace;
    }


    @Value("${nacos.gateway.route.config.group}")
    public void setNacosGroupId(String groupId)
    {
        NACOS_GROUP_ID=groupId;
    }


    @Value("${nacos.gateway.route.config.data-id}")
    public void setNacosRouteDataId(String dataId)
    {
        NACOS_ROUTE_DATA_ID=dataId;
    }
}

实现事件推送aware:动态更新路由网关 Service

/**
 * ProjectName xyc-commerce-springcloud
 * 实现事件推送aware:动态更新路由网关 Service
 * @author xieyucan
 * <br>CreateDate 2022/9/5 14:05
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    /**
     * 写路由定义 (gateway提供的bean)
     */
    private final RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 获取路由定义 (gateway提供的bean)
     */
    private final RouteDefinitionLocator routeDefinitionLocator;

    /**
     * 事件发布对象
     */
    private ApplicationEventPublisher publisher;

    public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter, 
    RouteDefinitionLocator routeDefinitionLocator) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator = routeDefinitionLocator;
    }

    @Override
    public void setApplicationEventPublisher(
    ApplicationEventPublisher applicationEventPublisher) {
        //完成事件推送句柄初始化
        this.publisher=applicationEventPublisher;
    }


    /**
     * 增加路由定义
     * @param routeDefinition
     * @return
     */
    public String addRouteDefinition(RouteDefinition routeDefinition)
    {
        log.info("gateway add route:[{}]",routeDefinition);
        //保存路由配置并且发布出去
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        //发布事件通知给gateway,同步新增的路由定义
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }

    /**
     * 更新多个路由配置
     * @param definitionList
     * @return
     */
    public String updateList(List<RouteDefinition> definitionList)
    {
        log.info("gateway update route:[{}]",definitionList);
        //拿到当前gateway中存储的定义
        List<RouteDefinition> routeDefinitionsExits =
                routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        if(!CollectionUtil.isEmpty(routeDefinitionsExits))
        {
            //清除掉之前所有旧的路由定义
            routeDefinitionsExits.forEach(rd->{
                log.info("delete route definition:[{}]",rd);
                deleteById(rd.getId());
            });
        }
        //把更新的路由定义同步到gateway中
        definitionList.forEach(definition->updateByRouteDefinition(definition));
        return "success";
    }

    /**
     * 根据路由id去删除路由配置
     * @param id
     * @return
     */
    private String deleteById(String id)
    {
        try {
            log.info("gateway delete route id:[{}]",id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));

            return "delete success";
        }catch (Exception ex){
            log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
            return "delete fail";
        }
    }


    /**
     * 更新路由
     * @param routeDefinition
     * 更新的实现策略:删除+新增=更新
     * @return
     */
    private String updateByRouteDefinition(RouteDefinition routeDefinition)
    {
        try {
            log.info("gateway update route:[{}]",routeDefinition);
            this.routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()));
        }catch (Exception ex){
            return "update fail,not find routed"+routeDefinition.getId();
        }

        try {
            this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));

            return "success";
        }catch (Exception ex){
            return "update route fail";
        }
    }
}

通过nacos下发动态路由配置,监听nacos中路由配置变更

/**
 * ProjectName xyc-commerce-springcloud
 * 通过nacos下发动态路由配置,监听nacos中路由配置变更
 * @author xieyucan
 * <br>CreateDate 2022/9/5 15:02
 */

@Slf4j
@Component
@DependsOn({"gatewayConfig"})
public class DynamicRouteServiceImplByNacos {

    /**
     * nacos 配置服务
     */
    private ConfigService configService;

    private final DynamicRouteServiceImpl dynamicRouteService;

    public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
        this.dynamicRouteService = dynamicRouteService;
    }


    /**
     * bean 在容器中构造完成之后会执行init方法
     */
    @PostConstruct
    public void init()
    {
        log.info("gateway route init");

        try {
            //初始化nacos配置客户端
            configService=initConfigService();
            if(configService==null)
            {
                log.error("init config service fail");
                return;
            }
            //通过nacos config并且知道路由配置路径去获取路由配置
            String configInfo=configService.getConfig(GatewayConfig.NACOS_ROUTE_DATA_ID,
                    GatewayConfig.NACOS_GROUP_ID,
                    GatewayConfig.DEFAULT_TIMEOUT);
            log.info("get current gateway config:[{}]",configInfo);
            List<RouteDefinition> routeDefinitions = 
            JSON.parseArray(configInfo, RouteDefinition.class);
            if(CollectionUtil.isNotEmpty(routeDefinitions))
            {
                for(RouteDefinition routeDefinition:routeDefinitions)
                {
                    log.info("init gateway config:[{}]",routeDefinition.toString());
                    dynamicRouteService.addRouteDefinition(routeDefinition);
                }
            }
        }catch (Exception e){
            log.error("gateway route init has some error:[{}]",e.getMessage(),e);
        }
        //添加一个监听器
        dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
        GatewayConfig.NACOS_GROUP_ID);
    }

    /**
     * 初始化nacosconfig,初始化之后才可以使用
     * @return
     */
    private ConfigService initConfigService()
    {
        try {
            Properties properties = new Properties();
            properties.setProperty("serverAddr",GatewayConfig.NACOS_SERVER_ADDR);
            properties.setProperty("namespace",GatewayConfig.NACOS_NAMESPACE);
            return configService= NacosFactory.createConfigService(properties);
        } catch (Exception e) {
            log.error("init gateway nacos config error:[{}]",e.getMessage(),e);
            return null;
        }
    }

    /**
     * 实现对nacos的监听,nacos下发的动态路由信息
     * @param dataId
     * @param group
     */
    private void dynamicRouteByNacosListener(String dataId,String group)
    {
        try {

            //给nacos config 客户端增加一个监听器
            configService.addListener(dataId, group, new Listener() {
                /**
                 * 提供线程池执行操作  return null;表示默认线程池
                 * @return
                 */
                @Override
                public Executor getExecutor() {
                    return null;
                }

                /**
                 * 监听器受到配置中心的配置更新
                 * nacos 中最新的配置定义
                 * @param configInfo  配置本来就是String类型,所以,我们需要将其转换为数组对象
                 */
                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("start to update config:[{}]",configInfo);
                    List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, 
                    RouteDefinition.class);
                    log.info("update route:[{}]",routeDefinitions.toString());
                    dynamicRouteService.updateList(routeDefinitions);
                }
            });
        }catch (NacosException ex){
            log.error("dynamic update gateway config error:[{}]",ex.getErrCode(),ex);
        }
    }
}

2:gateway路由配置(局部过滤器)

[
  {
    "id": "e-commerce-nacos-client",
    "predicates": [
      {
        "args": {
          "pattern": "/imooc/ecommerce-nacos-client/**"
        },
        "name": "Path"
      }
    ],
    "uri": "lb://e-commerce-nacos-client",
     //局部过滤器的用法,也就是需要在网关中声明
    "filters": [
      {
        "name": "HeaderToken"
      },
      {
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
    ]
  },
  {
  "id": "e-commerce-account-service",
  "predicates": [
    {
      "args": {
        "pattern": "/imooc/ecommerce-account-service/**"
      },
      "name": "Path"
    }
  ],
  "uri": "lb://e-commerce-account-service",
  "filters": [
    {
      "name": "StripPrefix",
      "args": {
        "parts": "1"
      }
    }
  ]
},
  {
    "id": "e-commerce-goods-service",
    "predicates": [
      {
        "args": {
          "pattern": "/imooc/ecommerce-goods-service/**"
        },
        "name": "Path"
      }
    ],
    "uri": "lb://e-commerce-goods-service",
    "filters": [
      {
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
    ]
  }
]

3:全局过滤器和局部过滤器

局部过滤器的使用(需要在网关配置类中声明,详细看第二点)

局部过滤器

/**
 * ProjectName xyc-commerce-springcloud
 * 请求头部携带token验证过滤器
 * @author xieyucan
 * <br>CreateDate 2022/9/6 9:08
 */
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //从http header中寻找key为token,value为imooc的键值对  (这里采用两个token,第一个为假的伪造token)
        String name=exchange.getRequest().getHeaders().getFirst("token");
        if("imooc".equals(name))
        {
           return chain.filter(exchange);
        }
        //标记此次请求没有权限,并结束这次请求
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE+2;
    }
}

局部过滤器工厂

/**
 * ProjectName xyc-commerce-springcloud
 * 让局部过滤器生效
 * 实现局部过滤器步骤:
 * 1:局部过滤器要实现 GatewayFilter, Ordered 相应方法
 * 2:实现抽象过滤器工厂
 * 3:在配置文件中配置  写GatewayFilterFactory 前面的即可,
 * 也就是 HeaderToken(不配置表示当前局部过滤器不生效)
 * @author xieyucan
 * <br>CreateDate 2022/9/6 9:22
 */
@Component
public class HeaderTokenGatewayFilterFactory 
    extends AbstractGatewayFilterFactory<Object> {

    @Override
    public GatewayFilter apply(Object config) {
        return new HeaderTokenGatewayFilter();
    }
}

全局过滤器(实现GlobalFilter, Ordered接口声明为@Component即可)

/**
 * ProjectName xyc-commerce-springcloud
 * 全局登录鉴权过滤器
 * @author xieyucan
 * <br>CreateDate 2022/9/6 10:26
 */
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {

    /**
     * 注册中心客户端,可以从注册中心中获取服务实例信息
     */
    private final LoadBalancerClient loadBalancerClient;

    private final RestTemplate restTemplate;

    public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient
                                       , RestTemplate restTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
    }

    /**
     * 登录、注册、鉴权
     * 1:如果是登录注册,则去授权中心拿token,返回给客户端
     * 2:如果是服务其他服务,则鉴权,没有权限返回401
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 如果是登录
        if(request.getURI().getPath().contains(GatewayConstant.LOGIN_URI))
        {
            //去授权中心获取token
            String token = getTokenAuthorityCenter(request, 
                           GatewayConstant.AUTHORITY_CENTER_TOKEN_FORMAT);

            //header 中不能设置为null
            response.getHeaders().
                add(CommonConstant.JWT_USER_INFO_KEY,token==null?"null":token);
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }
        //如果是注册
        if(request.getURI().getPath().contains(GatewayConstant.REGISTER_URI))
        {
            //去授权中心拿token,先创建用户,再返回token
            String token = getTokenAuthorityCenter(request, 
            GatewayConstant.AUTHORITY_CENTER_REGISTER_FORMAT);
            response.getHeaders()
                .add(CommonConstant.JWT_USER_INFO_KEY,token==null?"null":token);
            response.setStatusCode(HttpStatus.OK);
        }


        //访问其他服务,则鉴权、校验是否能够从token中解析用户信息
        String token=request.getHeaders().getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo=null;
        try {
            //解析token
            loginUserInfo= TokenParseUtil.parseUserInfoFromToken(token);
        }catch (Exception ex){
            log.error("parse user info from token error:[{}]",ex.getMessage(),ex);
        }
        //从token获取不到用户登录信息 返回401
        if(loginUserInfo==null)
        {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //解析通过则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE+2;
    }


    /**
     * 从post请求中获取到请求数据
     * @param request
     * @return
     */
    private String parseBodyFromRequest(ServerHttpRequest request)
    {
        //获取请求头
        Flux<DataBuffer> body=request.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();

        //订阅缓冲区去消费请求体中的数据
        body.subscribe(buffer->{
            CharBuffer charBuffer = 
                StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            //释放之前缓存的数据,否则会出现内存泄漏
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });
        //获取 request body
        return bodyRef.get();
    }


    /**
     * 从授权中心获取token
     * @param request
     * @param uriFormat
     * @return
     */
    private String getTokenAuthorityCenter(ServerHttpRequest request,String uriFormat)
    {
        /**
         * service id就是服务名称,这里实现了负载均衡
         */
        ServiceInstance instance = loadBalancerClient
            .choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
        log.info("nacos client info:[{}],[{}],[{]]",
                instance.getServiceId(),
                instance.getInstanceId(),
                instance.getMetadata());

        String requestUrl=String.format(uriFormat,instance.getHost()
                                        ,instance.getPort());

        UsernameAndPassword usernameAndPassword=JSON.parseObject(
            parseBodyFromRequest(request),
            UsernameAndPassword.class);

        log.info("login request url and body:[{}],[{}]",requestUrl,
                 JSON.toJSONString(usernameAndPassword));

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(usernameAndPassword), httpHeaders),
                JwtToken.class);
        if(null!=token)
        {
            return token.getToken();
        }
        return null;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值