创建项目

通过spring initializr创建spring boot项目,maven配置如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>top.wteng</groupId>
    <artifactId>MyGateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>MyGateway</name>
    <description>My Gateway Server</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
        <spring-nacos.discovery.version>2021.1</spring-nacos.discovery.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>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- 负载均衡 -->
        <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>
            <version>${spring-nacos.discovery.version}</version>
        </dependency>
        <!-- 自定义配置处理 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
       
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

集成Nacos

Spring Gateway本身集成nacos非常简单,只需在配置文件中将nacos配置加进去即可,无需任何其它配置,如下:

server:
  port: 8080
spring:
  application:
    name: MyGateway
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost:8848} # nacos地址
    gateway:
      enabled: true # 开启网关服务
      discovery: # 服务发现配置
        locator:
          enabled: true # 自动依据nacos中的服务id生成路由
          lower-case-service-id: true # 小写服务id

直接启动工程即可,网关会自动获取nacos中注册的所有服务,并生成路由进行负载均衡调用。如nacos中存在服务iduser-api的接口服务,其地址为192.168.1.20:3000,则访问网关http://localhost:8080/user-api/**会自动将请求转发至http://192.168.1.20:3000/**,即网关会以第一层路径(user-api)作为服务id去匹配相应服务,匹配到时将第一层路径截取掉,转发至相应服务。

要注意的是,若同时配置了routes,则routes的优先级低于自动路由,如若有以下配置:

