Преамбула
Предыдущая статья:Вы даже не знаете сигнатуру внешнего интерфейса? Еще есть время научиться.
Есть много мелких партнеров, которые ответили, что соответствующая подпись и проверка во внешнем API могут быть обработаны шлюзом.
Когда дело доходит до шлюзов, все должны быть с ними знакомы. На рынке широко используются: весеннее облако/конг/соул.
Роль шлюза API
(1) Проверка разрешений во внешнем интерфейсе
(2) Ограничения на количество и частоту интерфейсных вызовов
(3) Балансировка нагрузки, кэширование, маршрутизация, контроль доступа, прокси-сервер службы, мониторинг, ведение журнала и т. д. в шлюзе микрослужб.
Принцип реализации
В общих запросах доступ к серверу осуществляется напрямую через клиента.Нам нужно реализовать уровень API-шлюза посередине, внешний клиент обращается к шлюзу, а затем шлюз перенаправляет вызов.
основной процесс
Шлюз звучит очень сложно, а основная часть на самом деле основана на сервлете.javax.servlet.Filter
реализовать.
Мы позволяем клиенту вызывать шлюз, а затем равномерно анализируем и пересылаем заголовок сообщения в фильтре, вызываем сервер, а затем инкапсулируем его и возвращаем клиенту.
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() {
}
}
Далее нам просто нужно сосредоточиться на том, как переопределить метод дофильтра.
Выполнение
получить имя приложения
Шлюз предназначен для всех приложений внутри компании, и мы можем отличить его по уникальному appName каждой службы.
Например, если имя приложения test, то вызывается запрос шлюза:
https://gateway.com/test/version
Для этого запроса соответствующее имя приложения — 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, мы можем запросить соответствующую информацию 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);
}
Здесь запись временно фиксируется, и, наконец, возвращается адрес запроса фактического сервера.
Здесь вы также можете комбинировать определенные стратегии балансировки нагрузки/маршрутизации для дальнейшего выбора сервера.
Другой метод
Существуют различные способы поддержки HTTP. Мы временно поддерживаем запросы GET/POST.
По сути, это запрос на GET/POST-запросы для построения в виде сервера вызовов.
Здесь реализация может быть самой разнообразной, вот пример реализации клиента ok-http.
Определение интерфейса
Чтобы облегчить последующее расширение, все вызовы метода реализуют один и тот же интерфейс:
public interface IMethodType {
/**
* 处理
* @param context 上下文
* @return 结果
*/
IMethodTypeResult handle(final IMethodTypeContext context);
}
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 основан на инструментах http-вызова пакета ok-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;
}
}
Аутентификация шлюза
Приложение шлюза
После того, как мы добавим перехватчик, определите соответствующее приложение следующим образом:
@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-шлюза не сложен, он заключается в пересылке запросов на основе сервлетов.
Хотя это выглядит просто, на этой основе могут быть реализованы более мощные функции, такие как ограничение тока, ведение журнала, мониторинг и многое другое.
Если вы заинтересованы в шлюзе API, вы можете обратить внимание на волну, и последующий контент будет более захватывающим.
Примечания: используется много кодов, которые упрощены в тексте. Если вас интересует весь исходный код, вы можете подписаться на [Lao Ma Xiao Xifeng] и ответить [Gateway] в фоновом режиме, чтобы получить его.
Я старая лошадь, и я с нетерпением жду встречи с вами в следующий раз.