. . .

Dispatcher


Introduction

The Dispatcher service is an API Gateway providing a way to route APIs. It is built based on the Spring Cloud Gateway and includes all its features (as describe in the Spring Cloud Gateway public documentation). It also provides path rewriting rules for Yambas and the ApiOmat cloud environment. See How It works to have a deeper understanding of how the Spring Cloud Gateway works.

Installation

See [Dispatcher] Installation.

Usage

Dispatcher loads all rulesets at service start. Therefore Consul is queried for all existing services and the contextPath of each service is used to build up a rule for redirection to that service if the context path matches. If new services are registered at Consul, the ruleset has to be refreshed for Dispatcher to become aware of new services. More information: Actuator API

Getting existing Routes

To retrieve the current routes which are mapped by dispatcher use the following REST endpoint:bash

bash
curl http://{DISPATCHER_HOST}/actuator/gateway/routes

The received response object is a list of defined routes which looks like this:bash

bash
[
{
"route_id":"{ROUTE_ID}'",
"route_definition": {
"id":"{ROUTE_DEFINITION_ID}",
"predicates": [
{
"name":"{PREDICATE_NAME}",
"args": {
"{KEY_1}":"{VALUE_1}"
}
}
],
"filters": [
{
"name": "{FILTER_NAME}",
"args": {
"{KEY_1}":"{VALUE_1}"
}
}
],
"uri": "{URI_NAME}",
"order": 1
},
"order": 1
},
[...]
]

The response contains the following elements:

  • Route: The basic building block of the gateway. It is defined by an id (ROUTE_ID), a destination uri (URI_NAME), a collection of predicates (PREDICATE_NAME), and a collection of filters (FILTER_NAME). A route is matched if the aggregate predicate is true.

  • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This lets you match on anything from the HTTP request, such as headers or parameters.

  • Filter: These are instances of Spring Framework GatewayFilter that have been constructed with a specific factory. Here, you can modify requests and responses before or after sending the downstream request.

You can find further information in the Examples section below about configuring route definitions with predicates and with filters.

Manually reloading Routes

If it happens that the dispatcher doesn't automatically refreshes its routes, you can also use the following curl request to trigger the refreshing manually:bash

bash
curl -X POST http://{DISPATCHER_HOST}/actuator/gateway/refresh

Service Registration Options

Other services can modify their behavior on how to register to Dispatcher. Use the following tags in the discovery configuration of your service (e.g. for Spring Boot applications: spring.cloud.consul.dicovery.tags in application.yaml):

  • registerAsYambasContextPath: Add this tag to create routes for the microservice's REST interface paths that match the YAMBAS REST path conventions. This option is especially used for generated microservices to make them transparent to existing SDKs.

  • notRegisterOnGateway: Add this tag to hide the microservice from outside which means it will not be exposed through the gateway and will be only accessible behind the Dispatcher.


Configure CORS Handling

Since Dispatcher version 1.0.1 the following configuration for Cross-Origin Resource Sharing exists as default within the application.yml:

spring:
    cloud:
        gateway:
    	    default-filters:
            - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
            globalcors:
    	    	corsConfigurations:
    		    	'[/**]':
	    		    	allowedOrigins: "*"
	    			    allowedHeaders: "*"
        				allowedMethods:
        				- GET
    		    		- POST
    	    			- PUT
        				- DELETE
        				- OPTIONS
            discovery:
                locator:
                    enabled: false

This means that by default:

  • CORS requests are allowed from requests that originate from any domain for all paths and all headers

  • Duplicate response header values of Access-Control-Allow-Credentials and Access-Control-Allow-Origin will be removed in cases when both the gateway CORS logic and the downstream logic add them.
    The default strategy is set to RETAIN_FIRST (default) which means that the first header value which is found will be taken.

  • Routes are not created based on services registered by the default DiscoveryClient implementation (spring.cloud.gateway.discovery.locator.enabled: false).

Secure your routes with Spring Security and OAuth2

By using OAuth 2.0 protocol, users can grant limited access to their resources on one site to another site without having to share or expose their credentials. In the Dispatcher service we are using OAuth 2.0 protocol and Spring Security to provide the possibility to secure some parts of your applications by forcing authentication.

Application configuration

A client configuration is needed for the application to provide these mechanism. OAuth 2.0 client registration and provider can be configured for Dispatcher through the following properties (In the following configuration, our authentication provider Service which is called Bouncer is used and a client is created in Bouncer. Any other authentication providers that supports OAuth 2.0 can be used as well):


Property Key

Description

Example value

security.oauth2.client.registration.Easy.client-id

Client ID used for identification and configured in the authentication provider.

Dispatcher

security.oauth2.client.registration.Easy.client-secret

Client secret credentials provided by your authentication provider.

123456-1a2b-1a2b-3c4d-123abc456

security.oauth2.client.registration.Easy.provider

Authentication provider used by this client application.

bouncer

