How does Spring Cloud Feign implement JWT token relay to transfer authentication information

Implemented in the previous article Spring Cloud resources

Token relay

Token relay is a more formal term. In other words, it is to let the token pass between services to ensure that the resource server can correctly authenticate the caller.

Can't tokens be relayed automatically at Feign?

If we carry A Token to access service A, service A can certainly authenticate, but service A calls service B through Feign. At this time, the Token of A cannot be directly passed to service B.

Let's briefly explain the reason. Calls between services are made through the Feign interface. At the caller, we usually write Feign interfaces similar to the following:

@FeignClient(name = "foo-service",fallback = FooClient.Fallback.class)
public interface FooClient {
    @GetMapping("/foo/bar")
    Rest<Map<String, String>> bar();

    @Component
    class Fallback implements FooClient {
        @Override
        public Rest<Map<String, String>> bar() {
            return RestBody.fallback();
        }
    }
}

When we call Feign interface, we will generate the proxy class of the interface through dynamic proxy for us to call. If we don't open the fuse, we can extract the authentication object JwtAuthenticationToken of the resource server from the SecurityContext object provided by Spring Security, which contains the JWT Token. Then we can put the Token in the request header through the RequestInterceptor interface implementing Feign. The pseudo code is as follows:

/**
 * Spring IoC needs to be injected
 **/
static class BearerTokenRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate template) {
            final String authorization = HttpHeaders.AUTHORIZATION;
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            
            if (authentication instanceof JwtAuthenticationToken){
                JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
                String tokenValue = jwtAuthenticationToken.getToken().getTokenValue();
                template.header(authorization,"Bearer "+tokenValue);
            }
        }
    }

If we don't turn on the fuse, it's not a big problem. In order to prevent the avalanche of the call chain, the service fuse is basically not turned on. At this time, Authentication cannot be obtained from the SecurityContextHolder. At this time, Feign is called in a sub thread opened under the caller's calling thread. Because the fuse component I use is Resilience4J, the corresponding thread source code is in Resilience4JCircuitBreaker:

 Supplier<Future<T>> futureSupplier = () -> executorService.submit(toRun::get);

The SecurityContextHolder saves information through ThreadLocal by default. We all know that this cannot cross threads, and Feign's interceptor is just in the sub thread. Therefore, Feign with circuit breaker enabled cannot directly relay tokens.

❝ fuse components include outdated Hystrix, Resilience4J and Ali's Sentinel, and their mechanisms may be slightly different.

Implement token relay

Although token relay cannot be realized directly, I still found some information from it. The following code is found in FeignCircuitBreakerInvocationHandler, the processor of Feign interface agent:

private Supplier<Object> asSupplier(final Method method, final Object[] args) {
  final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  return () -> {
   try {
    RequestContextHolder.setRequestAttributes(requestAttributes);
    return this.dispatch.get(method).invoke(args);
   }
   catch (RuntimeException throwable) {
    throw throwable;
   }
   catch (Throwable throwable) {
    throw new RuntimeException(throwable);
   }
   finally {
    RequestContextHolder.resetRequestAttributes();
   }
  };
 }

This is the execution code of Feign proxy class. We can see that before execution:

  final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

Here is the information obtained from the request in the calling thread, including ServletHttpRequest, ServletHttpResponse and other information. Then, this information is added to the Setter in the lambda Code:

 RequestContextHolder.setRequestAttributes(requestAttributes);

If this is done in one thread, it is almost full. In fact, the return value of the Supplier is executed in another thread. The purpose of this is to save some requested metadata across threads.

InheritableThreadLocal

How does RequestContextHolder transfer data across threads?

public abstract class RequestContextHolder  {
 
 private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
   new NamedThreadLocal<>("Request attributes");

 private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
   new NamedInheritableThreadLocal<>("Request context");
// ellipsis
}

RequestContextHolder maintains two containers, one is ThreadLocal that cannot cross threads, and the other is namedinheritabilethreadlocal that implements InheritableThreadLocal. InheritableThreadLocal can transfer the data of the parent thread to the child thread. Based on this principle, RequestContextHolder brings the caller's request information into the child thread. With this principle, token relay can be realized.

Implement token relay

Change the initial Feign interceptor code to realize token relay:

    /**
     * Token relay
     */
    static class BearerTokenRequestInterceptor implements RequestInterceptor {
        private static final Pattern BEARER_TOKEN_HEADER_PATTERN = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
                Pattern.CASE_INSENSITIVE);

        @Override
        public void apply(RequestTemplate template) {
            final String authorization = HttpHeaders.AUTHORIZATION;
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (Objects.nonNull(requestAttributes)) {
                String authorizationHeader = requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
                Matcher matcher = BEARER_TOKEN_HEADER_PATTERN.matcher(authorizationHeader);
                if (matcher.matches()) {
                    // Clear token header to avoid infection
                    template.header(authorization);
                    template.header(authorization, authorizationHeader);
                }
            }
        }
    }

In this way, when you call FooClient.bar(), the resource server (OAuth2 Resource Server) in foo service can also obtain the caller's token, and then obtain the user's information to process resource permissions and business.

❝ don't forget to inject this interceptor into Spring IoC.

summary

Microservice token relay is very important to ensure the transmission of user state on the call link. And this is also the difficulty of microservices. Today, token relay is implemented with the help of some features of Feign and ThreadLocal for your reference. It's not easy to be original. Please click again, like and forward.

Posted by sargus on Tue, 02 Nov 2021 03:01:25 -0700