Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

Comment below, Nuggets officials are giving away perimeter what was wrong with the welcome comment section of this post stating that nuggets officials are giving away 100 perimeter

When Feign is used in Spring Cloud microservice development, the problem of token relay needs to be dealt with. Only token relay can ensure the transmission of user authentication information in the call chain, and realize the implicit transmission of user authentication information in service A to service B through Feign. Today we’ll share how to implement token relay in Feign.

The token relay

Token Relay is a formal term that allows Token Relay to be passed between services to ensure that the resource server can properly authenticate the resource to the caller. The client accesses service A with JWT through the gateway, and service A verifies and resolves JWT. When service A invokes service B, service B may also need to verify and resolve JWT. For example, to query my order and the logistics information of my order, the order service can obtain my userId through JWT. If there is no relay token, the userId needs to be explicitly passed to the logistics information service. Sometimes the downstream service still has permissions to deal with, so token relay is very necessary.

Can’t tokens be automatically relayed in Feign?

If we access service A with A Token, service A can definitely authenticate, but service A invokes service B through Feign. In this case, service A’s Token cannot be directly transferred to service B.

To briefly explain why, calls between services are made through the Feign interface. On the caller side we usually write Feign interfaces like 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() {
            returnRestBody.fallback(); }}}Copy the code

When we invoke the Feign interface, the dynamic proxy generates the proxy class for that interface for us to invoke. If we don’t turn on the fuse we can extract the resource server’s authentication object JwtAuthenticationToken from the SecurityContext object provided by Spring Security, It contains the JWT Token and then we can put the Token in the request header by implementing the Feign interceptor interface RequestInterceptor. The pseudo-code is as follows:

/** * Need to inject Spring IoC **/
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); }}}Copy the code

This is not a problem if we do not turn on the fuse, in order to prevent the call chain avalanche service fuse is basically not turned on. Authentication cannot be retrieved from the SecurityContextHolder. The Feign call starts in a child thread under the caller’s calling thread. Because I use the fusing of components is Resilience4J, corresponding thread execution source is Resilience4JCircuitBreaker segments of the following:

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

SecurityContextHolder stores information via a ThreadLocal by default. We all know that this can’t be done across threads, and the Feign interceptor is in the child thread. So Feign, which has a circuitBreaker on, cannot do token relay directly.

Fuse components such as outdated Hystrix, Resilience4J, and Ali’s Sentinel may have slightly different mechanisms.

Implement token relay

Although token relay cannot be implemented directly, I did find some information from it. Agent in Feign interface processor FeignCircuitBreakerInvocationHandler found in the following code:

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(); }}; }Copy the code

Here is the code for executing the Feign proxy class. We can see that before executing:

		final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Copy the code

Here is the information about the request in the calling thread, including ServletHttpRequest, ServletHttpResponse, and so on. Then Setter this information in the lambda code:

	RequestContextHolder.setRequestAttributes(requestAttributes);
Copy the code

If this is done in one thread, it would be too much. In fact, the Supplier returns the value in another thread. The goal is to save some request metadata across threads.

InheritableThreadLocal

How does the 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");
/ / to omit
}
Copy the code

RequestContextHolder maintenance for two container, one is not across threads ThreadLocal, one is realized InheritableThreadLocal NamedInheritableThreadLocal. RequestContextHolder InheritableThreadLocal is a RequestContextHolder that uses the InheritableThreadLocal thread to pass data from the parent thread to the child thread.

Implement token relay

Token relaying is implemented with a change to the original Feign interceptor code:

    /** * token relay */
    static class BearerTokenRequestInterceptor implements RequestInterceptor {
        private static final Pattern BEARER_TOKEN_HEADER_PATTERN = Pattern.compile("^Bearer (? 
      
       [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 the token header to avoid infectiontemplate.header(authorization); template.header(authorization, authorizationHeader); }}}}Copy the code

So when you call fooClient.bar (), the Resource Server (OAuth2 Resource Server) in foo-service can also get the caller’s token and get the user’s information to handle the Resource permissions and business.

Don’t forget to inject this interceptor into Spring IoC.

conclusion

Microservice token relay is very important to ensure the transfer of user state over the calling link. This is also the difficulty of microservices. Today we implement token relay with the help of Feign features and ThreadLocal features. Original is not easy, please give a lot of praise, comment.