序言

上一篇文章:你连对外接口签名都不会晓得?有工夫还是要学习学习。

有很多小伙伴反馈,对外的 API 中相干的加签,验签这些工作能够对立应用网关去解决。

说到网关,大家必定比拟相熟。市面上应用比拟宽泛的有:spring cloud/kong/soul。

API 网关的作用

(1)对外接口中的权限校验

(2)口调用的次数限度,频率限度

(3)微服务网关中的负载平衡,缓存,路由,访问控制,服务代理,监控,日志等。

实现原理

个别的申请时间接通过 client 拜访 server 端,咱们须要在两头实现一层 api 网关,内部 client 拜访 gateway,而后 gateway 进行调用的转发。

外围流程

网关听起来非常复杂,最外围的局部其实基于 Servlet 的 javax.servlet.Filter 进行实现。

咱们让 client 调用网关,而后在 Filter 中对立对音讯题进行解析转发,调用服务端后,再封装返回给 client。

import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;/** * @author binbin.hou * @since 1.0.0 */@WebFilter@Componentpublic class GatewayFilter implements Filter {    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);    public void init(FilterConfig filterConfig) throws ServletException {    }    public void doFilter(ServletRequest servletRequest,                         ServletResponse servletResponse,                         FilterChain filterChain) throws IOException, ServletException {        HttpServletRequest req = (HttpServletRequest) servletRequest;        LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));        //依据 URL 获取对应的服务名称        // 进行具体的解决逻辑        // TODO...        } else {            filterChain.doFilter(req, servletResponse);        }    }    public void destroy() {    }}

接下来,咱们只须要重点看一下如何重写 doFilter 办法即可。

具体实现

获取 appName

网关是面对公司外部所有利用的,咱们能够通过每一个服务的惟一 appName 作为辨别。

比方利用名称为 test,则调用网关的申请:

https://gateway.com/test/version

这个申请,对应的 appName 就是 test,理论申请的 url 是 /version。

具体实现也比较简单:

@Overridepublic Pair<String, String> getRequestPair(HttpServletRequest req) {    final String url = req.getRequestURI();    if(url.startsWith("/") && url.length() > 1) {        String subUrl = url.substring(1);        int nextSlash = subUrl.indexOf("/");        if(nextSlash < 0) {            LOGGER.warn("申请地址 {} 对应的 appName 不存在。", url);            return Pair.of(null, null);        }        String appName = subUrl.substring(0, nextSlash);        String realUrl = subUrl.substring(nextSlash);        LOGGER.info("申请地址 {} 对应的 appName: {}, 实在申请地址:{}", url, appName, realUrl);        return Pair.of(appName, realUrl);    }    LOGGER.warn("申请地址: {} 不是以 / 结尾,或者长度不够 2,间接疏忽。", url);    return Pair.of(null, null);}

申请头信息

依据 HttpServletRequest 构建出对应的申请头信息:

/** * 构建 map 信息 * @param req 申请 * @return 后果 * @since 1.0.0 */private Map<String, String> buildHeaderMap(final HttpServletRequest req) {    Map<String, String> map = new HashMap<>();    Enumeration<String> enumeration = req.getHeaderNames();    while (enumeration.hasMoreElements()) {        String name = enumeration.nextElement();        String value = req.getHeader(name);        map.put(name, value);    }    return map;}

服务发现

当咱们解析出申请的利用时 appName = test 时,就能够去查问配置核心中 test 利用中对应的 ip:port 信息。

