восстановить фон
Все делали приложения архитектуры b-s, то есть программные приложения на основе браузера. Теперь есть сцена, где сторона FE, то есть фронтенд-инжиниринг, отделена от фронтенда и бэкэнда и написана с использованием основного фреймворка фронтенда VUE. Сервер использует архитектуру springBoot.Теперь есть еще одна служба, которая также должна взаимодействовать с интерфейсной страницей, но поскольку логика системы аутентификации и входа в систему, а также логика распределенного хранилища сеансов находятся в службе 1, когда интерфейс взаимодействует с сервером 1, процесс аутентификации не помещается в шлюз. Поэтому, когда новый сервис взаимодействует с интерфейсом, нет необходимости повторно писать набор аутентификации и логики аутентификации. Наконец, я хочу перенаправить фиксированный внешний запрос на недавно добавленную службу 2 через прокси через службу 1.
как добиться
Идея: клиент отправляет запрос, прокси-сервер сопоставляет содержимое запроса, а затем действует как прокси для доступа к реальному серверу, и, наконец, реальный сервер возвращает ответ прокси, а прокси возвращается в браузер. Технология: Когда речь заходит об обратном прокси, первое, что приходит на ум, это nginx. Однако в наших нуждах больше требований к процессу переадресации:
- Необходимо управлять сеансом и определять поведение пересылки в соответствии со значением сеанса.
- Необходимо изменить сообщение Http, добавить заголовок или строку запроса.
Первый пункт определяет, что наша реализация должна быть основана на Servlet. ProxyServlet, предоставляемый springboot, может удовлетворить наши требования.ProxyServlet напрямую наследуется от HttpServlet и вызывает внутренний сервер асинхронно, поэтому нет проблем с эффективностью, а различные перезагружаемые функции также обеспечивают относительно мощный механизм настройки.
Процесс реализации
- импортировать зависимости
<dependency>
<groupId>org.mitre.dsmiley.httpproxy</groupId>
<artifactId>smiley-http-proxy-servlet</artifactId>
<version>1.11</version>
</dependency>
- Создайте класс конфигурации
@Configuration
public class ProxyServletConfiguration {
private final static String REPORT_URL = "/newReport_proxy/*";
@Bean
public ServletRegistrationBean proxyServletRegistration() {
List<String> list = new ArrayList<>();
list.add(REPORT_URL); //如果需要匹配多个url则定义好放到list中即可
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new ThreeProxyServlet());
registrationBean.setUrlMappings(list);
//设置默认网址以及参数
Map<String, String> params = ImmutableMap.of("targetUri", "null", "log", "true");
registrationBean.setInitParameters(params);
return registrationBean;
}
}
- Написать логику прокси
public class ThreeProxyServlet extends ProxyServlet {
private static final long serialVersionUID = -9125871545605920837L;
private final Logger logger = LoggerFactory.getLogger(ThreeProxyServlet.class);
public String proxyHttpAddr;
public String proxyName;
private ResourceBundle bundle =null;
@Override
public void init() throws ServletException {
bundle = ResourceBundle.getBundle("prop");
super.init();
}
@Override
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
// 初始切换路径
String requestURI = servletRequest.getRequestURI();
proxyName = requestURI.split("/")[2];
//根据name匹配域名到properties文件中获取
proxyHttpAddr = bundle.getString(proxyName);
String url = proxyHttpAddr;
if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
servletRequest.setAttribute(ATTR_TARGET_URI, url);
}
if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
URL trueUrl = new URL(url);
servletRequest.setAttribute(ATTR_TARGET_HOST, new HttpHost(trueUrl.getHost(), trueUrl.getPort(), trueUrl.getProtocol()));
}
String method = servletRequest.getMethod();
// 替换多余路径
String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);
Object proxyRequest;
if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
} else {
proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
}
this.copyRequestHeaders(servletRequest, (HttpRequest)proxyRequest);
setXForwardedForHeader(servletRequest, (HttpRequest)proxyRequest);
HttpResponse proxyResponse = null;
try {
proxyResponse = this.doExecute(servletRequest, servletResponse, (HttpRequest)proxyRequest);
int statusCode = proxyResponse.getStatusLine().getStatusCode();
servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());
this.copyResponseHeaders(proxyResponse, servletRequest, servletResponse);
if (statusCode == 304) {
servletResponse.setIntHeader("Content-Length", 0);
} else {
this.copyResponseEntity(proxyResponse, servletResponse, (HttpRequest)proxyRequest, servletRequest);
}
} catch (Exception var11) {
this.handleRequestException((HttpRequest)proxyRequest, var11);
} finally {
if (proxyResponse != null) {
EntityUtils.consumeQuietly(proxyResponse.getEntity());
}
}
}
@Override
protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse, HttpRequest proxyRequest) throws IOException {
HttpResponse response = null;
// 拦截校验 可自定义token过滤
//String token = servletRequest.getHeader("ex_proxy_token");
// 代理服务鉴权逻辑
this.getAuthString(proxyName,servletRequest,proxyRequest);
//执行代理转发
try {
response = super.doExecute(servletRequest, servletResponse, proxyRequest);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
}
- Добавьте файл конфигурации свойств
newReport_proxy = https://www.baidu.com
Приведенная выше конфигурация кратко представлена для метода записи /newReport_proxy/*, это означает, что когда ваш путь запроса начинается с newReport_proxy, напримерhttp://localhost:8080/newReport_proxy/test/get1Такой путь, реальный путь, который он запрашивает,www.baidu.com/test/get1. Главное заменить newReport_proxy на соответствующий проксируемый путь.* означает путь интерфейса в реальном прокси-проекте запроса.Эта конфигурация действительна как для get, так и для post запросов.
Столкнуться с проблемами
В соответствии с приведенной выше конфигурацией при выполнении прокси-переадресации интерфейс прокси-сервера пересылки должен быть аутентифицирован.Конкретный вызов схемы аутентификации — это «this.getAuthString(proxyName,servletRequest,proxyRequest);» этот код. Логика аутентификации прокси-сервиса вычисляет значение по алгоритму после входного параметра + значение токена, а затем передает его в заголовке. Тогда есть проблема, то есть, когда фронтенд использует метод requestBody для вызова запроса, будет ошибка, когда сервис 1 выполняет прокси-форвардинг:Застрял на выполнении метода doExecute(). После операции отладки найдите точку, то есть точку, где выполняется последний триггер для выполнения вызова службы прокси:Исключение выдается в позиции на приведенном выше рисунке Значение i на приведенном выше рисунке равно -1, что указывает на то, что в этом sessionBuffer нет данных и их невозможно прочитать, поэтому возвращается -1. Итак, что же такое этот sessionBuffer? Эта вещь переводится как ссылка на входной буфер сеанса, который блокирует соединение. Подобно классу InputStream, он также предоставляет методы для чтения строк текста. То есть поток данных, соответствующий запросу, отправляется на целевой сервис через этот класс. Ошибка в этой позиции указывает на то, что поток данных, который нужно отправить, отсутствует, поэтому когда запрошенная информация о потоке данных будет потеряна? То есть мы добавляем некоторую логику аутентификации, логика аутентификации должна получить параметры в requestBody, а параметры считываются из объекта запроса через поток. Мы также сталкивались с этой проблемой. Обычно содержимое тела в HttpServletRequest считывается только один раз, но в некоторых ситуациях оно может считываться несколько раз. Поскольку содержимое тела существует в виде потока, при первом его чтении После завершение, он не может быть прочитан во второй раз.Типичный сценарий заключается в том, что после того, как фильтр завершит проверку содержимого тела, бизнес-метод не может продолжить чтение потока, что приводит к ошибке синтаксического анализа.
наконец понял
Идея: Используйте декоратор, чтобы украсить запрос, чтобы он мог обернуть прочитанное содержимое для многократного чтения. На самом деле, Spring Boot предоставляет простую оболочку ContentCachingRequestWrapper, Из исходного кода эта оболочка нецелесообразна, Она не инкапсулирует базовую информацию о потоке ServletInputStream из http, поэтому в этом сценарии соответствующую информацию о потоке нельзя получить повторно.
- Реализуйте кэш потока, обратившись к классу ContentCachingRequestWrapper.
public class CacheStreamHttpRequest extends HttpServletRequestWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(CacheStreamHttpRequest.class);
private final ByteArrayOutputStream cachedContent;
private Map<String, String[]> cachedForm;
@Nullable
private ServletInputStream inputStream;
public CacheStreamHttpRequest(HttpServletRequest request) {
super(request);
this.cachedContent = new ByteArrayOutputStream();
this.cachedForm = new HashMap<>();
cacheData();
}
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
return this.inputStream;
}
@Override
public String getCharacterEncoding() {
String enc = super.getCharacterEncoding();
return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
@Override
public String getParameter(String name) {
String value = null;
if (isFormPost()) {
String[] values = cachedForm.get(name);
if (null != values && values.length > 0) {
value = values[0];
}
}
if (StringUtils.isEmpty(value)) {
value = super.getParameter(name);
}
return value;
}
@Override
public Map<String, String[]> getParameterMap() {
if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
return cachedForm;
}
return super.getParameterMap();
}
@Override
public Enumeration<String> getParameterNames() {
if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
return Collections.enumeration(cachedForm.keySet());
}
return super.getParameterNames();
}
@Override
public String[] getParameterValues(String name) {
if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
return cachedForm.get(name);
}
return super.getParameterValues(name);
}
private void cacheData() {
try {
if (isFormPost()) {
this.cachedForm = super.getParameterMap();
} else {
ServletInputStream inputStream = super.getInputStream();
IOUtils.copy(inputStream, this.cachedContent);
}
} catch (IOException e) {
LOGGER.warn("[RepeatReadHttpRequest:cacheData], error: {}", e.getMessage());
}
}
private boolean isFormPost() {
String contentType = getContentType();
return (contentType != null &&
(contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ||
contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) &&
HttpMethod.POST.matches(getMethod()));
}
private static class RepeatReadInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public RepeatReadInputStream(byte[] bytes) {
this.inputStream = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
@Override
public int readLine(byte[] b, int off, int len) throws IOException {
return this.inputStream.read(b, off, len);
}
@Override
public boolean isFinished() {
return this.inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
}
}
Основная логика вышеприведенного класса заключается в кэшировании объекта запроса с помощью метода cacheData() и сохранении его в классе ByteArrayOutputStream.При вызове объекта запроса для получения метода getInputStream() запишите код ядра InputStream из класса ByteArrayOutputStream. :
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
return this.inputStream;
}
При использовании этого инкапсулированного запроса вам необходимо заменить исходный запрос фильтром, зарегистрировать фильтр и заменить исходный запрос инкапсулированным классом в цепочке вызовов. Код:
//chain.doFilter(request, response);
//换掉原来的request对象 用new RepeatReadHttpRequest((HttpServletRequest) request) 因为后者流中由缓存拦截器httprequest替换 可重复获取inputstream
chain.doFilter(new RepeatReadHttpRequest((HttpServletRequest) request), response);
Это решает набор логики для распространения сервисного прокси + аутентификация прокси-сервиса.