security.oauth2.client.registration.Easy.redirect-uri

Redirect URI for authentication.

{baseUrl}/login/oauth2/code/{registrationId}

security.oauth2.client.registration.Easy.authorization-grant-type

Authorization grant type.

authorization_code

security.oauth2.client.registration.Easy.client-authentication-method

Client authentication method.

post

security.oauth2.client.registration.Easy.scope

Scopes of your client. Defaults may depend on your authentication provider settings. For example; sending openid as a scope is required to retrieve attributes of user from userinfo endpoint.

openid, address

security.oauth2.client.provider.bouncer.token-uri

URI from which the client application can retrieve a token using its client id and client secret.

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/auth

security.oauth2.client.provider.bouncer.authorization-uri

URI from which the client application can assert the validity of a token.

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/auth

security.oauth2.client.provider.bouncer.issuer-uri

URI that provide all information about token and authorisation, making toker-uri and authorization-uri facultative.

http://localhost:8095/auth/realms/Easy

To get started, you should create a client in your preferred Authentication Provider. In above example our Authentication Provider Service which is called Bouncer is used. Change client-id, client-secret, authorization-uri and token-uri with your client and provider settings. Other configurations can be kept as default.

Where to get these configuration values

Our authentication provider service is Bouncer. In these example we consider that it is started on localhost port 8095.

Client Configuration

You should create a client in your authentication provider then you will have client-id and client-secret. In the above example the client is created with authorization_code flow of OAuth 2.0.

Provider Configuration

You can statically write your provider settings for token-uri, authorization-uri and user-info-uri of your authentication provider.

Key

Value

token-uri

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/token

authorization-uri

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/auth

user-info-uri

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/userinfo

If your provider supports /.well-known/openid-configuration endpoint which is OAuth 2.0 standard, then you can set only issuer-uri to fetch your provider settings, Dispatcher will then retrieve them dynamically on startup. Because Bouncer is OAuth2 compliant, below is OpenID Connection configuration URL to get details about all security endpoints considering that we configured a realm named Easy and Bouncer is running on localhost port 8095, than configuring issuer-uri is enough:

Key

Value

issuer-uri

http://localhost:8095/auth/realms/Easy/protocol/openid-connect/token


Configure the secured paths

By default Dispatcher does not require authentication for any application. To force authentication for specific application registered to Dispatcher you can use the following property configuration (e.g. securing all endpoints to /example and /myapp):

