SpringCloud service gateway quickly landed in practice

Application of service gateway in microservice

Foreign service challenges


The application system of microservice architecture is very large. The basic components that need to be deployed independently include registration center, configuration center and service bus, Turbine exception aggregation and monitoring disk, call chain tracking and link aggregation, message intermediaries such as kafka and MQ, and microservice components and modules split according to business domain, A small system can easily produce more than 20 modules with so many deployment packages.

We used the method of localhost plus port for direct access. What should we do if these services are provided to external users? Developers configure URLs and port numbers for different requests on each page, but a lot of URLs are configured in our front end. We need developers to manually configure a set of routing tables. Therefore, we need to introduce a mechanism to reduce the maintenance cost of routing tables.

Another problem is security, which requires security verification. If the interfaces that do not provide external services are implemented by themselves, it will be a waste of resources. At this time, a middleware is needed to carry out security processing and external data security verification.

How can we provide external services, manage routing rules and do access control well? Under this background, API gateway came into being. It acts like a messenger room to receive all visiting requests.

1, Application of service gateway in microservice

1. Difficulties in external services

In the computer field, there is a design theory: any problem can be solved by introducing a middleware. If one is not enough, two

When we go to other companies, the first pass is the reception room / front desk. They can do two things

  • Access control: see if you have permission to access and refuse unauthorized visitors
  • Guide and guide: ask what you want to do, guide you how to get there, and find the content you want to visit
  • As the only external service, the gateway layer does not directly access the service layer. The gateway layer undertakes all HTTP requests. In practical applications, we will also use the gateway service together with Nginx

2. Access control

Access control should include two aspects. The specific implementation is not provided by the gateway layer, but the gateway carries two tasks as a carrier

  • Intercept request: some interfaces need to log in to the user's scope. For such interfaces, the gateway can check whether the access request carries identity information such as token, such as Authorization or token attribute in HTTP Header. If no token is carried, it indicates that there is no login. In this case, 403 can be returned
  • Authentication: for services with tokens, we need to verify whether the tokens are true or false. Otherwise, users can communicate by forging tokens. Services with expired or invalid tokens should be rejected

3. Routing rules

Routing rules include two aspects: URL mapping and service addressing

  • URL mapping: in most cases, the HTTP URL accessed by the client is often not the real path configured in conroller. For example, the client can initiate a request / password/update to modify the password, but there is no such service in the background. At this time, the gateway layer needs to make a routing rule to access the real service path of URL mapping
  • Service addressing: after the URL is mapped, the gateway layer needs to find the server address that can provide services. For service clusters, it also needs to implement the load balancing strategy (in spring cloud, the gateway implements service addressing with the help of eureka service discovery mechanism, and the Ribbon on which load balancing depends)

2, Second generation Gateway

1. Label of gateway

Label for Gateway

  • Gateway is the official push component of spring
  • The bottom layer is built based on Netty
  • Directly contributed by the spring open source community

2. What can gateway do

What can Gateway do

  • Routing addressing
  • load balancing
  • Current limiting
  • authentication

Gateway VS Zuul

#GatewayZuul 1.xZuul 2.x
ReliabilityOfficial endorsement supportFounder, once reliableIt's been skipping tickets and finally released
performanceNettySynchronization blocking, slow performanceNetty
QPSOver 30000About 2000020000-30000
Spring CloudIntegrated into component libraryIntegrated into component libraryThere is no plan to integrate into the component library, but it can be referenced
Long connectionsupportI won't support itsupport
Programming experienceSlightly complexSynchronization model, relatively simpleSlightly complex
Commissioning & Link TrackingAsynchronous model, slightly complexSynchronization mode, relatively easyAsynchronous model, slightly complex

To sum up, Gateway is the decisive choice for the new project

3, Fast implementation experience of Gateway

  • Create a gateway project
  • Connection Eureka automatically creates routing rules based on service discovery
  • Realize dynamic routing function through Actuator

Create project and add POM dependency

  <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- It is not available at present. It will be used for current limiting later -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
    </dependencies>

Create application startup class

