【spring 技术】基于springboot实现微服务之间FeignClient调用,免认证的功能

【spring 技术】基于springboot实现微服务之间FeignClient调用,免认证的功能

一、前言

在微服务开发中,经常使用FeginClient实现微服务直接调用,同时,一般线上服务的接口一般都会进行身份认证(token),接口被外部调用使用鉴权认证是合理的,但是内部微服务之间的相互调用如果进行鉴权认证,在一些场景下是没有必要的,鉴权认证肯定会降低服务直接的性能,有时一个客户端请求可能伴随着多个服务之间的链式请求(A->B->C->D->E),有的微服务是位于同一台服务器上的,是没有比要进行身份认证的,但是有些接口即对外提供服务,也供内部其它微服务调用,所以要区分开内部调用外部调用的情况;

今天就提供一个样例,用于实现内部FeignClient调用时,可设置为不进行token认证的方式;

开发环境springboot+secrity+Oauth2

二、拦截器OAuth2FeignRequestInterceptor

拦截器OAuth2FeignRequestInterceptor可以把请求链A->B->C中A的token复制copy给到B的请求中,实现了token的传递,我们贴一下这个拦截器的源码,大家参考一下,如下:

package org.springframework.cloud.openfeign.security;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Arrays;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.common.OAuth2AccessToken;

public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
    public static final String BEARER = "Bearer";
    public static final String AUTHORIZATION = "Authorization";
    private final OAuth2ClientContext oAuth2ClientContext;
    private final OAuth2ProtectedResourceDetails resource;
    private final String tokenType;
    private final String header;
    private AccessTokenProvider accessTokenProvider;

    public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource) {
        this(oAuth2ClientContext, resource, "Bearer", "Authorization");
    }

    public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource, String tokenType, String header) {
        this.accessTokenProvider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(), new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
        this.oAuth2ClientContext = oAuth2ClientContext;
        this.resource = resource;
        this.tokenType = tokenType;
        this.header = header;
    }

    public void apply(RequestTemplate template) {
        template.header(this.header, new String[0]);
        template.header(this.header, new String[]{this.extract(this.tokenType)});
    }

    protected String extract(String tokenType) {
        OAuth2AccessToken accessToken = this.getToken();
        return String.format("%s %s", tokenType, accessToken.getValue());
    }

    public OAuth2AccessToken getToken() {
        OAuth2AccessToken accessToken = this.oAuth2ClientContext.getAccessToken();
        if (accessToken == null || accessToken.isExpired()) {
            try {
                accessToken = this.acquireAccessToken();
            } catch (UserRedirectRequiredException var5) {
                this.oAuth2ClientContext.setAccessToken((OAuth2AccessToken)null);
                String stateKey = var5.getStateKey();
                if (stateKey != null) {
                    Object stateToPreserve = var5.getStateToPreserve();
                    if (stateToPreserve == null) {
                        stateToPreserve = "NONE";
                    }

                    this.oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve);
                }

                throw var5;
            }
        }

        return accessToken;
    }

    protected OAuth2AccessToken acquireAccessToken() throws UserRedirectRequiredException {
        AccessTokenRequest tokenRequest = this.oAuth2ClientContext.getAccessTokenRequest();
        if (tokenRequest == null) {
            throw new AccessTokenRequiredException("Cannot find valid context on request for resource '" + this.resource.getId() + "'.", this.resource);
        } else {
            String stateKey = tokenRequest.getStateKey();
            if (stateKey != null) {
                tokenRequest.setPreservedState(this.oAuth2ClientContext.removePreservedState(stateKey));
            }

            OAuth2AccessToken existingToken = this.oAuth2ClientContext.getAccessToken();
            if (existingToken != null) {
                this.oAuth2ClientContext.setAccessToken(existingToken);
            }

            OAuth2AccessToken obtainableAccessToken = this.accessTokenProvider.obtainAccessToken(this.resource, tokenRequest);
            if (obtainableAccessToken != null && obtainableAccessToken.getValue() != null) {
                this.oAuth2ClientContext.setAccessToken(obtainableAccessToken);
                return obtainableAccessToken;
            } else {
                throw new IllegalStateException(" Access token provider returned a null token, which is illegal according to the contract.");
            }
        }
    }

    public void setAccessTokenProvider(AccessTokenProvider accessTokenProvider) {
        this.accessTokenProvider = accessTokenProvider;
    }
}

三、Feign调用免认证实现(代码示例)

1)自定义注解@Inner

valuetrue接口需要认证 false接口不需要认证

package com.codvision.microservice.common.security.annotation;

import java.lang.annotation.*;

/**
 * @author microservice
 * @date 2019/4/13
 * <p>
 * 服务调用鉴权注解
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {

	/**
	 * 是否AOP统一处理
	 * @return false, true
	 */
	boolean value() default true;

	/**
	 * 需要特殊判空的字段(预留)
	 * @return {}
	 */
	String[] field() default {};

}

2)接口设置注解示例

@ApiOperation(value = "通过id查询", notes = "通过id查询")
  @Inner(value = false)
  @GetMapping("/file" )
  public R getTest() {
    String fileName = "123.png";
    try {
      FileInputStream fileInputStream = new FileInputStream("F:\\test\\image\\1111.PNG");
      template.putObject("yiyun-unv-test03", fileName, fileInputStream);

      String url = template.getObjectURL("yiyun-unv-test03",fileName,1);
      System.out.println(url);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return R.ok(null);
  }

3)feign调用示例

