共计 10150 个字符,预计需要花费 26 分钟才能阅读完成。
序言
上一篇文章:你连对外接口签名都不会晓得?有工夫还是要学习学习。
有很多小伙伴反馈,对外的 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
@Component
public 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。
具体实现也比较简单:
@Override
public 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 信息。
@Override
public 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
@Component
public 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
@ServletComponentScan
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);
}
}
而后把网关启动起来,启动端口号为 8080
服务端利用
而后启动服务端对应的服务,端口号为 8081。
查看版本号的控制器实现:
@RestController
public 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 网关感兴趣的话,无妨关注一波,后续内容,更加精彩。
备注:波及的代码较多,文中做了简化。如果你对全副源码感兴趣,能够關註【老马啸东风】,後臺回復【网关】即可取得。
我是老马,期待与你的下次重逢。