package com.icodingedu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServerApplication {

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

Create profile application

spring: 
  application:
    name: gateway-server
  cloud: 
    gateway: 
      discovery: 
        locator:
          enabled: true
server:
  port: 50080
    
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10080/eureka/

management:
  security:
    enabled: false
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

Start Eureka server, feign client (start two), and gateway server

Can pass http://localhost:50080/actuator/gateway/routes Check the routes rule. After opening it, you will find two service nodes registered according to eureka: FEIGN-CLIENT and GATEWAY-SERVER

Each node has an assertion: predicate and a filter: filters

Now you can access the service through the gateway: http://localhost:50080/FEIGN -The client / say hello accesses the service registered with eureka, and if the service has multiple nodes, it will poll for access

Note that FEIGN-CLIENT here must be capitalized at present: if it is lowercase, it will be 404. It is accessed according to the service name in eureka

If the gateway access does not want to use uppercase, you can modify the configuration of yaml to support lowercase access

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true #The routing service name can be all lowercase, but uppercase is not supported after setting
          
# http://localhost:50080/feign-client/sayhello 

Gateway supports the creation of dynamic routing rules

# POST dynamically creates and modifies routing rules
# Create address: http://localhost:50080/actuator/gateway/routes/myrouter
{
    "predicates": [
        {
            "name": "Path",
            "args": {
                "_genkey_0": "/myrouter-path/**"
            }
        }
    ],
    "filters": [
        {
            "name": "StripPrefix",
            "args": {
                "_genkey_0": "1"
            }
        }
    ],
    "uri": "lb://FEIGN-CLIENT",
    "order": 0
}
# You can see whether the new routing rule is created successfully
# http://localhost:50080/actuator/gateway/routes
# You can delete routing rules
# DELETE http://localhost:50080/actuator/gateway/routes/myrouter

# You can access the routing rules we created
# http://localhost:50080/myrouter-path/sayhello

4, Detailed explanation of routing function

1. Routing structure

Many routes can be defined in the Gateway. A Route is a set of routes containing complete forwarding rules, which is mainly composed of three parts

  • Assertion set: assertion is the first step of router processing. It is the matching rule of routing. It determines whether a network request can be matched to the current path for processing. The reason why it is a set is that we can add multiple assertions to a route. When each assertion is configured successfully, it is considered that we have passed the routing pass
  • Filter set: if the request passes the match asserted above, it indicates that it has been formally taken over by the route, and the end needs to pass the filter, such as permission verification. If the verification fails, it is set to Status Code 403 and the operation is interrupted
  • Uri: if the request passes through the filter successfully, it will go to the last step, that is, forwarding the request (URI is the uniform resource identifier)

2. Load balancing

For the last step of addressing, if Eureka based service discovery mechanism is adopted, the name after service registration can be used for access in the forwarding process of miscellaneous Gateway, and the background will realize load balancing with the help of Ribbon (specific load balancing strategy can be specified for a service). The configuration method is as follows: lb://FEIGN-SERVER, lb represents Ribbon as LoadBalancer

3. Routing workflow

  • Predicate Handler (assertion): first obtain all routes (the complete set of configured routes), and then cycle each Route in turn to match the request with all assertions configured in the Route. If all assertions of the current Route pass the verification, the Predicate Handler selects the current Route. This mode is a typical chain of responsibility
  • Filter Handler; After the Route is selected in the previous step, not only the filter defined in the current Route will take effect, but also the Global Filter we added in the project will participate in the specific processing process. Pre Filter and Post Filter refer to the action stage of the filter
  • Addressing: this step will forward the request to the address specified by the URI. Before sending the request, all Pre type filters will be executed, and the Post filter will work after the call request returns

5, Detailed explanation of assertion function

1. Predict mechanism

Predict is a new function introduced in Java 8. It is similar to the Assertion when we write unit tests. Predict receives a judgment condition, returns a boolean result of true or false, and tells the caller to send the judgment result. You can also use the and, or, and negative operators to connect multiple predicates in series to make a common judgment

Predict is actually the data code we connect with the Gateway. For example, it is required that your Request must have a specified parameter called name, and the corresponding value must be a specified person name (Gavin). If the Request does not contain name or the name is not Gavin, the assertion fails, and it will pass only when the mark and value are the same

2. Action stage of assertion

After a request arrives at the gateway layer, it must first perform assertion matching, and will enter the Filter stage only after all assertions are met

3. Introduction to common assertions

Gateway provides more than ten built-in assertions and introduces some common assertions

4. Path assertion

Path assertion is the most commonly used assertion request, which is used for almost all routes

.route(r -> r.path("/gateway/**")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
.route(r -> r.path("/baidu")
						 .uri("http://www.baidu.com")
)

The use of Path assertion is very simple, just like the way we configure @ RequestPath in the Controller. Fill in a URL matching rule in the Path assertion. When the actual requested URL matches the rule in the assertion, it will be sent to the URI specified address in the route. This address can be a specific HTTP address or a service name registered in Eureka, Routing rules can write multiple binding relationships at a time

5.Method assertion

This assertion specifically validates the HTTP Method

.route(r -> r.path("/gateway/**")
						 .and().method(HttpMethod.GET)
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

Associate the Path assertion with the method through an and connector. If we access / gateway/sample and the method is GET, the above routing rules will be adapted

6.RequestParam matching

Request assertion is often used in business. It will query the specified attributes from the Parameters list in ServerHttpRequest,

.route(r -> r.path("/gateway/**")
						 .and().method(HttpMethod.GET)
						 .and().query("name","test")
						 .and().query("age")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
  • Property name verification, such as query("age"), at this time, the assertion will only verify whether a property called age is included in the QueryParameters list, not its value
  • Attribute value verification, such as query("name", "test"), will not only verify whether the name attribute exists, but also whether its value matches the assertion. The current assertion will not verify whether the attribute value of name in the parameter is test

6.Header assertion

Header assertion is to check whether the header information carries relevant attributes or tokens

.route(r -> r.path("/gateway/**")
						 .and().header("Authorization")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

7. Cookie assertion

Cookie validates the information stored in the cookie. Cookie assertion is similar to several assertions described above. The only difference is that it must be verified together with the attribute value. It cannot only verify whether the attribute exists alone

.route(r -> r.path("/gateway/**")
						 .and().cookie("name","test")
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

8. Time slice verification

There are three modes of time matching: Before, After and Between. These assertions specify the time range within which the route will take effect

.route(r -> r.path("/gateway/**")
						 .and().before(ZonedDateTime.now().plusMinutes(1))
						 .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

6, Configuration for implementing assertions

1. Configure in yaml

Go to the yaml configuration file of the gateway server project for configuration

# The new configuration routes and discovery are of the same level
# id is the unique identifier of this assertion
# The uri is where the request will be forwarded if all the assertions match
# StripPrefix is equivalent to replacing localhost:50080/gavinrouter/sayhello with feign client / sayhello
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
      - id: feignclient
        uri: lb://FEIGN-CLIENT
        predicates:
        - Path=/gavinrouter/**
        filters:
        - StripPrefix=1

After configuration, you can access it through the following path

http://localhost:50080/gavinrouter/sayhello

2. Configure in Java program

Create a config package and create GatewayConfiguration in it

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/gatewatjava/**")
                             .and().method(HttpMethod.GET)
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}
Access verification after modification: http://localhost:50080/gatewatjava/sayhello

7, After assertion implements the second kill of the gateway layer

The gateway calls the feign client business. We need to create a controller in feign client to realize the corresponding functions

The product s used in this need to be defined in feign client INTF in advance

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Product {

    private Long productId;
    private String description;
    private Long stock;
}

Create a GatewayController in feign client

import com.icodingedu.springcloud.pojo.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@Slf4j
@RequestMapping("gateway")
public class GatewayController {

    //The Product should be defined in feign client INTF in advance
    public static final Map<Long, Product> items = new ConcurrentHashMap<>();

    @GetMapping("detail")
    public Product getProduct(Long pid){
        //If not for the first time, create one first
        if(!items.containsKey(pid)){
            Product product = Product.builder().productId(pid)
                    .description("new arrival")
                    .stock(100L).build();
            items.putIfAbsent(pid,product);
        }
        return items.get(pid);
    }

    @GetMapping("placeOrder")
    public String buy(Long pid){
        Product product = items.get(pid);
        if(product==null){
            return "Product Not Found";
        }else if(product.getStock()<=0L){
            return "Sold Out";
        }
        //If it is a single application, even the cluster can retain this code. The cluster solution requires a distributed lock to place the control on the central node
        synchronized (product){
            if(product.getStock()<=0L){
                return "Sold Out";
            }
            product.setStock(product.getStock()-1);
        }
        return "Order Placed";
    }
}

Go back to the gateway server project and define assertions in the way of time extension

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/gatewayjava/**")
                             .and().method(HttpMethod.POST)
                             .and().query("name","gavin")
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.now().plusSeconds(30))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT")
                )
                .build();
    }
}

Time nodes can be precisely defined

@Configuration
public class GatewayConfiguration {

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        LocalDateTime ldt = LocalDateTime.of(2020,10,24,20,31,10);
        return builder.routes()
                .route(r -> r.path("/gavinjava/**")
                             .and().method(HttpMethod.POST)
                             .and().query("name","gavin")
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT")
                )
                .build();
    }
}

8, Filter principle and life cycle

All open source frameworks implement filters in the same way. In a way similar to the responsibility chain, the events in the traditional responsibility chain mode will be transmitted until a processing object takes over, while the filter is a little different from the traditional responsibility chain. All filters must be filtered and processed all the way to the end until they are processed by the last filter

1. Implementation mode of filter

It is very simple to implement a filter in the Gateway. Just implement the default method of the Gateway filter interface

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

There are two key messages

  • ServerWebExchange: This is the HTTP request response interaction protocol encapsulated by Spring, from which we can obtain various request parameters in request and response, and add content to them
  • GatewayFilterChain: it is the call chain of the filter. At the end of the method, we need to pass the exchange object into the next object in the call chain

2. Filter execution phase

The Gateway implements effects similar to Pre and Post through the code in the Filter

Pre and Post refer to the execution phase of the current filter. Pre is executed before the next filter, and Post is executed after the filter is executed. We can also define pre and Post execution logic in the Gateway Filter at the same time

The Pre type and Post type are one before the filter is executed and the other after the filter is executed

Filters can be arranged sequentially

Org. Org can be implemented in the Gateway springframework. core. The ordered interface is used to specify the execution order of the filter by implementing the getOrder method

public int getOrder(){
  return 0;
}
// For Pre type filters, the larger the number, the higher the priority, and the earlier it will be executed. However, for Post type filters, the smaller the number, the earlier it is executed

3. Filter example

Header filter

This series has many group filters that can add information to the specified Header

.filters(f -> f.addResponseHeader("name","gateway-server"))
//It is equivalent to adding a name attribute to the header, and the corresponding value is gateway server

StripPrefix filter

This is a common filter. Its function is to remove some URL paths

.route(r -> r.path("/gateway-test/**")
						 .filters(f -> f.stripPrefix(1))
       			 .uri("lb://FEIFN-SERVICE/")
)
//If the HTTP request accesses / gateway test / sample / update, if there is no StripPreix filter, the access path forwarded to feign-service is the same / / feign-service / gateway test / sample / update. If this filter is added, gateway will intercept the URL path according to the configuration in stripPrefix(1), for example, 1 is set here, Then remove a prefix, and finally the path sent to the background service becomes / / FEIGN-SERVICE/sample/update

PrefixPath filter

The function of StripPrefix is completely opposite to that of StripPrefix. It will add a prefix in front of the request path

.route(r -> r.path("/gateway-test/**")
						 .filters(f -> f.prefixPath("go"))
       			 .uri("lb://FEIGN-SERVICE/")
)
//If the path we access is / gateway test / sample, if we use this filter, it will become / / feign-service / go / gateway test / sample

RedirectTo filter

He can redirect requests that receive a specific status code to a specified web address

.filters(f -> f.redirect(304,"https://www.baidu.com"))
//Caused by: java.lang.IllegalArgumentException: status must be a 3xx code, but was 404

9, Custom filter to realize interface timing function

Go to the gateway server project team to modify and create a filter package

package com.icodingedu.springcloud.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class TimerFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //Time the interface and output log
        StopWatch timer = new StopWatch();
        //Start timing
        timer.start(exchange.getRequest().getURI().getRawPath());
        //We can also process the call chain and manually put in the request parameters
        exchange.getAttributes().put("requestTimeBegin",System.currentTimeMillis());
        return chain.filter(exchange).then(
            //This is where the call is made after the filter is executed
            Mono.fromRunnable(() -> {
                timer.stop();;
                log.info(timer.prettyPrint());
            })
        );
    }

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

Go to the gateway configuration to set a custom filter

@Configuration
public class GatewayConfiguration {

    @Autowired
    private TimerFilter timerFilter;

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        LocalDateTime ldt = LocalDateTime.of(2020,10,24,21,05,10);
        return builder.routes()
                .route(r -> r.path("/gavinjava/**")
                             .and().method(HttpMethod.GET)
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                                            .filter(timerFilter)
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT/")
                )
                .build();
    }
}

The test results are as follows

# The percentage here refers to the percentage of the execution time of this interface in the whole execution link
# 1 second = 1000000000(ns)9 zeros
---------------------------------------------
ns         %     Task name
---------------------------------------------
1775599286  100%  /sayhello

The filter defined above is for specific routes. We can also define a global filter that can be directly applied to all routes. We only need to modify the inheritance of the filter, and all routes can be loaded automatically without calling

@Slf4j
@Component
public class TimerFilter implements GlobalFilter, Ordered

Remove all timefilters referenced in the original config

10. Analysis of authority authentication scheme

1. User authentication of traditional single application

Use the session to save the login status and authenticate through the stored key value. When one machine cannot synchronize the session to other machines, our problem comes. How to authenticate the service application

2. Solutions in distributed environment

2.1. Synchronous session

Session replication is the easiest solution to think of first. You can copy sessions from one machine to other machines in the cluster. For example, Tomcat also has a built-in session synchronization scheme, but this is not a very elegant solution. It will bring the following two problems

  • Timing problem: synchronization takes some time. We can't guarantee the timeliness of session synchronization. That is, when the user initiates two requests and falls on different machines, the information written to the session in the previous request may not be synchronized to all machines, and the business logic has been executed in the latter request, which will cause dirty reading and unreal reading
  • **Data redundancy: * * all servers need to save a complete set of session s, which produces a large amount of redundant data

2.2. Reverse proxy: bind IP or consistency hash

This scheme is implemented at the Nginx gateway layer. We can specify that some IP requests fall on a specified machine. In this way, the session will always exist on the same machine. However, compared with the previous session replication method, the method of binding IP has more obvious defects, as follows:

  • Load balancing: when the IP is bound, the load balancing policy cannot be applied in the gateway layer, and the failure of a server will have a great impact on the visiting users of the specified IP. For the gateway layer, the configuration of this routing rule is also troublesome
  • IP change: the IP of many operators will be switched from time to time, which will lead to the request after IP replacement being routed to different service nodes for processing. In this way, the session information set earlier can not be read

In order to solve the second problem, you can do it through consistent hash routing. For example, you can hash according to the user ID, and different hash values fall on different machines to ensure sufficient and balanced distribution. In this way, you can avoid the problem of IP switching, but you still can't solve the problem of load balancing mentioned in the first point

2.3. Redis solution

By centralizing the session, it is transferred from the server storage to redis

At the tomcat level, you can directly use components to put the container's session into redis. Another scheme can store the session into redis with the help of the session management method of springboot

3. Alternative to distributed Session

3.1. OAuth 2.0

OAuth 2.0 is an open authorization standard protocol, which allows third-party applications to access the user's specific private resources in a service, but does not provide account and password information to third-party applications

3.2. JWT authentication

JWT is also a Token based authentication mechanism. Its basic idea is to exchange a user name + password for an Access Token

Authentication process

1. User name + password access authentication service

  • Verification passed: the server returns an Access Token to the client and saves the Token somewhere on the server for subsequent access control (it can be saved in the database or Redis)
  • Validation failed: do not generate Token

2. The client uses the token to access the resource, and the server verifies the validity of the token

  • Token error or expiration: intercept the request and ask the client to re apply for the token
  • Correct token: release allowed

11, Implement JWT authentication

Complete the authentication operation through the following steps

  • Create auth service (login and authentication service)
  • Add JwtService class to realize token creation and verification
  • The gateway layer integrates auth service (adds AuthFilter to the gateway layer, and returns 403 if there is no login)

Create an auth service API module in the gateway

Add POM dependency

  <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
</dependencies>

Create an entity package and an account entity object

package com.icodingedu.springcloud.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {

    private String username;

    private String token;

    //When a token is close to invalidation, you can use refreshToken to generate a new token
    private String refreshToken;
}

Create an AuthResponseCode class under the entity package

package com.icodingedu.springcloud.entity;

public class AuthResponseCode {
    
    public static final Long SUCCESS = 1L;
    
    public static final Long INCORRECT_PWD = 1000L;
    
    public static final Long USER_NOT_FOUND = 1001L;
    
    public static final Long INVALID_TOKEN = 1002L;
}

Create an AuthResponse processing result class under the entity package

package com.icodingedu.springcloud.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {

    private Account account;
    
    private Long code;

}

Create a service package and create the interface AuthService in it

package com.icodingedu.springcloud.service;

import com.icodingedu.springcloud.entity.AuthResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@FeignClient("auth-service")
public interface AuthService {

    @PostMapping("/login")
    @ResponseBody
    public AuthResponse login(@RequestParam("username") String username,
                              @RequestParam("password") String password);

    @GetMapping("/verify")
    @ResponseBody
    public AuthResponse verify(@RequestParam("token") String token,
                               @RequestParam("username") String username);

    @PostMapping("/refresh")
    @ResponseBody
    public AuthResponse refresh(@RequestParam("refresh") String refreshToken);
}

Create the module of auth service implemented by the service, or put it in the gateway directory

Import POM dependencies

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>com.icodingedu</groupId>
            <artifactId>auth-service-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

Create startup class application

package com.icodingedu.springcloud;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(AuthServiceApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

Create a service package and create a JwtService class

package com.icodingedu.springcloud.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.icodingedu.springcloud.entity.Account;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Date;

@Slf4j
@Service
public class JwtService {
    
    //In the production environment, it should be encrypted from the outside and passed in
    private static final String KEY = "you must change it";
    //In the production environment, it should be encrypted from the outside and passed in
    private static final String ISSUER = "gavin";
    //Define expiration time
    private static final long TOKEN_EXP_TIME = 60000;
    //Define the passed in parameter name
    private static final String USERNAME = "username";

    /**
     * Generate token
     * @param account Account information
     * @return token
     */
    public String token(Account account){
        //token generation time
        Date now = new Date();
        //Algorithm used to generate token
        Algorithm algorithm = Algorithm.HMAC256(KEY);
        
        String token = JWT.create()
                          .withIssuer(ISSUER) //Issuer
                          .withIssuedAt(now) //Release time
                          .withExpiresAt(new Date(now.getTime()+TOKEN_EXP_TIME)) //token expiration time
                          .withClaim(USERNAME,account.getUsername()) //username passed in for publication
                          .sign(algorithm); //Sign with the algorithm set above
        log.info("jwt generated user={}",account.getUsername());
        return token;
    }

    /**
     * Validate token
     * @param token
     * @param username
     * @return
     */
    public boolean verify(String token,String username){
        log.info("verify jwt - user={}",username);
        try{
            //The encryption and decryption algorithm is the same
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            //Build a verifier: verify the content of JWT, which is an interface
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .withClaim(USERNAME,username)
                    .build();
            //Verify and pass directly without error
            verifier.verify(token);
            return true;
        }catch(Exception ex){
            log.error("auth failed",ex);
            return false;
        }
    }
}

Create the controller package and create the JwtController class

package com.icodingedu.springcloud.controller;

import com.icodingedu.springcloud.entity.Account;
import com.icodingedu.springcloud.entity.AuthResponse;
import com.icodingedu.springcloud.entity.AuthResponseCode;
import com.icodingedu.springcloud.service.AuthService;
import com.icodingedu.springcloud.service.JwtService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@Slf4j
@RestController
public class JwtController implements AuthService {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public AuthResponse login(String username, String password) {
        Account account = Account.builder()
                .username(username)
                .build();

        //TODO 0 - in this step, you need to verify the user name and password. Generally, it is in the database. It is assumed that the verification has passed
        //TODO, if the verification fails, return here
        //1 - generate token
        String token = jwtService.token(account);
        account.setToken(token);
        //2 - save the key to get the new token here
        account.setRefreshToken(UUID.randomUUID().toString());
        //3 - save the token, save and retrieve the token, and know which token is associated with the update only when refresh
        redisTemplate.opsForValue().set(account.getRefreshToken(),account);
        //4 - return token
        return AuthResponse.builder()
                .account(account)
                .code(AuthResponseCode.SUCCESS)
                .build();
    }

    @Override
    public AuthResponse verify(String token, String username) {
        boolean flag = jwtService.verify(token, username);

        return AuthResponse.builder()
                .code(flag?AuthResponseCode.SUCCESS:AuthResponseCode.INVALID_TOKEN)
                .build();
    }

    @Override
    public AuthResponse refresh(String refreshToken) {
        //When using redisTemplate to save an object, the object must be an object that can be serialized
        Account account = (Account) redisTemplate.opsForValue().get(refreshToken);
        if(account == null){
            return AuthResponse.builder()
                    .code(AuthResponseCode.USER_NOT_FOUND)
                    .build();
        }
        //Get a new token
        String token = jwtService.token(account);
        account.setToken(token);
        //Update new refreshToken
        account.setRefreshToken(UUID.randomUUID().toString());
        //Delete the original
        redisTemplate.delete(refreshToken);
        //Add a new token
        redisTemplate.opsForValue().set(account.getRefreshToken(),account);
        return AuthResponse.builder()
                .account(account)
                .code(AuthResponseCode.SUCCESS)
                .build();
    }
}

Set application profile

spring.application.name=auth-service
server.port=50081

eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/

spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379

info.app.name=auth-service
info.app.description=test

management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

You can start the verification: Eureka server and auth service

Verified in PostMan: login, verify, refresh

Open and transform the gateway server

Dependency is introduced into POM and three dependencies are added

   <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>
				<!--because springcloud gateway Is based on webflux Yes, if necessary web Import starter-webflux instead of starter-web-->
        <dependency>
            <groupId>com.icodingedu</groupId>
            <artifactId>auth-service-api</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>

Create an authenticated service class GatewayAuthService in the gateway server

package com.icodingedu.springcloud.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class GatewayAuthService {

    //In the production environment, it should be encrypted from the outside and passed in
    private static final String KEY = "you must change it";
    //In the production environment, it should be encrypted from the outside and passed in
    private static final String ISSUER = "gavin";
    //Define the passed in parameter name
    private static final String USERNAME = "username";

    /**
     * Validate token
     * @param token
     * @param username
     * @return
     */
    public boolean verify(String token,String username){
        log.info("verify jwt - user={}",username);
        try{
            //The encryption and decryption algorithm is the same
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            //Build a verifier: verify the content of JWT, which is an interface
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .withClaim(USERNAME,username)
                    .build();
            //Verify and pass directly without error
            verifier.verify(token);
            return true;
        }catch(Exception ex){
            log.error("auth failed",ex);
            return false;
        }
    }
}

Create a new class: AuthFilter

package com.icodingedu.springcloud.filter;

import com.icodingedu.springcloud.entity.AuthResponse;
import com.icodingedu.springcloud.entity.AuthResponseCode;
import com.icodingedu.springcloud.service.GatewayAuthService;
import io.netty.util.internal.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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;

@Slf4j
@Component("authFilter")
public class AuthFilter implements GatewayFilter, Ordered {
    
    private static final String AUTH = "Authorization";
    
    private static final String USERNAME = "icodingedu-username";
    
    @Autowired
    private GatewayAuthService authService;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("Auth Start");
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(AUTH);
        String username = headers.getFirst(USERNAME);

        ServerHttpResponse response = exchange.getResponse();
        if(StringUtils.isBlank(token)){
            log.error("token not found");
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        AuthResponse resp = AuthResponse.builder()
                .code(authService.verify(token,username)?AuthResponseCode.SUCCESS:AuthResponseCode.INVALID_TOKEN)
                .build();
        if(resp.getCode() != 1L){
            log.error("invalid token");
            response.setStatusCode(HttpStatus.FORBIDDEN);
            return response.setComplete();
        }
        //TODO stores the user information in the request header again and transmits it to the downstream business
        ServerHttpRequest.Builder mutate = request.mutate();
        mutate.header(USERNAME, username);
        ServerHttpRequest buildRequest = mutate.build();
        
        //If TODO needs to put data in the response, it can also be put in the header of the response
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add("icoding-user",username);
        
        return chain.filter(
                exchange.mutate()
                        .request(buildRequest)
                        .response(response)
                        .build());
    }

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

Inject AuthFilter into configuration

@Configuration
public class GatewayConfiguration {

    @Autowired
    private AuthFilter authFilter;

    @Bean
    @Order
    public RouteLocator customerRouters(RouteLocatorBuilder builder){
        LocalDateTime ldt = LocalDateTime.of(2020,10,24,21,30,10);
        return builder.routes()
                .route(r -> r.path("/gavinjava/**")
                             .and().method(HttpMethod.GET)
                             .filters(f -> f.stripPrefix(1)
                                            .addResponseHeader("java-param","gateway-config")
                                            .filter(authFilter)
                             )
                             .uri("lb://FEIGN-CLIENT")
                )
                .route(r -> r.path("/secondkill/**")
                             .and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
                             .filters(f -> f.stripPrefix(1))
                             .uri("lb://FEIGN-CLIENT/")
                )
                .build();
    }
}

Start the service to verify: Eureka server, auth service, feign client, gateway server

12, Achieve gateway layer current limiting

Create a current limiting configuration class RedisLimiterConfiguration

package com.icodingedu.springcloud.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

@Configuration
public class RedisLimiterConfiguration {

    //We limit the current according to the IP address requested by the user
    @Bean
    @Primary //There is more than one KeyResolver in a system
    public KeyResolver remoteAddressKeyResolver(){
        return exchange -> Mono.just(
            exchange.getRequest()
                    .getRemoteAddress()
                    .getAddress()
                    .getHostAddress()
        );
    }

    @Bean("redisLimiterUser")
    @Primary
    public RedisRateLimiter redisRateLimiterUser(){
        //This is equivalent to a token bucket. We can also create a current limiting script ourselves
        //defaultReplenishRate: current limiting bucket rate, 10 per second
        //defaultBurstCapacity: capacity of barrels, 60
        return new RedisRateLimiter(10,60);
    }

    @Bean("redisLimiterProduct")
    public RedisRateLimiter redisRateLimiterProduct(){
        //The capacity of the bucket is 100, and the speed of creating tokens is 20 per second
        return new RedisRateLimiter(20,100);
    }
}

Configure application redis information of yaml

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
  redis:
    host: localhost
    port: 6379
    database: 0
  main:
    allow-bean-definition-overriding: true

When using, you need to configure it in gateway configuration and add RedisLimiter configuration

	@Configuration
	public class GatewayConfiguration {
	
	    @Autowired
	    private KeyResolver hostNameResolver;
	
	    @Autowired
	    @Qualifier("redisLimiterUser")
	    private RateLimiter rateLimiter;
	
	    @Autowired
	    private AuthFilter authFilter;
	
	    @Bean
	    @Order
	    public RouteLocator customerRouters(RouteLocatorBuilder builder){
	        LocalDateTime ldt = LocalDateTime.of(2020,10,24,21,30,10);
	        return builder.routes()
	                .route(r -> r.path("/gavinjava/**")
	                             .and().method(HttpMethod.GET)
	                             .filters(f -> f.stripPrefix(1)
	                                            .addResponseHeader("java-param","gateway-config")
	                                            .filter(authFilter)
	                                            .requestRateLimiter(
	                                                    c ->{
	                                                        c.setKeyResolver(hostNameResolver);
	                                                        c.setRateLimiter(rateLimiter);
	                                                        c.setStatusCode(HttpStatus.BAD_GATEWAY);
	                                            })
	                             )
	                             .uri("lb://FEIGN-CLIENT")
	                )
	                .route(r -> r.path("/secondkill/**")
	                             .and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
	                             .filters(f -> f.stripPrefix(1))
	                             .uri("lb://FEIGN-CLIENT/")
	                )
	                .build();
	    }
	}

Keywords: Java Spring Boot Spring Cloud Microservices Middleware

Added by pollysal on Wed, 05 Jan 2022 08:19:15 +0200