SpringCloud解決feign呼叫token丟失問題

Naylor發表於2024-05-17

背景討論

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);
        }
    }
}


引用

https://zhuanlan.zhihu.com/p/545508501

相關文章