dispatcher:
security:
route-matchers:
- /example/**
- /myapp/**

Note that you can't set security configuration at runtime.

Use a filter to relay the token within the proxied call

If you are using OAuth2 to generate token for a specific application at Dispatcher, you will have to use a token relay filter in the route definition to convey it to your secured application. This can be achieved with the following definition, e.g. for an application SecuredBackend running on localhost port 9999 with a context path /securedApp :

application.xml
routes:
  - id: securedApp
    uri: http://localhost:9999
    predicates:
      - Path=/securedApp/**
    filters:
      - TokenRelay=

Examples

This example section helps you to overcome the first barriers when using Spring Cloud Gateway functionalites. In Usage section above you already read about routes, predicates and filters. This section helps you to actually set them up.

Route definitions can be created from data stored in a database, from internal logic or by sending requests to the actuator gateway API. This guide provides you examples on how to create route definitions using the actuator gateway API .


Create a Route Definition via POST

Sending a POST request to the actuator gateway API is one way to create a route. Be aware that posting a route containing an ID which already exists will overwrite the existing route definition.
This means that if you want to update a route which already have filters or predicates you want to keep, you have to make sure to also add these filters and predicates values to the request data.

The following snippet contains a request skeleton for creating a new route definition:bash

bash
curl --location --request POST 'http://{DISPATCHER_HOST}/actuator/gateway/routes/{ROUTE_ID}' --header 'Content-Type: application/json' --data-raw '{
"id":"{ROUTE_DEFINITION_ID}",
"predicates": [
{
"name": "{PREDICATE_NAME}",
"args": {
"{KEY_1}":"{VALUE_1}"
}
}
],
"filters": [
{
"name": "{FILTER_NAME}",
"args": {
"{KEY_1}":"{VALUE_1}"
}
}
],
"uri": "http://example.com",
"order": 1
}'

After posting the defined route can be retrieved via GET request like already shown in the Usage section.

Keep in mind that internally the gateway implementation includes some caching features for routes. So manually refreshing the routes is always a good idea like show in the Usage section.


... with specific Predicate

In order to get a first working example we will define a predicate for a route to the specific URI http://example.com . The predicate will be set up to match a specific path:bash

bash
curl --location --request POST 'http://{DISPATCHER_HOST}/actuator/gateway/routes/exampleRoute' --header 'Content-Type: application/json' --data-raw '{
"id":"exampleRoute",
"predicates": [
{
"name": "Path",
"args":
{
"pattern": "/example/**"
}
}
],
"filters": [ ],
"uri": "http://example.com",
"order": 1
}'

After this route is defined every request against the Dispatcher containing a context path started by /example/ will be routed to http://example.com .

For more information about different predicates which can also be set within Dispatcher's application.yml, please refer to Route Predicate Factories.

... with specific Filter

Additionally we will now add a filter to the route definition to manipulate the request which will be routed. In the following example the additional header X-Request-Foo will be added:bash

bash
curl --location --request POST 'http://{DISPATCHER_HOST}/actuator/gateway/routes/exampleRoute' --header 'Content-Type: application/json' --data-raw '{
"id": "exampleRoute",
"predicates": [
{
"name": "Path",
"args":
{
"pattern": "/example/**"
}
}
],
"filters": [
{
"name": "AddRequestHeader",
"args":
{
"name": "X-Request-Foo",
"value": "Bar"
}
}
],
"uri": "http://example.com",
"order": 1
}'

For more information about the available filters please refer to GatewayFilter Factories.

Configure a Request Rate Limiter

If you want to limit the amount of incoming requests the RequestRateLimiter is the right choice. The current implementation of the Dispatcher service provides you to use the existing RequestRateLimiter of Spring (RequestRateLimiter GatewayFilter Factory).


The internal Spring implementation uses Redis together with a Token Bucket Algorithm. So this means you need a Redis server instance for the RequestRateLimiter. The implementation contains two arguments the replenishRate and the burstCapacity.

The replenishRate is how many requests per second do you want a user to be allowed to do, without any dropped requests. This is the rate that the token bucket is filled.

The burstCapacity is the maximum number of requests a user is allowed to do in a single second. This is the number of tokens the token bucket can hold.


Alternative explanation:

Imagine you are in a video game and your health bar equals the number of request. Then the burstCapacity is the maximum damage per second you can take and the replenishRate is the number of request per second that heals you after you got hurt. Incoming request can only be handled if you have health left.


This means:

  • a steady rate is accomplished by setting the same value in replenishRate and burstCapacity

  • temporary bursts can be allowed by setting burstCapacity higher than replenishRate, but the time to recover from the burst takes longer

If the limits are reached then the RequestRateLimiter filter will return HTTP 429 - Too Many Requests if a keyResolver was configured properly.

By default, if the KeyResolver does not find a key, requests are always denied with HTTP 403 - Forbidden. You can adjust this behavior by playing around with the spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key ( true or false ) and spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code properties (please refer to the Spring documentation for that).


Dispatcher 1.1.0 automatically provides the following default KeyResolver implementation which helps you to be able to use the RequestRateLimiter functionality out of the box: java

java
@Bean
KeyResolver userKeyResolver( )
{
return exchange -> Mono.just( "1" );
}

As soon as this or any other KeyResolver implementation is provided you be able to progress with the setup and configure your RequestRateLimiter.


1.) Setup Redis Instance

To setup a RequestRateLimiter for a specific route you first need to make sure that your Redis instance is running and is defined within your application.yaml. This setting is optional as long as your Redis is located at the default port on localhost:6379 .
In case your Redis has a different host and/or port, make sure the Dispatcher knows where to search by configuring the host and port in the application.yml of Dispatcher:bash

spring:
  redis:
    host: localhost
    port: 6379

If you do not choose to change the host and port of your Redis instance and decide to take the default port 6379 on localhost, you don't need to configure the Redis instance in the Dispatcher application.yml like above.

2.) Send POST request to Dispatcher

To create setup the RequestRateLimiter send the following POST request to the running Dispatcher: bash

bash
curl --location --request POST 'http://{DISPATCHER_HOST}/actuator/gateway/routes/exampleRoute' --header 'Content-Type: application/json' --data-raw '{
"id": "exampleRoute",
"predicates": [
{
"name": "Path",
"args":
{
"pattern": "/example/**"
}
}
],
"filters": [
{
"name": "RequestRateLimiter",
"args":
{
"redis-rate-limiter.replenishRate": 10,
"redis-rate-limiter.burstCapacity": 20
}
}
],
"uri": "http://example.com/",
"order": 1
}'

To make your changes persistent you can also define your route within the application.yml of Dispatcher like it is shown in Springs documentation about the Redis Rate Limiter .


3.) Refresh your routes

To refresh your freshly created route definition containing the RequestRateLimiter send a POST request against http://{DISPATCHER_HOST}/actuator/gateway/refresh like shown within the usage section.


4.) Test your Setup

To test your RequestRateLimiter setup you just need to send more request against your route in one second than you are allowed to do. The replenish rate and burst capacity will be written to the response headers x-ratelimit-replenish-rate and x-ratelimit-burst-capacity. Additionally you will get an important information within the header x-ratelimit-remaining about the amount of requests you have left within the next second. If the limits are reached you should get the response status HTTP 429 - Too Many Requests.


Further Information

For a deeper use of the Spring Cloud Gateway features please refer to the following documentation links :