spring:
  application:
    name: MyGateway
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost:8848}
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes: # 路由配置
      - id: user-api # 路由id
        uri: lb://user-api # 依据微服务进行负载均衡
        predicates: # 断言规则
        - Path=/user-api/** # 对以user-api开头的路径进行处理转发
        filters: # 过滤器配置
        - StripPrefix=1 # 转发时去掉第一次路径
        - OtherFilter # 其它过滤器

此时locator设成了true同时配置了具体routes,则routes中的user-api配置不会生效,因为其优先级低于自动生成规则,因而filters中配置的过滤器也不会走到。因此想要手动定制路由、配置一些过滤器等内容,要将自动发现并生成路由设为false

跨域配置

配置跨域时,首先自定义一些配置,包括允许的HeaderMethodOrigin,配置示例如下:

customize:
  web:
    cors:
      allowedOrigins:
        - http://localhost:8080
        - http://127.0.0.1:8080
      allowedHeaders:
        - "*"
      allowedMethods:
        - GET
        - POST
        - PUT
        - DELETE
        - OPTIONS

对应的Java配置类如下:

package top.wteng.gateway.properties;

import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "customize.web.cors") // 配置前缀
public class CorsCustomizedProperties {
    private final Logger logger = LoggerFactory.getLogger(CorsCustomizedProperties.class);

    private List<String> allowedOrigins = Collections.emptyList();
    private List<String> allowedHeaders = Collections.emptyList();
    private List<String> allowedMethods = Collections.emptyList();

    public List<String> getAllowedOrigins() {
        return this.allowedOrigins;
    }

    public void setAllowedOrigins(List<String> allowedOrigins) {
        this.allowedOrigins = allowedOrigins;
    }

    public List<String> getAllowedHeaders() {
        return this.allowedHeaders;
    }

    public void setAllowedHeaders(List<String> allowedHeaders) {
        this.allowedHeaders = allowedHeaders;
    }

    public List<String> getAllowedMethods() {
        return this.allowedMethods;
    }

    public void setAllowedMethods(List<String> allowedMethods) {
        this.allowedMethods = allowedMethods;
    }

    @Override
    public String toString() {
        return "{" +
            " allowedOrigins='" + getAllowedOrigins() + "'" +
            ", allowedHeaders='" + getAllowedHeaders() + "'" +
            ", allowedMethods='" + getAllowedMethods() + "'" +
            "}";
    }
}

然后编写跨域配置的过滤器bean

package top.wteng.gateway.configuration;

import java.util.HashSet;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
import org.springframework.web.cors.reactive.CorsWebFilter;

import top.wteng.gateway.properties.CorsCustomizedProperties;

@Configuration
public class CorsFilterConfiguration {
    @Autowired private CorsCustomizedProperties corsProperties;

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        new HashSet<>(corsProperties.getAllowedHeaders()).forEach(configuration::addAllowedHeader);
        new HashSet<>(corsProperties.getAllowedMethods()).forEach(configuration::addAllowedMethod);
        new HashSet<>(corsProperties.getAllowedOrigins()).forEach(configuration::addAllowedOrigin);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        // 注册到所有路径
        source.registerCorsConfiguration("/**", configuration);
        return new CorsWebFilter(source);
    }
}

此时若启动程序,会发现严格跨域下任然会有跨域错误,因为默认跨域列表里会包含*,在严格跨模式下,一个请求源可能会匹配到其具体请求源以及*两个内容,此时跨域也不会允许,为解决此问题,需要将网关默认添加的一些请求头去掉,在配置文件gateway下加入:

gateway:
  filter:
    remove-hop-by-hop:
      headers:
        - trailer
        - te
        - keep-alive
        - transfer-encoding
        - upgrade
        - proxy-authenticate
        - connection
        - proxy-authorization
        - x-application-context
        - access-control-allow-credentials
        - access-control-allow-headers
        - access-control-allow-methods
        - access-control-allow-origin
        - access-control-max-age
        - vary

完整配置如下所示:

server:
  port: 8080
spring:
  application:
    name: MyGateway
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost:8848} # nacos地址
    gateway:
      filter:
        remove-hop-by-hop:
          headers:
            - trailer
            - te
            - keep-alive
            - transfer-encoding
            - upgrade
            - proxy-authenticate
            - connection
            - proxy-authorization
            - x-application-context
            - access-control-allow-credentials
            - access-control-allow-headers
            - access-control-allow-methods
            - access-control-allow-origin
            - access-control-max-age
            - vary
      enabled: true # 开启网关服务
      discovery: # 服务发现配置
        locator:
          enabled: true # 自动依据nacos中的服务id生成路由
          lower-case-service-id: true # 小写服务id
customize:
  web:
    cors:
      allowedOrigins:
        - http://localhost:8080
        - http://127.0.0.1:8080
      allowedHeaders:
        - "*"
      allowedMethods:
        - GET
        - POST
        - PUT
        - DELETE
        - OPTIONS

全局异常处理

网关默认的异常返回格式为HTML渲染,对于前端接口调用,接受的应为JSON格式,可以通过设置全局异常处理,来改变网关异常返回的默认格式。

这里处理的异常为网关级的异常,也即网关直接抛出的异常,如请求的路由不存在或网关有内部错误,对于网关代理的下层服务返回的异常,不会进行处理,如下层服务返回了404,对网关来讲,这个请求是没问题的,网关不会作为异常去捕获处理,因为网关只是转发,且下层服务返回的异常应该是已经经过下层服务本身依据自身需求处理过的了,无需网关再处理。

要实现返回统一JSON异常,只需实现ErrorWebExceptionHandler,更改请求响应的返回行为即可,首先定义返回格式:

package top.wteng.gateway.util;

import org.springframework.http.HttpStatus;

public static class ErrorResult {
    private int statusCode; // 数字状态码
    private String message; // 信息
    private HttpStatus statusError; // 枚举状态码

    public ErrorResult() {}
    public ErrorResult(int statusCode, String message, HttpStatus statusError) {
        this.statusCode = statusCode;
        this.message = message;
        this.statusError = statusError;
    }

    public void setStatusCode(int code) {
        this.statusCode = code;
    }
    public int getStatusCode() {
        return this.statusCode;
    }

    public void setMessage(String message) {
        this.message = message;
    }
    public String getMessage() {
        return this.message;
    }

    public void setError(HttpStatus statusError) {
        this.statusError = statusError;
    }
    public HttpStatus getError() {
        return this.statusError;
    }
}

然后实现配置接口:

package top.wteng.gateway.configuration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import top.wteng.gateway.gateway.util.ErrorResult;

import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

@Order(-1) // 保证优先级低于默认的ResponseStatusExceptionHandler,这样能拿到响应状态码
@Configuration
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        }
        // 将返回格式设为JSON
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        if (ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex).getStatus());
        }
        // 改变请求响应返回行为
        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            ErrorResult errorResult = new ErrorResult(response.getStatusCode().value(), ex.getMessage(), response.getStatusCode());
            try {
                // 返回ErrorResult
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResult));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
                return bufferFactory.wrap(new byte[0]);
            }
        }));
    }
    
}