Spring Cloud中如何保证各个微服务之间调用的安全性?
但是在在微服务集群中服务之间暴力的接口,或者对于第三方开放的接口如果不做及安全和认证,后果可想而知。
阅读下文之前思考几个问题:
如何在restTemplate远程调用请求增加添加统一认证?
服务认证如何规范加密和解密?
远程调用统一什么协议比较合适?
如下图,三个服务注册到同一个注册中心集群,服务A、B、C之间如果不做任何限制,服务之间的接口基本是互通的。
但是如果A、B、C之间要做服务认证该如何设计?如果外部定制集成服务D接入怎么保证服务的安全性?
怎么加认证信息?
假设服务A是组织架构服务,服务B是规则引擎服务,服务C是公式引擎服务,如果B、C请求A服务调用用户信息除了开放接口规定参数。我们如何加入权限认证信息。
目前市面上主要两种方案处理
请求体Body中加入参数校验 => SDK集成场景较多
请求头Header中加入认证信息 token
请求头Header中加入认证信息 token,如下图结构所示。
▐ 拦截@FeignClient 客户端
feign远程调用请求增加头部信息处理,重新定义 RequestInterceptor
public class FeignApiInterceptor implements RequestInterceptor {
/**
* 统一处理feign的远程调用拦截
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 远程调用请求增加头部信息处理(简写代码如下)
requestTemplate.header("X-SERVICE-NAME", "...");
}
}
▐ 拦截RestTemplate远程调用请求
RestTemplate 提供高度封装的接口,可以让我们非常方便地进行 Rest API 调用。常见的方法如下:
/**
* restTemplate远程调用请求增加头部信息处理
*/
@Slf4j
public class RestApiHeaderInterceptor implements ClientHttpRequestInterceptor {
/**
* 处理restTemplate的请求拦截
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
long startTime = System.currentTimeMillis();
try {
HttpHeaders headers = request.getHeaders();
// 远程调用请求增加头部信息处理(简写代码如下)
requestTemplate.header("X-SERVICE-NAME", "...");
ClientHttpResponse response = execution.execute(request, body);
// 加入链路深度Deep
// ....
return response;
} finally {
// do something
}
}
}
▐ 服务注册客户端配置
@Configuration
@EnableFeignClients(basePackages = NamingConstant.BASE_PACKAGE)
public class DiscoveryClientConfig {
//......
/**
* feign请求拦截器
*/
@Bean
public RequestInterceptor feignInterceptor() {
return new FeignApiInterceptor();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(HttpClient httpClient,
Environment environment) {
RestTemplate restTemplate = new RestTemplate();
restTemplateBuilder.configure(restTemplate);
restTemplate.setRequestFactory(buildFactory(httpClient, environment));
// 加入自定义拦截器
restTemplate.getInterceptors().add(new RestApiHeaderInterceptor());
return restTemplate;
}
}
拓展补充:
在处理RestTemplate的请求拦截的时候我们也可以追加链路追踪日志,具体对于链路日志的拓展可查阅《微服务分布式架构中,如何实现日志链路跟踪?》
简写代码
@Slf4j
public class RestApiHeaderInterceptor implements ClientHttpRequestInterceptor {
/**
* 处理restTemplate的请求拦截
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
long startTime = System.currentTimeMillis();
try {
// 远程调用请求增加头部信息处理
ClientHttpResponse response = execution.execute(request, body);
// 获取传递线程变量
Collection<String> values = response.getHeaders()
.get(TraceUtil.TRACE_RPCCOUNT);
if (!CollectionUtils.isEmpty(values)) {
int value = Integer.valueOf(values.iterator().next());
// 链路深度追加
TraceUtil.getRpcCounter().addAndGet(value + 1);
}
return response;
} finally {
// 是否开启链路追踪
if (TraceUtil.isTraceLoggerOn()) {
// 输出链路日志(接口耗时)
TraceUtil.log(StringHelper.join("dt:", System.currentTimeMillis() - startTime,
", TRACE-RPC-", request.getMethod(),
", URI:", request.getURI()));
}
}
}
}
针对与validateToken方法这里就不做展开,对于加密方式大同小异,根据自己项目情况来定。
怎么接收认证信息?
读过Spring-Web源码应该知道OncePerRequestFilter过滤器基类,旨在保证在任何 servlet 容器上每个请求分派一次执行。它提供了一个带有 HttpServletRequest 和 HttpServletResponse 参数的doFilterInternal方法,详细用法可阅读源码。
另一种也出现在它自己的线程中的调度类型是ERROR 。如果子类希望静态声明是否应该在错误调度期间调用一次,它们可以覆盖shouldNotFilterErrorDispatch() 。
getAlreadyFilteredAttributeName方法确定如何识别请求已被过滤。默认实现基于具体过滤器实例的配置名称。
▐ 定义基础过滤器
public abstract class BaseWebFilter extends OncePerRequestFilter {
/**
* 返回类名,避免filter不被执行
*/
@Override
protected String getFilterName() {
return null;
}
}
过滤器定义虚拟类, 继承自接口的过滤器。如果声明为springbean,他自动加载到请求过滤链(因为继承了GenericFilterBean接口,spring默认把继承此类的过滤器bean加到web过滤链)中,通过注解@order定义其优先级如果不申明为springbean,又要加入到过滤链中,可以通过FilterRegistrationBean定义,并指定优先级。
▐ 封装拦截 ApiValidationFilter
@Override
public BaseWebFilter getFilterInstance() {
return new BaseWebFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 拦截校验
if (validateToken(request)) {
// 校验通过执行下一个lan'j
chain.doFilter(request, response);
} else {
// 封装统一认证失败返回信息
response.setStatus(401);
response.setCharacterEncoding(
Charset.defaultCharset().displayName());
response.setContentType(
MimeTypeUtils.APPLICATION_JSON.toString());
response.getWriter().println(JsonUtil.toJsonString(Response
.err("status.401")));
}
}
};
}
针对与validateToken方法这里就不做展开,对于加密方式大同小异,根据自己项目情况来定。
private boolean validateToken(HttpServletRequest request) {
if (!needValidate(request)) {
return true;
}
String security = resolveToken(request);
if (security == null) {
log.info("验证信息缺失,请求地址:{}", request.getRequestURI());
return false;
}
try {
// 解析security加密规则
return true;
} catch (Exception e) {
log.info("验证信息无效:{},请求地址:{}", security, request.getRequestURI());
return false;
}
}
总结
回顾整个方案设计与实现,大致可分为以下几个步骤
参数加入位置:请求头Header还是请求体Body
确定加入范围:@FeignClient客户端、构建RestTemplate以及集成服务远程接口调用
确定拦截位置:Web线程拦截器,用于统一处理线程变量,该过滤器执行顺序早于springsecurity的过滤器
确定加密/解密方式:加密字符串和解密