微服务开发系列 第十篇:Gateway

总概

A、技术栈

  • 开发语言:Java 1.8
  • 数据库:MySQL、Redis、MongoDB、Elasticsearch
  • 微服务框架:Spring Cloud Alibaba
  • 微服务网关:Spring Cloud Gateway
  • 服务注册和配置中心:Nacos
  • 分布式事务:Seata
  • 链路追踪框架:Sleuth
  • 服务降级与熔断:Sentinel
  • ORM框架:MyBatis-Plus
  • 分布式任务调度平台:XXL-JOB
  • 消息中间件:RocketMQ
  • 分布式锁:Redisson
  • 权限:OAuth2
  • DevOps:Jenkins、Docker、K8S

B、本节实现目标

  • 新建mall-gateway服务,所有请求通过Gateway转发
  • Gateway鉴权token
  • Gateway配置白名单
  • 所有服务swagger通过gateway访问,并提供下列列表选择服务
  • @RestControllerAdvice拦截Controller返回统一格式数据
  • @ControllerAdvice拦截返回统一格式Exception

一、API Gateway

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

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。

  • 存在跨域请求,在一定场景下处理相对复杂。

  • 认证复杂,每个服务都需要独立认证。

  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。

  • 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:

微服务开发系列 第十篇:Gateway_第1张图片

API 网关

二、Spring Cloud Gateway简介

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

由于Spring 5.0支持 Netty,Http2,而Spring Boot 2.0支持Spring 5.0,因此Spring Cloud Gateway支持 Netty和Http2。

补充:
1、Zuul(1.x) 基于 Servlet,使用阻塞 API,它不支持任何长连接 ,如 WebSockets。
2、Zuul(2.x) 基于Netty。
3、Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型,支持 WebSockets,支持限流等新特性。
4、Spring Cloud 已经不再集成 Zuul 2.x 。

三、架构说明

认证服务(mall-auth)负责认证授权,网关服务(mall-gateway)负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

具体服务:

  • [mall-auth]:认证服务,负责对登录用户进行认证授权颁发token。
  • [mall-gateway]:网关服务,负责请求转发和校验认证和鉴权。
  • [mall-member]:受保护的API服务,用户鉴权通过后可以访问该服务,该类服务还有[mall-product]、[mall-search]等等。

四、代码实现

4.1 新建mall-gateway服务

新建mall-gateway服务用户token鉴权、API请求转发

4.2 pom.xml



    4.0.0
    
        mall-pom
        com.ac
        1.0-SNAPSHOT
    

    com.ac
    mall-gateway
    ${mall.version}
    mall-gateway
    网关服务

    
        
            com.ac
            mall-core
            1.0-SNAPSHOT
        

        
            com.ac
            mall-oauth2-module
            1.0-SNAPSHOT
        

        
        
            org.springframework
            spring-webmvc
            test
        

        
            org.springframework.cloud
            spring-cloud-starter-gateway
            4.0.4
        

        
            org.springframework.security.oauth
            spring-security-oauth2
            2.3.4.RELEASE
        

        
            org.springframework.security
            spring-security-oauth2-resource-server
        
    

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    8
                    8
                
            
        
    

4.3 配置路由Route(路由)、白名单

bootstrap-dev.yml

server:
  port: 6001

spring:
  application:
    name: mall-gateway

  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: dev_id
        file-extension: yml
        shared-configs:
          - data-id: common.yml
            group: DEFAULT_GROUP
            refresh: true
      discovery:
        namespace: dev_id

    gateway:
      routes:
        - id: mall-member-route            # 当前路由的标识, 要求唯一
          uri: lb://mall-member            # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
          predicates:
            - Path=/mall-member/**         # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:
            - StripPrefix=1                # 转发之前去掉1层路径

        - id: mall-search-route
          uri: lb://mall-search
          predicates:
            - Path=/mall-search/**
          filters:
            - StripPrefix=1

        - id: mall-product-route
          uri: lb://mall-product
          predicates:
            - Path=/mall-product/**
          filters:
            - StripPrefix=1

        - id: mall-order-route
          uri: lb://mall-order
          predicates:
            - Path=/mall-order/**
          filters:
            - StripPrefix=1
 
#gateway swagger开关
swagger:
  enable: true

#配置白名单路径
mall:
  security:
    ignore:
      urls:
        - "/**/member/list"
        - "/**/redis/**"

