背景討論
feign請求
在微服務環境中,完成一個http請求,經常需要調用其他好幾個服務纔可以完成其功能,這種情況非常普遍,無法避免。那麼就需要服務之間的通過feignClient發起請求,獲取需要的 資源。
認證和鑑權
一般而言,微服務項目部署環境中,各個微服務都是運行在內網環境,網關服務負責請求的路由,對外通過nginx暴露給請求者。
這種情況下,似乎網關這裏做一個認證,就可以確保請求者是合法的,至於微服務調用微服務,反正都是自己人,而且是內網,無所謂是否驗證身份了。
我有一個朋友,他們公司的項目確實就是這樣做的,正經的商業項目。
講道理,只要框架提供了這樣的功能,那麼就有存在的意義,但是,如果涉及權限的校驗,微服務之間的feign調用就需要知道身份了,即需要做鑑權。
token
無論是JWT、還是OAUTH2、還是shiro,大家比較公認的認證、鑑權方案,就是在請求頭中放一堆東西,然後服務提供者通過解析這些東西完成認證和鑑權,這些東西俗稱token。
在feign調用中需要解決的就是token傳遞的問題,只有請求發起者將正確的token傳遞給服務提供者,服務提供者才能完成認證&鑑權,進而返回需要的資源。
問題描述
在feign調用中可能會遇到如下問題:
- 同步調用中,token丟失,這種可以通過創建一個攔截器,將token做透傳來解決
- 異步調用中,token丟失,這種就無法直接透傳了,因爲子線程並沒有token,這種需要先將token從父線程傳遞到子線程,再進行透傳
解決方案
token透傳
編寫一個攔截器,在feign請求前,將http請求攜帶的token傳遞給restTemplate。
具體實現方式爲:
-
創建一個Component實現com.nghsmart.ar.context.RequestAttributeContext中的RequestInterceptor接口
-
重寫apply方法
-
通過RequestContextHolder對象獲取到RequestAttributes
-
通過RequestAttributes對象獲取到HttpServletRequest
-
通過HttpServletRequest對象獲取到請求頭
-
在請求頭中把token拿出來
-
將token塞進restTemplate創建的http請求頭中
示例代碼:
BizFeignRequestInterceptor
import com.nghsmart.ar.context.RequestAttributeContext;
import com.nghsmart.common.core.utils.ServletUtils;
import com.nghsmart.common.core.utils.StringUtils;
import com.nghsmart.common.core.utils.ip.IpUtils;
import com.nghsmart.common.security.constant.FeignRequestHeader;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.AbstractRequestAttributes;
import org.springframework.web.context.request.FacesRequestAttributes;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Slf4j
@Order(1)
@Component
public class BizFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (null! = attributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
String token = servletRequestAttributes.getRequest().getHeader("token");
requestTemplate.header("token",token);
}
}
}
token異步線程傳遞
上述添加BizFeignRequestInterceptor只能解決同步調用環境下的token傳遞問題,當是異步線程環境下就GG了。
通過在主線程中主動將RequestAttribute傳遞到子線程中可以解決一部分異步線程中token傳遞的問題,示例代碼如下:
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
但是這種方式有弊端,當主線程先於子線程結束的時候,子線程將獲取不到RequestAttribute,原因是Tomcat會在http請求結束的時候清空數據。
我們可以創建一個InheritableThreadLocal用來保存RequestAttribute,這樣就可以完美解決問題了。
實現思路爲:
-
創建一個 RequestAttributeContext,其中維護一個InheritableThreadLocal對象,用來存RequestAttributes
-
創建一個RequestAttributeInterceptor,實現HandlerInterceptor, WebMvcConfigurer接口,用來在請求開始前把 RequestAttributes 存放到 RequestAttributeContext 中
-
修改 BizFeignRequestInterceptor ,當無法獲取到 RequestAttributes 的時候,就從 RequestAttributeContext 中獲取
-
透傳邏輯不變
相關示例代碼如下:
RequestAttributeContext
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
@Slf4j
public class RequestAttributeContext {
private static final ThreadLocal<RequestAttributes> context = new InheritableThreadLocal<>();
public static void setAttribute(RequestAttributes attributes) {
if (null == attributes) {
log.debug("RequestAttributes is null");
}
context.set(attributes);
}
public static RequestAttributes getAttribute() {
return context.get();
}
public static void removeAttribute() {
context.remove();
}
}
RequestAttributeInterceptor
import com.alibaba.fastjson.JSON;
import com.nghsmart.ar.context.RequestAttributeContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Configuration
public class RequestAttributeInterceptor implements HandlerInterceptor, WebMvcConfigurer {
/**
* 重寫 WebMvcConfigurer 的 addInterceptors,將 RequestAttributeInterceptor 添加到攔截器列表
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this).addPathPatterns("/**").excludePathPatterns("/swagger-resources/**", "/v2/api-docs/**");
}
/**
* 重寫 HandlerInterceptor 的 preHandle,在請求開始處理前,將 RequestAttribute 存入 RequestAttributeContext
*
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
RequestAttributeContext.setAttribute(requestAttributes);
return true;
}
}
BizFeignRequestInterceptor
import com.nghsmart.ar.context.RequestAttributeContext;
import com.nghsmart.common.core.utils.ServletUtils;
import com.nghsmart.common.core.utils.StringUtils;
import com.nghsmart.common.core.utils.ip.IpUtils;
import com.nghsmart.common.security.constant.FeignRequestHeader;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.AbstractRequestAttributes;
import org.springframework.web.context.request.FacesRequestAttributes;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Slf4j
@Order(1)
@Component
public class BizFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (null! = attributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
String token = servletRequestAttributes.getRequest().getHeader("token");
requestTemplate.header("token",token);
}else {
RequestAttributes requestAttributes = RequestAttributeContext.getAttribute();
if (null != requestAttributes) {
RequestContextHolder.setRequestAttributes(requestAttributes);
} else {
log.debug("requestAttributes is null");
}
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
String token = servletRequestAttributes.getRequest().getHeader("token");
requestTemplate.header("token",token);
}
}
}