feign中的方法设置参数@RequestHeader(SecurityConstants.FROM) String from,用于标明是否为内部调用请求

/**
 * @author microservice
 * @date 2018/6/22
 */
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteUserService {

	/**
	 * 通过用户名查询用户、角色信息
	 * @param username 用户名
	 * @param from 调用标志
	 * @return R
	 */
	@GetMapping("/user/info/{username}")
	R<UserInfo> info(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM) String from);

}

3)aop切面

用于校验请求者和接口是否需要进行认证,使用AOP获取请求request信息和被调用的接口信息,重点校验了注解@Inner

:这里是实现内部接口实现免认证的关键,同时可以设置权限,实现内部接口不对外暴露的功能

package com.codvision.microservice.common.security.component;

import cn.hutool.core.util.StrUtil;
import com.codvision.microservice.common.core.constant.SecurityConstants;
import com.codvision.microservice.common.security.annotation.Inner;
import com.codvision.microservice.common.security.util.MicroserviceSecurityMessageSourceUtil;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.AccessDeniedException;

import javax.servlet.http.HttpServletRequest;

/**
 * @author microservice
 * @date 2018/11/26
 * <p>
 * 服务间接口不鉴权处理逻辑
 */
@Slf4j
@Aspect
@AllArgsConstructor
public class MicroserviceSecurityInnerAspect {

	private final HttpServletRequest request;

	@SneakyThrows
	@Around("@within(inner) || @annotation(inner)")
	public Object around(ProceedingJoinPoint point, Inner inner) {
		// 先判断 inner 是否为空, 为空则获取类上注解
		if (inner == null) {
			Class<?> aClass = point.getTarget().getClass();
			inner = AnnotationUtils.findAnnotation(aClass, Inner.class);
		}

		String header = request.getHeader(SecurityConstants.FROM);
		if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
			log.warn("访问接口 {} 没有权限", inner.value());
			throw new AccessDeniedException(MicroserviceSecurityMessageSourceUtil.getAccessor().getMessage(
					"AbstractAccessDecisionManager.accessDenied", new Object[] { inner.value() }, "access denied"));
		}
		return point.proceed();
	}

}

4)自定义OAuth2FeignRequestInterceptor拦截器

AccessTokenContextRelay, 上下文token 中转器.非常简单从上下文获取认证信息得到把 token 放到上下文

package com.codvision.microservice.common.security.interceptor;

import cn.hutool.core.collection.CollUtil;
import com.codvision.microservice.common.core.constant.SecurityConstants;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

import java.util.Collection;

/**
 * @author microservice
 * @date 2018/8/13 扩展OAuth2FeignRequestInterceptor
 */
@Slf4j
public class MicroserviceFeignClientInterceptor extends OAuth2FeignRequestInterceptor {

	private final OAuth2ClientContext oAuth2ClientContext;

	private final AccessTokenContextRelay accessTokenContextRelay;

	/**
	 * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
	 * within Authorization header
	 * @param oAuth2ClientContext provided context
	 * @param resource type of resource to be accessed
	 * @param accessTokenContextRelay
	 */
	public MicroserviceFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource,
			AccessTokenContextRelay accessTokenContextRelay) {
		super(oAuth2ClientContext, resource);
		this.oAuth2ClientContext = oAuth2ClientContext;
		this.accessTokenContextRelay = accessTokenContextRelay;
	}

	/**
	 * Create a template with the header of provided name and extracted extract 1. 如果使用
	 * 非web 请求,header 区别 2. 根据authentication 还原请求token
	 * @param template
	 */
	@Override
	public void apply(RequestTemplate template) {
		Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
		if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
			return;
		}

		accessTokenContextRelay.copyToken();
		if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) {
			super.apply(template);
		}
	}

}

5)Feign 的拦截器实现

package com.codvision.microservice.common.security.interceptor;

import feign.Feign;
import feign.RequestInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

/**
 * fegin 配置增强
 *
 * @author L.cm
 */
@Configuration
@ConditionalOnClass(Feign.class)
public class MicroserviceFeignConfiguration {

	@Bean
	@ConditionalOnProperty("security.oauth2.client.client-id")
	public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
			OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) {
		return new MicroserviceFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay);
	}

}
Spring Cloud微服务架构中,Nacos是一个注册中心和配置中心。Feign是一个声明式的Web服务客户端,它使得编写Web服务客户端变得更加容易。 使用Feign调用接口需要以下步骤: 1. 在pom.xml中添加Feign依赖 ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> ``` 2. 在启动类上添加@EnableFeignClients注解启用Feign客户端 ```java @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` 3. 创建接口,并使用@FeignClient注解指定调用的服务名称和服务路径 ```java @FeignClient(name = "service-provider") public interface UserService { @GetMapping("/user/{id}") String getUserById(@PathVariable("id") Long id); } ``` 其中,name属性指定服务名称,GetMapping注解指定服务路径。 4. 在需要使用该服务的地方注入UserService并调用方法即可 ```java @RestController public class UserController { @Autowired private UserService userService; @GetMapping("/user/{id}") public String getUserById(@PathVariable("id") Long id) { return userService.getUserById(id); } } ``` 在这个例子中,我们定义了一个名为UserService的Feign客户端,指定了调用的服务名称和服务路径。然后在UserController中注入了UserService并调用了其方法。最终,Feign会自动将该请求转发到名为service-provider的微服务,并返回结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dylan~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值