【微服务】Staffjoy 项目源码解析(十)—— Faraday 模块

一. 架构分析

Faraday 模块 属于网关,其代码结构与普通的 MVC 设计不同
下面为大致功能分配图

Faraday架构

在这里插入图片描述

请将上面两张表互相参照着理解
路由解析: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");
            }
        }
    }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值