「这是我参与2022首次更文挑战的第32天,活动详情查看:2022首次更文挑战」
一、前言
微服务之间需要通信,属于内部通讯,同时会有一些安全问题:
- 内部接口不能被外部访问。
- 在传统微服务中,内部通讯默认为安全,不需要鉴权。
在工作中,一个外部请求转换内部请求如图:
所有的请求通过 api
网关进入,转换为内部请求,会有如下场景:
- 外部请求经过
api
网关,进行鉴权:例如用户登录token
机制 - 外部请求经过
api
网关,无需鉴权:例如获取平台公共信息,将对应接口地址放入ignore-urls
配置中 - 内部请求,默认可信,一般不经过网关,由其调用服务进行负载均衡去直接调用其他服务
举个栗子:管理员通过 cms
删除用户
- 正常流程:请求先打到
cms
服务,管理员鉴权完后,调用user
服务,删除对应的用户B。 - 异常流程:用户A登录后,通过接口访问,直接去删除用户B。
本文就来讨论,如何阻止非法间接内部访问。
先来回顾下服务调用 feign
使用方式:
- 服务A 定义
controller
@RestController
@RequestMapping(path = "/test")
public class TestController {
@GetMapping("/feign")
public String test() {
return "test feign success";
}
}
复制代码
- 服务B 定义
feign
@FeignClient(value = "test")
public interface TestClient {
@GetMapping(value = "/test/feign")
String test();
}
复制代码
服务B 通过 feign
调用服务A的接口。
二、设计
为放在内部接口直接对外提供,可从这几个方向考虑:
api
网关转换外部请求时打上标签(或者删除对应tag
)- 服务调用每次调用都带上对应标识:例如在
HTTP
中header
加入特殊参数 - 微服务针对内部接口定义拦截器:只有识别是内部接口才能访问
(1)feign
配置
每个内部请求都带上一个标识:x-inner-request:true
。
@Component
public class FeignInnerRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("x-inner-request", true);
}
}
复制代码
只需要在将配置放在第三方 jar
指定的文件中即可,使用者会自动加载,从而避免的代码的侵入:
- 在资源目录下新建目录
META-INF
- 在
META-INF
目录下新建文件spring.factories
- 在文件中添加下面配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.test.config.InternalApiInterceptor, \
cn.test.config.FeignInnerRequestInterceptor, \
cn.test.config.InternalApiConfig
复制代码
(2)网关过滤
每个外部请求转内部请求都带上一个标识:x-inner-request:false
。
这里使用 gateway
:
@Configuration
public class InternalApiGatewayFilter {
@Bean
@Order(1)
public GlobalFilter filterInteralApi() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
request = exchange.getRequest().mutate().header("x-inner-request", false).build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
复制代码
(3)微服务内部请求拦截器
这需要两个部分:
- 注解:用来表示方法、类是内部访问的。
- 拦截器:用来统一处理拦截请求
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface InternalApi {
/**
* 是否内部接口, 默认 true
* @return 是否
*/
boolean value() default true;
}
复制代码
拦截器:
@Slf4j
@Component
public class InternalApiInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
InternalApi internalApi = getAnnotation(handler);
if (Objects.isNull(internalApi)) {
return true;
}
boolean isInternalApi = Boolean.valueOf(request.getHeader("x-inner-request"));
if (!isInternalApi) {
throw new Exception(); // 抛出异常不给访问
}
return true;
}
private InternalApi getAnnotation(Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
InternalApi internalApi = handlerMethod.getMethodAnnotation(InternalApi.class);
if (Objects.isNull(internalApi)) {
internalApi = handlerMethod.getMethod()
.getDeclaringClass().getAnnotation(InternalApi.class);
}
return internalApi;
}
private <A extends Annotation> A getAnnotation(HandlerMethod handlerMethod,
Class<A> annotationType) {
A annotation = handlerMethod.getMethodAnnotation(annotationType);
if (Objects.isNull(annotation)) {
return handlerMethod.getMethod()
.getDeclaringClass().getAnnotation(annotationType);
}
return annotation;
}
}
复制代码