重点说明一下配置,- StripPrefix=1 转发之前去掉1层路径,如:127.0.0.1:6001/mall-member/member/264260572479489,去掉第一层路径mall-member,就变成了127.0.0.1:6001/member/264260572479489,会被转发到mall-member服务。

4.4 Application配置@ComponentScan

mall-core服务config包里的WebMvcConfigurer配置类,和mall-gateway服务里排除的spring-webmvc有冲突,因此排除该目录下的配置类

@ComponentScan(
        value = "com.ac.*",
        excludeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.ac.core.config.*")})
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

4.5 swagger配置

4.5.1 配置类

package com.ac.gateway.config;

import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import java.util.ArrayList;
import java.util.List;

@Configuration
@Primary
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
public class GateWaySwaggerConfig implements SwaggerResourcesProvider {
    public static final String API_URI = "/v2/api-docs";
    private final RouteLocator routeLocator;
    private final GatewayProperties gatewayProperties;

    public GateWaySwaggerConfig(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
        this.routeLocator = routeLocator;
        this.gatewayProperties = gatewayProperties;
    }

    @Override
    public List get() {
        List resources = new ArrayList<>();
        List routes = new ArrayList<>();
        //取出gateway的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        //结合配置的route-路径(Path),和route过滤,只获取有效的route节点
        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
                .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
                                predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                        .replace("/**", API_URI)))));

        return resources;
    }

    private SwaggerResource swaggerResource(String name, String location) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion("1.0");
        return swaggerResource;
    }
}

4.5.2 controller类

package com.ac.gateway.controller;

import com.ac.gateway.config.GateWaySwaggerConfig;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;

import javax.annotation.Resource;

/**
 * @author Alan Chen
 * @description 在浏览器中打开gateway的swagger地址时,会将请求自动打到下面API
 * http://127.0.0.1:6001/swagger-ui.html
 * @date 2023/02/22
 */
@ConditionalOnProperty(name = "swagger.enable", havingValue = "true")
@RestController
public class SwaggerController {

    @Resource
    private GateWaySwaggerConfig gateWaySwaggerConfig;

    @GetMapping("/swagger-resources/configuration/security")
    public Mono> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources/configuration/ui")
    public Mono> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK));
    }

    @GetMapping("/swagger-resources")
    public Mono swaggerResources() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }

    @GetMapping("/")
    public Mono swaggerResourcesN() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }

    @GetMapping("/csrf")
    public Mono swaggerResourcesCsrf() {
        return Mono.just((new ResponseEntity<>(gateWaySwaggerConfig.get(), HttpStatus.OK)));
    }
}

在GateWaySwaggerConfig、SwaggerController类上都加上了@ConditionalOnProperty(name = "swagger.enable", havingValue = "true") 注解,该注解表示当swagger.enable配置值为true时,则将当前类初始化为bean。该开关用户关闭生产环境swagger,保证服务安全性。

微服务开发系列 第十篇:Gateway_第2张图片

下拉选择服务

4.6 @RestControllerAdvice拦截Controller返回统一格式数据

该配置类放在mall-core模块

package com.ac.core.config;

import com.ac.core.response.RepResult;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Alan Chen
 * @description Controller返回参数全局包装成ResponseResult对象
 * 使用是一般需要指定basePackages,@RestControllerAdvice(basePackages = {"com.netx.web.controller"})
 * 只拦截controller包下的类;否则swagger也会拦截影响swagger正常使用
 * @date 2023/04/15
 */
@EnableWebMvc
@Configuration
@RestControllerAdvice
public class GlobalReturnConfig implements ResponseBodyAdvice, WebMvcConfigurer {

    /**
     * 支持返回 text/plan 格式  字符串不会带双引号
     *
     * @return
     */
    public boolean supportTextPlan() {
        return false;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class> aClass) {
        //排除swagger的请求 springfox.documentation.swagger2.web.Swagger2Controller
        if (methodParameter.getDeclaringClass().getName().contains("swagger")) {
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object returnObj, MethodParameter methodParameter, MediaType mediaType, Class> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        // 返回值为void
        if (returnObj == null) {
            return RepResult.success();
        }

        //全局异常会拦截统一封装成ResponseResult对象,因此不需要再包装了
        if (returnObj instanceof RepResult) {
            return returnObj;
        }

        return RepResult.success(returnObj);

    }

    /**
     * 解决不能返回单个字符的问题
     *
     * @param converters
     */
    @Override
    public void configureMessageConverters(List> converters) {

        if (supportTextPlan()) {
            converters.add(stringHttpMessageConverter());
        }

        //创建fastJson消息转换器
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();

        // 解决Content-Type cannot contain wildcard type '*'问题
        List supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        supportedMediaTypes.add(MediaType.APPLICATION_PDF);
        supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XML);
        supportedMediaTypes.add(MediaType.IMAGE_GIF);
        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
        supportedMediaTypes.add(MediaType.IMAGE_PNG);
        supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
        supportedMediaTypes.add(MediaType.TEXT_HTML);
        supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
        supportedMediaTypes.add(MediaType.TEXT_XML);
        converter.setSupportedMediaTypes(supportedMediaTypes);

        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        // 字段为null时依然返回到前端,而不是省略该字段
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        converter.setFastJsonConfig(fastJsonConfig);

        converters.add(converter);
    }

