【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
value
:true
接口需要认证 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);
}
}