当前位置: 首页 > 科技观察

Spring Cloud Feign如何实现JWT令牌中继以传递认证信息

时间:2023-03-19 01:12:57 科技观察

SpringCloudFeign是如何实现JWTtokenrelay传递认证信息的,服务端可以正确认证调用者。Feign不能自动中继token吗?如果我们带着Token去访问A服务,A服务肯定可以认证,但是A服务是通过Feign调用B服务的。这时候A的token是不能直接传递给B的服务的。这里简单解释一下原因。服务之间的调用是通过Feign接口进行的。在调用方,我们通常会写一个类似下面的Feign接口:@FeignClient(name="foo-service",fallback=FooClient.Fallback.class)publicinterfaceFooClient{@GetMapping("/foo/bar")Rest>bar();@ComponentclassFallbackimplementsFooClient{@OverridepublicRest>bar(){returnRestBody.fallback();}}}当我们调用Feign接口时,会调用该接口的代理类通过动态代理生成供我们调用。如果我们不开启熔断器,我们可以从SpringSecurity提供的SecurityContext对象中提取资源服务器的认证对象JwtAuthenticationToken,其中包含JWTtoken,然后我们可以通过实现Feign的拦截器接口RequestInterceptor将Token放在请求头中,pseudocode如下:/***需要注入SpringIoC**/staticclassBearerTokenRequestInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplatetemplate){finalStringauthorization=HttpHeaders.AUTHORIZATION;Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();if(authenticationinstanceofJwtAuthenticationToken){JwtAuthenticationTokenjwtAuthenticationToken=(JwtAuthenticationToken)验证;StringtokenValue=jwtAuthenticationToken.getToken().getTokenValue();template.header(authorization,"Bearer"+tokenValue);}}}如果我们不开启断路器,问题不大。为了防止调用链雪崩,服务断路器基本是不开启的。此时无法从SecurityContextHolder获取Authentication。因为此时Feign的调用是在调用者的调用线程下开辟的一个子线程中进行的。由于我使用的fuse组件是Resilience4J,对应的线程源码在Resilience4JCircuitBreaker中:Supplier>futureSupplier=()->executorService.submit(toRun::get);SecurityContextHolder默认通过ThreadLocal保存信息,我们都知道这个不能跨线程,而此时Feign的拦截器正好在子线程中,所以打开了断路器功能(circuitBreaker)的Feign不能直接进行token中继.Fuse组件包括过时的Hystrix、Resilience4J、阿里的哨兵Sentinel,它们的机制可能略有不同。实现tokenrelay虽然不能直接实现tokenrelay,但是还是从中找到了一些资料。在Feign接口代理的处理器FeignCircuitBreakerInvocationHandler中发现了如下代码:privateSupplierasSupplier(finalMethodmethod,finalObject[]args){finalRequestAttributesrequestAttributes=RequestContextHolder.getRequestAttributes();return()->{try{RequestContextHolder.setRequestAttributes(requestAttributes));returnthis.dispatch.get(method).invoke(args);}catch(RuntimeExceptionthrowable){throwthrowable;}catch(Throwablethrowable){thrownewRuntimeException(throwable);}finally{RequestContextHolder.resetRequestAttributes();}};}这是Feign代理类的执行代码,执行前我们可以看到:finalRequestAttributesrequestAttributes=RequestContextHolder.getRequestAttributes();这里是调用线程中请求的信息,包括ServletHttpRequest、ServletHttpResponse等信息。然后设置lambda代码中的信息:RequestContextHolder.setRequestAttributes(requestAttributes);如果这是在一个线程中完成的,那简直就是full,实际上Supplier的返回值是在另一个线程中执行的。这样做的目的是跨线程保存一些请求的元数据。InheritableThreadLocalpublicabstractclassRequestContextHolder{privatestaticfinalThreadLocalrequestAttributesHolder=newNamedThreadLocal<>("Requestattributes");privatestaticfinalThreadLocalinheritableRequestAttributesHolder=newNamedInheritableThreadLocal<>("Requestcontext");//省略}RequestContextHolder维护了两个容器,一个是不能跨线程的ThreadLocal,一个是NamedInheritableThreadLocal,它实现了InheritableThreadLocal。InheritableThreadLocal可以将父线程的数据传递给子线程。基于这个原理,RequestContextHolder将调用者的请求信息带入子线程。借助这个原理,可以实现令牌中继。TokenRelay通过更改初始Feign拦截器代码实现令牌中继:/***tokenrelay*/staticclassBearerTokenRequestInterceptorimplementsRequestInterceptor{privatestaticfinalPatternBEARER_TOKEN_HEADER_PATTERN=Pattern.compile("^Bearer(?[a-zA-Z0-9-._~+/]+=*)$",Pattern.CASE_INSENSITIVE);@Overridepublicvoidapply(RequestTemplatetemplate){finalStringauthorization=HttpHeaders.AUTHORIZATION;ServletRequestAttributesrequestAttributes=(ServletRequest.getRequestAttributes)RequestContext(ServletRequestAttributesrequestAttributes);如果(Objects.nonNull(requestAttributes)){StringauthorizationHeader=requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);Matchermatcher=BEARER_TOKEN_HEADER_PATTERN.matcher(authorizationHeader);if(matcher.matches()){//清除tokenheader避免Infecttemplate.header(authorization);template.header(authorization,authorizationHeader);}}}}这样调用FooClient.bar()时,foo-service中的资源服务器(OAuth2ResourceServer)就可以同样获取调用者的Tokens,进而获取用户信息来处理资源权限和业务。不要忘记将这个拦截器注入SpringIoC。综上所述,微服务令牌中继对于保证调用环节中用户状态的传递非常重要。而这也是微服务的难点。今天借助Feign和ThreadLocal的一些特性实现tokenrelay,供大家参考。