    @Bean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        return new StringHttpMessageConverter();
    }
}

微服务开发系列 第十篇:Gateway_第3张图片

查询用户接口

虽然查询用户接口,返回的是一个用户对象,但返回到前端时,统一返回的是RepResult格式,将用户数据放在了data里。

微服务开发系列 第十篇:Gateway_第4张图片

统一返回RepResult格式

4.7 @ControllerAdvice拦截返回统一格式Exception

该配置放在mall-core里

package com.ac.core.exception;

import com.ac.core.i18n.I18nResource;
import com.ac.core.response.RepResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author Alan Chen
 * @description 全局异常处理
 * @date 2023/4/27
 */
@Slf4j
@ControllerAdvice
@Component
public class GlobalExceptionHandler {

    private I18nResource validationI18nSource;

    private I18nResource responseMessageI18nSource;

    /**
     * 是否开启Validator国际化功能
     * @return
     */
    protected boolean enableValidationI18n(){
        return false;
    }

    /**
     * 国际化文件地址
     * @return
     */
    protected String validationI18nSourcePath(){
        return "i18n/validation";
    }

    /**
     * 是否开启消息国际化
     * @return
     */
    protected boolean enableResponseMessageI18n(){
        return false;
    }

    /**
     * 消息国际化文件地址
     * @return
     */
    protected String responseMessageI18nSourcePath(){
        return "i18n/messages";
    }


    /**
     * 全局异常捕捉处理
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public RepResult errorHandler(Exception ex) {
        ex.printStackTrace();
        log.error("Exception:"+ex.getMessage());
        return RepResult.fail(ex.getMessage());
    }

    /**
     * validator校验失败信息处理
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public RepResult bindExceptionHandler(BindException exception) {
        exception.printStackTrace();
        return doValidationException(exception.getBindingResult());
    }

    /**
     * validator校验失败信息处理
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public RepResult validationHandler(MethodArgumentNotValidException exception) {
        exception.printStackTrace();
        log.error("MethodArgumentNotValidException:"+exception.getMessage());
        return doValidationException(exception.getBindingResult());
    }

    /**
     * 拦截捕捉业务异常 ServiceException.class
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = ServerException.class)
    public RepResult commonExceptionHandler(ServerException ex) {

        ex.printStackTrace();
        log.error("ServiceException:"+ex.getMessage());

        if(enableResponseMessageI18n()){
            if(responseMessageI18nSource == null){
                responseMessageI18nSource = new I18nResource(responseMessageI18nSourcePath());
            }
            String messageKey = ex.getMessage();
            try{
                String message = responseMessageI18nSource.getValue(messageKey);
                String[] placeholder = ex.getPlaceholder();
                if(placeholder!=null && placeholder.length>0){
                   for(int i =0;i

微服务开发系列 第十篇:Gateway_第5张图片

统一异常格式

五、token鉴权测试

5.1 鉴权拦截成功

请求gateway访问mall-member服务接口,不携带token,请求被拦截

微服务开发系列 第十篇:Gateway_第6张图片

鉴权拦截成功

5.2 鉴权成功转发请求

请求gateway访问mall-member服务接口,携带合法token,请求被正确转发

微服务开发系列 第十篇:Gateway_第7张图片

鉴权成功转发请求

5.3 白名单

在bootstrap-dev.yml里配置了白名单:

#配置白名单路径
mall:
  security:
    ignore:
      urls:
        - "/**/member/list"
        - "/**/redis/**"

请求gateway访问mall-member服务白名单接口,不携带token,请求被正确转发

微服务开发系列 第十篇:Gateway_第8张图片

访问白名单接口

你可能感兴趣的:(微服务,微服务,gateway,java,mall-gateway服务,Controller)