@Overridepublic String buildRequestUrl(Pair<String, String> pair) {    String appName = pair.getValueOne();    String appUrl = pair.getValueTwo();    String ipPort = "127.0.0.1:8081";    //TODO: 依据数据库配置查问    // 依据是否启用 HTTPS 拜访不同的地址    if (appName.equals("test")) {        // 这里须要波及到负载平衡        ipPort = "127.0.0.1:8081";    } else {        throw new GatewayServerException(GatewayServerRespCode.APP_NAME_NOT_FOUND_IP);    }    String format = "http://%s/%s";    return String.format(format, ipPort, appUrl);}

这里临时固定写死,最初返回理论服务端的申请地址。

这里也能够联合具体的负载平衡/路由策略,做进一步的服务端抉择。

不同 Method

HTTP 反对的形式是多样的,咱们临时反对一下 GET/POST 申请。

实质上就是针对 GET/POST 申请,构建模式的申请调用服务端。

这里的实现形式能够十分多样,此处以 ok-http 客户端为例作为实现。

接口定义

为了便于前期拓展,所有的 Method 调用,实现雷同的接口:

public interface IMethodType {    /**     * 解决     * @param context 上下文     * @return 后果     */    IMethodTypeResult handle(final IMethodTypeContext context);}

GET

GET 申请。

@Service@MethodTypeRoute("GET")public class GetMethodType implements IMethodType {    @Override    public IMethodTypeResult handle(IMethodTypeContext context) {        String respBody = OkHttpUtil.get(context.url(), context.headerMap());        return MethodTypeResult.newInstance().respJson(respBody);    }}

POST

POST 申请。

@Service@MethodTypeRoute("POST")public class PostMethodType implements IMethodType {    @Override    public IMethodTypeResult handle(IMethodTypeContext context) {        HttpServletRequest req = (HttpServletRequest) context.servletRequest();        String postJson = HttpUtil.getPostBody(req);        String respBody = OkHttpUtil.post(context.url(), postJson, context.headerMap());        return MethodTypeResult.newInstance().respJson(respBody);    }}

OkHttpUtil 实现

OkHttpUtil 是基于 ok-http 封装的 http 调用工具类。

import com.github.houbb.gateway.server.util.exception.GatewayServerException;import com.github.houbb.heaven.util.util.MapUtil;import okhttp3.*;import java.io.IOException;import java.util.Map;/** * @author binbin.hou * @since 1.0.0 */public class OkHttpUtil {    private static final MediaType JSON            = MediaType.parse("application/json; charset=utf-8");    /**     * get 申请     * @param url 地址     * @return 后果     * @since 1.0.0     */    public static String get(final String url) {        return get(url, null);    }    /**     * get 申请     * @param url 地址     * @param headerMap 申请头     * @return 后果     * @since 1.0.0     */    public static String get(final String url,                             final Map<String, String> headerMap) {        try {            OkHttpClient client = new OkHttpClient();            Request.Builder builder = new Request.Builder();            builder.url(url);            if(MapUtil.isNotEmpty(headerMap)) {                for(Map.Entry<String, String> entry : headerMap.entrySet()) {                    builder.header(entry.getKey(), entry.getValue());                }            }            Request request = builder                    .build();            Response response = client.newCall(request).execute();            return response.body().string();        } catch (IOException e) {            throw new GatewayServerException(e);        }    }    /**     * get 申请     * @param url 地址     * @param body 申请体     * @param headerMap 申请头     * @return 后果     * @since 1.0.0     */    public static String post(final String url,                              final RequestBody body,                             final Map<String, String> headerMap) {        try {            OkHttpClient client = new OkHttpClient();            Request.Builder builder = new Request.Builder();            builder.post(body);            builder.url(url);            if(MapUtil.isNotEmpty(headerMap)) {                for(Map.Entry<String, String> entry : headerMap.entrySet()) {                    builder.header(entry.getKey(), entry.getValue());                }            }            Request request = builder.build();            Response response = client.newCall(request).execute();            return response.body().string();        } catch (IOException e) {            throw new GatewayServerException(e);        }    }    /**     * get 申请     * @param url 地址     * @param bodyJson 申请体 JSON     * @param headerMap 申请头     * @return 后果     * @since 1.0.0     */    public static String post(final String url,                              final String bodyJson,                              final Map<String, String> headerMap) {        RequestBody body = RequestBody.create(JSON, bodyJson);        return post(url, body, headerMap);    }}

调用后果解决

申请完服务端之后,咱们须要对后果进行解决。

第一版的实现十分粗犷:

/** * 解决最初的后果 * @param methodTypeResult 后果 * @param servletResponse 响应 * @since 1.0.0 */private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,                                    final ServletResponse servletResponse) {    try {        final String respBody = methodTypeResult.respJson();        // 重定向(因为网络安全等起因,这个计划应该被废除。)        // 这里能够从新定向,也能够通过 http client 进行申请。        // GET/POST        //获取字符输入流对象        servletResponse.setCharacterEncoding("UTF-8");        servletResponse.setContentType("text/html;charset=utf-8");        servletResponse.getWriter().write(respBody);    } catch (IOException e) {        throw new GatewayServerException(e);    }}

残缺实现

咱们把下面的次要流程放在一起,残缺的实现如下:

import com.alibaba.fastjson.JSON;import com.github.houbb.gateway.server.util.exception.GatewayServerException;import com.github.houbb.gateway.server.web.biz.IRequestAppBiz;import com.github.houbb.gateway.server.web.method.IMethodType;import com.github.houbb.gateway.server.web.method.IMethodTypeContext;import com.github.houbb.gateway.server.web.method.IMethodTypeResult;import com.github.houbb.gateway.server.web.method.impl.MethodHandlerContainer;import com.github.houbb.gateway.server.web.method.impl.MethodTypeContext;import com.github.houbb.heaven.support.tuple.impl.Pair;import com.github.houbb.heaven.util.lang.StringUtil;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;/** * 网关过滤器 * * @author binbin.hou * @since 1.0.0 */@WebFilter@Componentpublic class GatewayFilter implements Filter {    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);    @Autowired    private IRequestAppBiz requestAppBiz;    @Autowired    private MethodHandlerContainer methodHandlerContainer;    public void init(FilterConfig filterConfig) throws ServletException {    }    public void doFilter(ServletRequest servletRequest,                         ServletResponse servletResponse,                         FilterChain filterChain) throws IOException, ServletException {        HttpServletRequest req = (HttpServletRequest) servletRequest;        LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));        //依据 URL 获取对应的服务名称        Pair<String, String> pair = requestAppBiz.getRequestPair(req);        Map<String, String> headerMap = buildHeaderMap(req);        String appName = pair.getValueOne();        if(StringUtil.isNotEmptyTrim(appName)) {            String method = req.getMethod();            String respBody = null;            String url = requestAppBiz.buildRequestUrl(pair);            //TODO: 其余办法的反对            IMethodType methodType = methodHandlerContainer.getMethodType(method);            IMethodTypeContext typeContext = MethodTypeContext.newInstance()                    .methodType(method)                    .url(url)                    .servletRequest(servletRequest)                    .servletResponse(servletResponse)                    .headerMap(headerMap);            // 执行前            // 执行            IMethodTypeResult methodTypeResult = methodType.handle(typeContext);            // 执行后            // 后果的解决            this.methodTypeResultHandle(methodTypeResult, servletResponse);        } else {            filterChain.doFilter(req, servletResponse);        }    }    public void destroy() {    }    /**     * 解决最初的后果     * @param methodTypeResult 后果     * @param servletResponse 响应     * @since 1.0.0     */    private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,                                        final ServletResponse servletResponse) {        try {            final String respBody = methodTypeResult.respJson();            // 重定向(因为网络安全等起因,这个计划应该被废除。)            // 这里能够从新定向,也能够通过 http client 进行申请。            // GET/POST            //获取字符输入流对象            servletResponse.setCharacterEncoding("UTF-8");            servletResponse.setContentType("text/html;charset=utf-8");            servletResponse.getWriter().write(respBody);        } catch (IOException e) {            throw new GatewayServerException(e);        }    }    /**     * 构建 map 信息     * @param req 申请     * @return 后果     * @since 1.0.0     */    private Map<String, String> buildHeaderMap(final HttpServletRequest req) {        Map<String, String> map = new HashMap<>();        Enumeration<String> enumeration = req.getHeaderNames();        while (enumeration.hasMoreElements()) {            String name = enumeration.nextElement();            String value = req.getHeader(name);            map.put(name, value);        }        return map;    }}

网关验证

网关利用

咱们把拦截器加好当前,定义对应的 Application 如下:

@SpringBootApplication@ServletComponentScanpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}

而后把网关启动起来,启动端口号为 8080

服务端利用

而后启动服务端对应的服务,端口号为 8081。

查看版本号的控制器实现:

@RestControllerpublic class MonitorController {    @RequestMapping(value = "version", method = RequestMethod.GET)    public String version() {        return "1.0-demo";    }}

申请

咱们在浏览器上间接拜访 api 网关:

http://localhost:8080/test/version

页面返回:

1.0-demo

小结

API 网关实现的原理并不难,就是基于 servlet 对申请进行转发。

尽管看起来简略,然而能够在这个根底上实现更多弱小的个性,比方限流,日志,监控等等。

如果你对 API 网关感兴趣的话,无妨关注一波,后续内容,更加精彩。

备注:波及的代码较多,文中做了简化。如果你对全副源码感兴趣,能够關註【老马啸东风】,後臺回復【网关】即可取得。

我是老马,期待与你的下次重逢。