在Spring cloud项目开发中,经常需要通过feign远程调用其他的服务的方法。有时因为跨服务调用的时间较长,就会存在需要异步跨服务调用的场景,此时就会出现请求头(主要是token)丢失的问题。
报错:
Full authentication is required to access this resource
一、项目场景
1 异步生成Excel文件
2 跨服务调用上传文件
3 返回文件id
4 回更业务表
思路:从request中获取token;再将token设置到上下文工具中
二、代码示例
1.工具类
基于ThreadLocal类封装的工具类,用于在同一线程中的封装数据和获取数据。
/**
* 上下文工具类
*/
public class BaseContextHandler {
private static final ThreadLocal<Map<String, String>> THREAD_LOCAL = new ThreadLocal();
public BaseContextHandler() {
}
public static void set(String key, Object value) {
Map<String, String> map = getLocalMap();
map.put(key, value == null ? "" : value.toString());
}
public static <T> T get(String key, Class<T> type) {
Map<String, String> map = getLocalMap();
return Convert.convert(type, map.get(key));
}
public static <T> T get(String key, Class<T> type, Object def) {
Map<String, String> map = getLocalMap();
return Convert.convert(type, map.getOrDefault(key, String.valueOf(def == null ? "" : def)));
}
public static String get(String key) {
Map<String, String> map = getLocalMap();
return (String)map.getOrDefault(key, "");
}
public static Map<String, String> getLocalMap() {
Map<String, String> map = (Map)THREAD_LOCAL.get();
if (map == null) {
map = new HashMap(10);
THREAD_LOCAL.set(map);
}
return (Map)map;
}
public static void setLocalMap(Map<String, String> threadLocalMap) {
THREAD_LOCAL.set(threadLocalMap);
}
public static String getToken() {
return (String)get("Authorization", String.class);
}
public static void setToken(String token) {
set("Authorization", token);
}
public static void remove() {
if (THREAD_LOCAL != null) {
THREAD_LOCAL.remove();
}
}
}
2.Feign配置
在请求头中获取不到数据时,从BaseContextHandler里面取ThreadLocal存起来的请求头。
/**
* feign添加请求头拦截器
*/
public class FeignAddHeaderRequestInterceptor implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(FeignAddHeaderRequestInterceptor.class);
private static final List<String> HEADER_NAME_LIST = Arrays.asList("Authorization", "x-trace-id", "X-Real-IP", "x-forwarded-for");
public FeignAddHeaderRequestInterceptor() {
}
public void apply(RequestTemplate template) {
if (TracingContext.tracing().hasGroup()) {
template.header("X-FEIGN-ID", new String[]{TracingContext.tracing().groupId()});
}
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
HEADER_NAME_LIST.forEach((headerName) -> {
template.header(headerName, new String[]{BaseContextHandler.get(headerName)});
});
} else {
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
if (request == null) {
log.warn("path={}, 在FeignClient API接口未配置FeignConfiguration类, 故而无法在远程调用时获取请求头中的参数!", template.path());
} else {
HEADER_NAME_LIST.forEach((headerName) -> {
template.header(headerName, new String[]{(String)StringUtils.defaultIfEmpty(request.getHeader(headerName), BaseContextHandler.get(headerName))});
});
}
}
}
3.业务代码示例
1)控制器层
@GetMapping("/asyncFeignTest")
public R asyncFeignTest() {
try {
// 第一步:在主线程中, 通过请求头获取token
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String token = request.getHeader("Authorization");
// 调用异步方法时,将token传入
asyncFeignTestService.asyncFeignTest(token);
return R.success("成功");
} catch (Exception e) {
log.error("异常>>>>>>>>>>>>>>>>>>", e);
return R.fail("失败");
}
}
2)服务层
public interface AsyncFeignTestService{
void asyncFeignTest(String token);
Long feignFileUpload(String token);
}
3)服务实现层
/**
* 调用生成文件的方法,并获取返回的数据
* @return
* @throws IOException
*/
@Async
@Override
public void asyncFeignTest(String token) {
try {
// 调用含feign调用的方法,并获取返回的数据
Long resultId = this.feignFileUpload(token);
if (resultId != null) {
log.info("feign调用服务返回id成功,resultId:{}", resultId);
// 回更业务表代码
// ...
}
} catch (Exception e) {
log.error("feign调用服务返回id方法异常:>>>>>>>>>>>>{}", e);
}
}
/**
* 生成文件并保存到文件服务
* @return
* @throws IOException
*/
@Override
public Long feignFileUpload(String token) throws IOException {
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 文件生成准备 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// 获取模版文件
ClassPathResource classPathResource = new ClassPathResource("xls_template/file_template_fixed.xlsx");
InputStream fis = classPathResource.getInputStream();
// 保存写入的数据流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
// 设置excel输出策略
ExcelWriter excelWriter = EasyExcel
.write(outputStream, InternalSolutionFillDTO.class)
.withTemplate(fis)
.build();
// 转为文件上传
String fileName = "文件"+System.currentTimeMillis()+".xlsx";
String originName = "file_"+System.currentTimeMillis()+".xlsx";
MockMultipartFile customerFile = new MockMultipartFile(fileName,originName
, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
, outputStream.toByteArray());
// 第二部:在调用feign方法前,设置token
BaseContextHandler.setToken(token);
// 上传文件并获取主键
R uploadFileVo = fileApi.upload(customerFile, "bucket");
if (uploadFileVo.getData() != null) {
FileResultVO customerFileVo = JSONUtil.toBean(JSONUtil.toJsonStr(uploadFileVo.getData()), FileResultVO.class);
if (customerFileVo != null && customerFileVo.getId() != null) {
return customerFileVo.getId();
}
}
} catch (Exception e) {
log.error("文件生成,方法异常>>>>>>>>>>>>>>", e);
} finally {
fis.close();
}
return null;
}