Feign服务间相互调用

项目中访问方式大致分为以下三种

  1. 外部从网关访问,需要token鉴权。如后台的增删改查是最常用的,只要用户登录就可以了(有些接口可能需要加权限)
  2. 外部从网关访问,不需要token鉴权,直接对外暴露无token访问。配置security.oauth2.client.release-urls就可以了
  3. 服务间Feign访问,不需要token鉴权,但又不对外暴露,外部不能访问,只能服务之间访问。实现方式如下

鉴于第三种情况,我们配置ignore-url使接口直接对处暴露,此时该接口不需要鉴权,服务内部通过Feign访问,服务外部通过url也可以访问,并不是我们想要的,所以我们通过在header中加入了一个from(SecurityConstants.FROM)参数来辨别是否内部调用。只有请求Header带有from参数时,才允许通过访问,否则抛出异常。那这样其实在外网通过Gateway访问时,也可以手动带上这个from参数来访问。所以需要在Gateway中设置了一个GlobalFilter过滤器,这个过滤器会删除请求头里的from参数。这样外部访问由于缺少头部信息,被拒绝访问;而服务间通过Feign访问,不经过Gateway,则可以正常访问。

使用场景:以上第三种情况,如A服务要调用B服务的方法

  • 在B服务相应接口上使用@Inside注解,使得url无需鉴权(如:SysLogController.save方法)

    @Inside的作用其实就是在项目加载时,获取有@Inside注解方法的uri,然后将这些uri自动加入到ignore-url中

    /**
     * <p>
     * 日志表 前端控制器
     * </p>
     *
     * @author
     */
    @RestController
    @AllArgsConstructor
    @RequestMapping("/log")
    @Api(value = "log", tags = "日志管理模块")
    public class SysLogController {
        private final SysLogService sysLogService;
    
        /**
         * 插入日志
         *
         * @param sysLog 日志实体
         * @return
         */
        @ApiOperation(value = "插入日志")
        @Inside
        @PostMapping("/save")
        public R save(@Valid @RequestBody SysLog sysLog) {
            return R.ok(sysLogService.save(sysLog));
        }
    }
    
  • 在A服务编写Feign接口(如:FeignLogService)

    //ServiceNameConstants.UMPS_ADMIN_SERVICE为相应的服务名
    @FeignClient(contextId = "feignLogService", value = ServiceNameConstants.UMPS_ADMIN_SERVICE)
    public interface FeignLogService {
        /**
         * 保存日志
         *
         * @param sysLog 日志实体
         * @param from   是否内部调用
         * @return succes、false
         */
        @PostMapping("/log/save")
        R<Boolean> saveLog(@RequestBody SysLog sysLog, @RequestHeader(SecurityConstants.FROM) String from);
    }
    
  • 在A服务调用接口(如:SysLogListener),带上SecurityConstants.FROM=SecurityConstants.FROM_IN参数指定为内部识别

    @Async
    @Order
    @EventListener(SysLogEvent.class)
    public void saveSysLog(SysLogEvent event) {
    	SysLog sysLog = event.getSysLog();
    	reignLogService.saveLog(sysLog, SecurityConstants.FROM_IN);
    }
    
  • 以上就完成了A服务调用B服务的过程

服务间的token传递

  • 没有标识内部调用的,Feign拦截器会将上游服务的token传递给下游服务
public class BaseFeignClientInterceptor extends OAuth2FeignRequestInterceptor {

	/**
	 * fein拦截器将本服务的token,通过copyToken的形式传递给下游服务
	 * @param requestTemplate
	 */
	@Override
	public void apply(RequestTemplate requestTemplate) {
		Collection<String> fromHeader = requestTemplate.headers().get(SecurityConstants.FROM);
		if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
			return;
		}
		accessTokenContextRelay.copyToken();
		if (oAuth2ClientContext != null
			&& oAuth2ClientContext.getAccessToken() != null) {
			super.apply(requestTemplate);
		}
	}
}

fallback统一处理

  • SpringCloud使用feign的时候,需要指定fallback策略, 但大多数情况的fallback策略为了保证幂等都会很简单,输出错误日志即可,实际开发中一个feignService就要定义一个fallback非常不方便
  • 通过重写com.alibaba.cloud.sentinel.feign.SentinelFeign、com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler实现fallback统一处理


@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
		if (EQUALS.equals(method.getName())) {
			try {
				Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
				return equals(otherHandler);
			}
			catch (IllegalArgumentException e) {
				return false;
			}
		}
		else if (HASH_CODE.equals(method.getName())) {
			return hashCode();
		}
		else if (TO_STRING.equals(method.getName())) {
			return toString();
		}

		Object result;
		InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
		// only handle by HardCodedTarget
		if (target instanceof Target.HardCodedTarget) {
			Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
			MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
					.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method));
			// resource default is HttpMethod:protocol://url
			if (methodMetadata == null) {
				result = methodHandler.invoke(args);
			}
			else {
				String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url()
						+ methodMetadata.template().path();
				Entry entry = null;
				try {
					ContextUtil.enter(resourceName);
					entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
					result = methodHandler.invoke(args);
				}
				catch (Throwable ex) {
					// fallback handle
					if (!BlockException.isBlockException(ex)) {
						Tracer.trace(ex);
					}
					if (fallbackFactory != null) {
						try {
							Object fallbackResult = fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex),
									args);
							return fallbackResult;
						}
						catch (IllegalAccessException e) {
							// shouldn't happen as method is public due to being an
							// interface
							throw new AssertionError(e);
						}
						catch (InvocationTargetException e) {
							throw new AssertionError(e.getCause());
						}
					}
					else {
						// 若是R类型 执行自动降级返回R
						if (R.class == method.getReturnType()) {
							log.error("feign 服务间调用异常", ex);
							return R.failed(ex.getLocalizedMessage());
						}
						else {
							throw ex;
						}
					}
				}
				finally {
					if (entry != null) {
						entry.exit(1, args);
					}
					ContextUtil.exit();
				}
			}
		}
		else {
			// other target type using default strategy
			result = methodHandler.invoke(args);
		}

		return result;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值