一. 架构分析
Faraday 模块 属于网关,其代码结构与普通的 MVC 设计不同
下面为大致功能分配图
请将上面两张表互相参照着理解
路由解析:ReverseProxyFilter
路由映射表:MappingsProvider
Http映射表:HttpClientProvider
请求截获器:PreForwardRequestInterceptor
响应截获器:PostForwardResponseInterceptor
请求转发:RequestForwarder
负载均衡:LoadBalancer
二. 代码分析
我将根据请求的传递路线进行分析
即,从 ReverseProxyFilter 开始
首先,请求传入,调用 doFilterInternal 方法处理
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String originUri = extractor.extractUri(request);
String originHost = extractor.extractHost(request);
log.debug("Incoming request", "method", request.getMethod(),
"host", originHost,
"uri", originUri);
HttpHeaders headers = extractor.extractHttpHeaders(request);
HttpMethod method = extractor.extractHttpMethod(request);
String traceId = traceInterceptor.generateTraceId();
traceInterceptor.onRequestReceived(traceId, method, originHost, originUri, headers);
// 解析路由,找到对应的 Mapping
MappingProperties mapping = mappingsProvider.resolveMapping(originHost, request);
if (mapping == null) {
traceInterceptor.onNoMappingFound(traceId, method, originHost, originUri, headers);
log.debug(String.format("Forwarding: %s %s %s -> no mapping found", method, originHost, originUri));
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().println("Unsupported domain");
return;
} else {
log.debug(String.format("Forwarding: %s %s %s -> %s", method, originHost, originUri, mapping.getDestinations()));
}
byte[] body = extractor.extractBody(request);
addForwardHeaders(request, headers);
// 转发数据构建
RequestData dataToForward = new RequestData(method, originHost, originUri, headers, body, request);
// 请求截获器处理
preForwardRequestInterceptor.intercept(dataToForward, mapping);
if (dataToForward.isNeedRedirect() && !isBlank(dataToForward.getRedirectUrl())) {
log.debug(String.format("Redirecting to -> %s", dataToForward.getRedirectUrl()));
response.sendRedirect(dataToForward.getRedirectUrl());
return;
}
ResponseEntity<byte[]> responseEntity =
requestForwarder.forwardHttpRequest(dataToForward, traceId, mapping);
this.processResponse(response, responseEntity);
}
逻辑如下:
通过传入的 request 参数通过 RequestDataExtractor 解析获得 Uri , host,header,mathod 参数
通过 traceInterceptor 记录参数到日志中
通过 mappingsProvider 解析路由,找到对应的 Mapping
构建转发到其他类的数据 RequestData ,参数为刚刚解析出的参数
请求截获器调用方法处理,参数为刚刚生成的 RequestData 和 mapping
最后通过 requestForwarder 得到返回的 Response
通过自定义的 processResponse 进行处理
处理方法如下
// 处理返回的响应
protected void processResponse(HttpServletResponse response, ResponseEntity<byte[]> responseEntity) {
response.setStatus(responseEntity.getStatusCode().value());
responseEntity.getHeaders().forEach((name, values) ->
values.forEach(value -> response.addHeader(name, value))
);
if (responseEntity.getBody() != null) {
try {
response.getOutputStream().write(responseEntity.getBody());
} catch (IOException e) {
throw new FaradayException("Error writing body of HTTP response", e);
}
}
}
简单来说就是设置状态和头文件,然后转化成 输出流 输出。
然后,先直接到 RequestForwarder 类进行请求转发的分析,其他技术细节先放一放
从之前被调用的 forwardHttpRequest 方法开始
public ResponseEntity<byte[]> forwardHttpRequest(RequestData data, String traceId, MappingProperties mapping) {
// 解析出目标地址
ForwardDestination destination = resolveForwardDestination(data.getUri(), mapping);
prepareForwardedRequestHeaders(data, destination);
// 请求拦截器
traceInterceptor.onForwardStart(traceId, destination.getMappingName(),
data.getMethod(), data.getHost(), destination.getUri().toString(),
data.getBody(), data.getHeaders());
RequestEntity<byte[]> request = new RequestEntity<>(data.getBody(), data.getHeaders(), data.getMethod(), destination.getUri());
// 发送请求,得到返回
ResponseData response = sendRequest(traceId, request, mapping, destination.getMappingMetricsName(), data);
log.debug(String.format("Forwarded: %s %s %s -> %s %d", data.getMethod(), data.getHost(), data.getUri(), destination.getUri(), response.getStatus().value()));
// 响应拦截器
traceInterceptor.onForwardComplete(traceId, response.getStatus(), response.getBody(), response.getHeaders());
postForwardResponseInterceptor.intercept(response, mapping);
prepareForwardedResponseHeaders(response);
return status(response.getStatus())
.headers(response.getHeaders())
.body(response.getBody());
}
protected ForwardDestination resolveForwardDestination(String originUri, MappingProperties mapping) {
return new ForwardDestination(createDestinationUrl(originUri, mapping), mapping.getName(), resolveMetricsName(mapping));
}
protected URI createDestinationUrl(String uri, MappingProperties mapping) {
String host = loadBalancer.chooseDestination(mapping.getDestinations());
try {
return new URI(host + uri);
} catch(URISyntaxException e) {
throw new FaradayException("Error creating destination URL from HTTP request URI: " + uri + " using mapping " + mapping, e);
}
}
protected ResponseData sendRequest(String traceId, RequestEntity<byte[]> request, MappingProperties mapping, String mappingMetricsName, RequestData requestData ) {
ResponseEntity<byte[]> response;
long startingTime = nanoTime();
try {
// 通过 httpClient 映射表找到对应的服务进行转发(即restTemplate.exchange方法,参数为实体类型和返回数据类型)
response = httpClientProvider.getHttpClient(mapping.getName()).exchange(request, byte[].class);
recordLatency(mappingMetricsName, startingTime);
} catch (HttpStatusCodeException e) {
recordLatency(mappingMetricsName, startingTime);
response = status(e.getStatusCode())
.headers(e.getResponseHeaders())
.body(e.getResponseBodyAsByteArray());
} catch (Exception e) {
recordLatency(mappingMetricsName, startingTime);
traceInterceptor.onForwardFailed(traceId, e);
throw e;
}
UnmodifiableRequestData data = new UnmodifiableRequestData(requestData);
return new ResponseData(response.getStatusCode(), response.getHeaders(), response.getBody(), data);
}
逻辑如下:
首先通过自定义的 resolveForwardDestination解析出目标地址
该方法的核心逻辑是通过原始的 uri 加上根据映射表选择的 host 生成目标地址
然后,通过 traceInterceptor 记录一下 RequestData 中的参数到日志
生成 byte 数组的请求实体 RequestEntity 后,和之前的参数一起
通过自定义的 sendRequest 方法发送请求,得到返回值
sendRequest 方法逻辑如下:
通过 httpClientProvider 这个 http映射表找到对应的服务名进行转发
本质调用的是 restTemplate.exchange 方法
得到返回的 response
将 RequestData 转化数据结构为 UnmodifiableRequestData 后,
结合 response 的 StatusCode,Headers,Body 参数生成 ResponseData 进行返回
再回到 resolveForwardDestination 方法
通过响应拦截器 postForwardResponseInterceptor 进行处理后,
通过prepareForwardedResponseHeaders 方法处理头部后
返回数据
接着开始分析流程中的技术细节
首先是路由映射表 MappingsProvider
主要有俩方法,一个解析映射表,一个更新映射表
代码如下
// 解析路由映射
public MappingProperties resolveMapping(String originHost, HttpServletRequest request) {
if (shouldUpdateMappings(request)) {
updateMappings();
}
List<MappingProperties> resolvedMappings = mappings.stream()
.filter(mapping -> originHost.toLowerCase().equals(mapping.getHost().toLowerCase()))
.collect(Collectors.toList());
if (isEmpty(resolvedMappings)) {
return null;
}
return resolvedMappings.get(0);
}
// 更新路由映射表
@PostConstruct
protected synchronized void updateMappings() {
List<MappingProperties> newMappings = retrieveMappings();
mappingsValidator.validate(newMappings);
mappings = newMappings;
httpClientProvider.updateHttpClients(mappings);
log.info("Destination mappings updated", mappings);
}
解析第一步就会判断是否要更新映射表
但是,其实这个就是个假的,映射表在 yml 文件中被定义好了
所以都不需要更新。
然后将传入的 host 字符串解析成一个 MappingProperties 的 list
返回 list 的 首个值
然后是 http 映射表,关键方法也是两个
一个是创建 restTemplate 用于 restful 接口处理
然后是创建 httpClient
代码如下
protected RestTemplate createRestTemplate(MappingProperties mapping) {
CloseableHttpClient client = createHttpClient(mapping).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(client);
requestFactory.setConnectTimeout(mapping.getTimeout().getConnect());
requestFactory.setReadTimeout(mapping.getTimeout().getRead());
return new RestTemplate(requestFactory);
}
protected HttpClientBuilder createHttpClient(MappingProperties mapping) {
return create().useSystemProperties().disableRedirectHandling().disableCookieManagement();
}
都是调用已经封装好的方法,了解怎么使用即可
最后到俩拦截器
首先是 AuthRequestInterceptor 这个请求拦截器
代码较复杂,慢慢分析
首先是主方法 intercept
@Override
public void intercept(RequestData data, MappingProperties mapping) {
// sanitize incoming requests and set authorization information
String authorization = this.setAuthHeader(data, mapping);
this.validateRestrict(mapping);
this.validateSecurity(data, mapping, authorization);
// TODO - filter restricted headers
}
三个语句,句句要分析
首先第一句,获得一个授权字符串
调用了 setAuthHeader 方法
private String setAuthHeader(RequestData data, MappingProperties mapping) {
// default to anonymous web when prove otherwise
String authorization = AuthConstant.AUTHORIZATION_ANONYMOUS_WEB;
HttpHeaders headers = data.getHeaders();
Session session = this.getSession(data.getOriginRequest());
if (session != null) {
if (session.isSupport()) {
authorization = AuthConstant.AUTHORIZATION_SUPPORT_USER;
} else {
authorization = AuthConstant.AUTHORIZATION_AUTHENTICATED_USER;
}
this.checkBannedUsers(session.getUserId());
headers.set(AuthConstant.CURRENT_USER_HEADER, session.getUserId());
} else {
// prevent hacking
headers.remove(AuthConstant.CURRENT_USER_HEADER);
}
headers.set(AuthConstant.AUTHORIZATION_HEADER, authorization);
return authorization;
}
/**
* 拦截请求,取出 JWT 进行校验,并取出用户会话数据
*/
private Session getSession(HttpServletRequest request) {
String token = Sessions.getToken(request);
if (token == null) return null;
try {
DecodedJWT decodedJWT = Sign.verifySessionToken(token, signingSecret);
String userId = decodedJWT.getClaim(Sign.CLAIM_USER_ID).asString();
boolean support = decodedJWT.getClaim(Sign.CLAIM_SUPPORT).asBoolean();
Session session = Session.builder().userId(userId).support(support).build();
return session;
} catch (Exception e) {
log.error("fail to verify token", "token", token, e);
return null;
}
}
核心逻辑是通过传入的 RequestData 查看是否有 session
这个 getSession 方法会取出 token 校验 JWT
根据 JWT 取出 userId 及 support 权限
生成 session 返回
然后获得 session 了就根据 session 赋予不同的权限,设置不同的 header 参数
在最后返回权限。
接着到第二句语句
调用 validRestrict 方法
private Service getService(MappingProperties mapping) {
String host = mapping.getHost();
String subDomain = host.replace("." + envConfig.getExternalApex(), "");
Service service = ServiceDirectory.getMapping().get(subDomain.toLowerCase());
if (service == null) {
throw new FaradayException("Unsupported sub-domain " + subDomain);
}
return service;
}
private void validateRestrict(MappingProperties mapping) {
Service service = this.getService(mapping);
if (service.isRestrictDev() && !envConfig.isDebug()) {
throw new FaradayException("This service is restrict to dev and test environment only");
}
}
该方法会提取 service 判断是否能进行下一步操作
不能访问相应的子网就会抛异常
然后到最后一句
validSecurity 方法
private void validateSecurity(RequestData data, MappingProperties mapping, String authorization) {
// Check perimeter authorization
if (AuthConstant.AUTHORIZATION_ANONYMOUS_WEB.equals(authorization)) {
Service service = this.getService(mapping);
if (SecurityConstant.SEC_PUBLIC != service.getSecurity()) {
log.info("Anonymous user want to access secure service, redirect to login");
// send to login
String scheme = "https";
if (envConfig.isDebug()) {
scheme = "http";
}
int port = data.getOriginRequest().getServerPort();
try {
URI redirectUrl = new URI(scheme,
null,
"www." + envConfig.getExternalApex(),
port,
"/login/", null, null);
String returnTo = data.getHost() + data.getUri();
String fullRedirectUrl = redirectUrl.toString() + "?return_to=" + returnTo;
data.setNeedRedirect(true);
data.setRedirectUrl(fullRedirectUrl);
} catch (URISyntaxException e) {
log.error("Fail to build redirect url", e);
}
}
}
}
他会验证返回的权限是否符合请求的服务权限
验证发现访问了需要权限的页面,但没有权限后
在 RequestData 中设置重定向的目标地址
重定向到登录界面
最后是响应拦截器 CacheResponseInterceptor
@Override
public void intercept(ResponseData data, MappingProperties mapping) {
HttpHeaders respHeaders = data.getHeaders();
if (respHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
List<String> values = respHeaders.get(HttpHeaders.CONTENT_TYPE);
if (values.contains("text/html")) {
// insert header to prevent caching
respHeaders.set(HttpHeaders.CACHE_CONTROL, "no-cache");
}
}
}