Build a Spring Cloud Gateway from scratch

New Spring Boot project

How to build a new Spring Boot project will not be described here in detail. You can not browse previous blogs or Baidu directly. The corresponding pom file is posted here.

pom depends on the following:

<?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.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lifengdi</groupId>
    <artifactId>gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

<!--        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>-->

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
    </dependencies>

    <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>

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

</project>

Because it is a gateway project, there is no need to rely on spring boot starter web.

The configuration file is as follows:

server:
  port: 8080
spring:
  application:
    name: spring-cloud-gateway-demo
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #Enable routing access
      routes:
        - id: path_route
          # Designated domain name
          uri: http://localhost:8081
          predicates:
            - Path=/jar/**
          filters:
            # Fuse configuration
            - name: Hystrix
              args:
                name: default
                fallbackUri: forward:/fallback
        - id: path_route2
          # Designated domain name
          uri: http://localhost:8082
          predicates:
            - Path=/war/**
          filters:
            # Fuse configuration
            - name: Hystrix
              args:
                name: hystrix1
                fallbackUri: forward:/fallback

  mvc:
    throw-exception-if-no-handler-found: true

# Default fusing timeout 30s
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000
    hystrix1:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000

Fusing (interface or project)

The jar package related to fusing is as follows:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

Default fuse callback interface:

package com.lifengdi.gateway.hystrix;

import com.lifengdi.gateway.exception.BaseException;
import com.lifengdi.gateway.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: Li Fengdi
 * @date: 2020-03-18 16:35
 */
@RestController
@Slf4j
public class DefaultHystrixController {
    @RequestMapping("/fallback")
    public ResponseResult<Object> fallback(){

        log.error("Triggered fuse......");
        return ResponseResult.fail(BaseException.DEFAULT_HYSTRIX.build());
    }
}

The specific configuration file is as follows:

      routes:
        - id: path_route
          # Designated domain name
          uri: http://localhost:8081
          predicates:
            - Path=/jar/**
          filters:
            # Fuse configuration
            - name: Hystrix
              args:
                name: default
                fallbackUri: forward:/fallback
        - id: path_route2
          # Designated domain name
          uri: http://localhost:8082
          predicates:
            - Path=/war/**
          filters:
            # Fuse configuration
            - name: Hystrix
              args:
                name: hystrix1
                fallbackUri: forward:/fallback

  mvc:
    throw-exception-if-no-handler-found: true

# Default fusing timeout 30s
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000
    hystrix1:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000

default and hystrix 1 are user-defined parameters. Multiple fuse policies can be configured. Different interfaces and services can be configured with corresponding timeout separately. No additional development is required, but additional configuration files are needed.

Global session sharing

Dependent jar package:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Related yml configuration:

spring:
  redis:
    database: 0
    host: localhost
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 300
        max-idle: 8
        max-wait: -1ms
        min-idle: 0
  session:
    store-type: redis

spring.session.store-typeSpring is implemented by redis by default. There are other configurations.

Add the code as follows:

Permission is related. All are released by default:

package com.lifengdi.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {
    @Bean
    SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity serverHttpSecurity)
            throws Exception {
        serverHttpSecurity
                .csrf().disable()
                .authorizeExchange().pathMatchers("/**").permitAll()
                .anyExchange()
                .authenticated();
        return serverHttpSecurity.build();
    }
}

session related:

package com.lifengdi.gateway.config;

import com.lifengdi.gateway.resolver.MyCookieWebSessionIdResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseCookie;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
import org.springframework.web.server.session.CookieWebSessionIdResolver;
import org.springframework.web.server.session.WebSessionIdResolver;

import java.util.function.Consumer;

@Configuration
@EnableRedisWebSession(maxInactiveIntervalInSeconds = 10*60*60, redisNamespace = "my:spring:session")
public class WebSessionConfig {

    @Bean
    public WebSessionIdResolver webSessionIdResolver() {
        CookieWebSessionIdResolver resolver = new MyCookieWebSessionIdResolver();
        resolver.setCookieName("SESSIONID");

        Consumer<ResponseCookie.ResponseCookieBuilder> consumer = responseCookieBuilder -> {
            responseCookieBuilder.path("/");
        };
        resolver.addCookieInitializer(consumer);
        return resolver;
    }

}

Note that @ enablererediswebsession annotation is used instead of @ enablereredishttpsession, which is different from zuul.

When zuul is used as the gateway, you can directly use @ enablereredishttpsession in the configuration to share session information through redis

Spring also provides @ enablererediswebsession to support WebFlux.

It is worth mentioning that the internal implementation of these two annotations is not the same, and the configuration that needs to be customized is not the same.

Here, the custom cookie name, path and so on are implemented by the custom webSessionIdResolver, not the cookie serializer. If cookie serializer is used, it will not work for @ enablererediswebsession. It's been a long time before this hole!

The MyCookieWebSessionIdResolver code is as follows:

package com.lifengdi.gateway.resolver;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpCookie;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.session.CookieWebSessionIdResolver;

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

/**
 * Customize the WebSessionId parser to be compatible with {@ link org. Springframework. Session. Data. Redis. Config. Annotation. Web. Http. Enablereredishttpsession}
 * <p>
 * When using enablereredishttpsession, useBase64Encoding in {@ link DefaultCookieSerializer} defaults to true, and base64 is used for the sessionId in the cookie
 * Encryption, but if {@ link org. Springframework. Session. Data. Redis. Config. Annotation. Web. Server. Enablererediswebsession}, the default is
 * The resolver of does not decrypt the sessionId, resulting in incorrect session
 * </p>
 *
 * @author: Li Fengdi
 * @date: 2020/3/16 15:41
 */
@Slf4j
public class MyCookieWebSessionIdResolver extends CookieWebSessionIdResolver {

    @Override
    public List<String> resolveSessionIds(ServerWebExchange exchange) {
        MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
        List<HttpCookie> cookies = cookieMap.get(getCookieName());
        if (cookies == null) {
            return Collections.emptyList();
        }
        return cookies.stream().map(HttpCookie::getValue).map(this::base64Decode).collect(Collectors.toList());
    }

    /**
     * base64 Decode
     *
     * @param base64Value base64Value
     * @return Decoded string
     */
    private String base64Decode(String base64Value) {
        try {
            byte[] decodedCookieBytes = Base64.getDecoder().decode(base64Value);
            return new String(decodedCookieBytes);
        } catch (Exception ex) {
            log.debug("Unable to Base64 decode value: " + base64Value);
            return null;
        }
    }

}

In fact, this code is implemented by referring to the code in cookieSerializer.

If useBase64Encoding is specified as false, that is, sessionId is not encrypted, then this code is not needed.

The code has been uploaded to git. You can go and have a look if you need it.

git code address: https://github.com/lifengdi/spring-cloud-gateway-demo

Original address: https://www.lifengdi.com/archives/article/1776

Tags: Java Spring Session Redis

Posted on Wed, 18 Mar 2020 08:15:42 -0400 by biznickman