smiley-http-proxy-servlet 实现springboot 接口反向代理,站点代理,项目鉴权,安全的引入第三方项目服务

8 篇文章 0 订阅

背景:
项目初期 和硬件集成,实现了些功能服务,由于是局域网环境,安全问题当时都可以最小化无视。随着对接的服务越来越多,部分功能上云,此时就需要有一种手段可以控制到其他项目/接口的访问权限。 无疑 反向代理是最轻快的解决办法。

反向代理 集成第三方的服务接口或web监控界面,并与项目实现的鉴权方法

依赖 smiley-http-proxy-servlet GitHub链接

  2.0 版开始,代理切换到jakarta servlet-api
  <!--HTTP 代理 Servlet-->
   <dependency>
       <groupId>org.mitre.dsmiley.httpproxy</groupId>
       <artifactId>smiley-http-proxy-servlet</artifactId>
       <version>2.0</version>
	</dependency>

javax servlet-api 请选择


<dependency>
    <groupId>org.mitre.dsmiley.httpproxy</groupId>
    <artifactId>smiley-http-proxy-servlet</artifactId>
    <version>${smiley-http-proxy-servlet.version}</version>
    <classifier>javax</classifier>
</dependency>

单文件 直接用代码 ProxyServlet.java

/*
 * Copyright MITRE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mitre.dsmiley.httpproxy;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.AbortableHttpRequest;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.HeaderGroup;
import org.apache.http.util.EntityUtils;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpCookie;
import java.net.URI;
import java.util.BitSet;
import java.util.Enumeration;
import java.util.Formatter;

/**
 * An HTTP reverse proxy/gateway servlet. It is designed to be extended for customization
 * if desired. Most of the work is handled by
 * <a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>.
 * <p>
 *   There are alternatives to a servlet based proxy such as Apache mod_proxy if that is available to you. However
 *   this servlet is easily customizable by Java, secure-able by your web application's security (e.g. spring-security),
 *   portable across servlet engines, and is embeddable into another web application.
 * </p>
 * <p>
 *   Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html
 * </p>
 *
 * @author David Smiley dsmiley@apache.org
 */
@SuppressWarnings({"deprecation", "serial", "WeakerAccess"})
public class ProxyServlet extends HttpServlet {

  /* INIT PARAMETER NAME CONSTANTS */

  /** A boolean parameter name to enable logging of input and target URLs to the servlet log. */
  public static final String P_LOG = "log";

  /** A boolean parameter name to enable forwarding of the client IP  */
  public static final String P_FORWARDEDFOR = "forwardip";

  /** A boolean parameter name to keep HOST parameter as-is  */
  public static final String P_PRESERVEHOST = "preserveHost";

  /** A boolean parameter name to keep COOKIES as-is  */
  public static final String P_PRESERVECOOKIES = "preserveCookies";

  /** A boolean parameter name to keep COOKIE path as-is  */
  public static final String P_PRESERVECOOKIEPATH = "preserveCookiePath";

  /** A boolean parameter name to have auto-handle redirects */
  public static final String P_HANDLEREDIRECTS = "http.protocol.handle-redirects"; // ClientPNames.HANDLE_REDIRECTS

  /** An integer parameter name to set the socket connection timeout (millis) */
  public static final String P_CONNECTTIMEOUT = "http.socket.timeout"; // CoreConnectionPNames.SO_TIMEOUT

  /** An integer parameter name to set the socket read timeout (millis) */
  public static final String P_READTIMEOUT = "http.read.timeout";

  /** An integer parameter name to set the connection request timeout (millis) */
  public static final String P_CONNECTIONREQUESTTIMEOUT = "http.connectionrequest.timeout";

  /** An integer parameter name to set max connection number */
  public static final String P_MAXCONNECTIONS = "http.maxConnections";

  /** A boolean parameter whether to use JVM-defined system properties to configure various networking aspects. */
  public static final String P_USESYSTEMPROPERTIES = "useSystemProperties";

  /** A boolean parameter to enable handling of compression in the servlet. If it is false, compressed streams are passed through unmodified. */
  public static final String P_HANDLECOMPRESSION = "handleCompression";

  /** The parameter name for the target (destination) URI to proxy to. */
  public static final String P_TARGET_URI = "targetUri";

  protected static final String ATTR_TARGET_URI =
          ProxyServlet.class.getSimpleName() + ".targetUri";
  protected static final String ATTR_TARGET_HOST =
          ProxyServlet.class.getSimpleName() + ".targetHost";

  /* MISC */

  protected boolean doLog = false;
  protected boolean doForwardIP = true;
  /** User agents shouldn't send the url fragment but what if it does? */
  protected boolean doSendUrlFragment = true;
  protected boolean doPreserveHost = false;
  protected boolean doPreserveCookies = false;
  protected boolean doPreserveCookiePath = false;
  protected boolean doHandleRedirects = false;
  protected boolean useSystemProperties = true;
  protected boolean doHandleCompression = false;
  protected int connectTimeout = -1;
  protected int readTimeout = -1;
  protected int connectionRequestTimeout = -1;
  protected int maxConnections = -1;

  //These next 3 are cached here, and should only be referred to in initialization logic. See the
  // ATTR_* parameters.
  /** From the configured parameter "targetUri". */
  protected String targetUri;
  protected URI targetUriObj;//new URI(targetUri)
  protected HttpHost targetHost;//URIUtils.extractHost(targetUriObj);

  private HttpClient proxyClient;

  @Override
  public String getServletInfo() {
    return "A proxy servlet by David Smiley, dsmiley@apache.org";
  }


  protected String getTargetUri(HttpServletRequest servletRequest) {
    return (String) servletRequest.getAttribute(ATTR_TARGET_URI);
  }

  protected HttpHost getTargetHost(HttpServletRequest servletRequest) {
    return (HttpHost) servletRequest.getAttribute(ATTR_TARGET_HOST);
  }

  /**
   * Reads a configuration parameter. By default it reads servlet init parameters but
   * it can be overridden.
   */
  protected String getConfigParam(String key) {
    return getServletConfig().getInitParameter(key);
  }

  @Override
  public void init() throws ServletException {
    String doLogStr = getConfigParam(P_LOG);
    if (doLogStr != null) {
      this.doLog = Boolean.parseBoolean(doLogStr);
    }

    String doForwardIPString = getConfigParam(P_FORWARDEDFOR);
    if (doForwardIPString != null) {
      this.doForwardIP = Boolean.parseBoolean(doForwardIPString);
    }

    String preserveHostString = getConfigParam(P_PRESERVEHOST);
    if (preserveHostString != null) {
      this.doPreserveHost = Boolean.parseBoolean(preserveHostString);
    }

    String preserveCookiesString = getConfigParam(P_PRESERVECOOKIES);
    if (preserveCookiesString != null) {
      this.doPreserveCookies = Boolean.parseBoolean(preserveCookiesString);
    }

    String preserveCookiePathString = getConfigParam(P_PRESERVECOOKIEPATH);
    if (preserveCookiePathString != null) {
      this.doPreserveCookiePath = Boolean.parseBoolean(preserveCookiePathString);
    }

    String handleRedirectsString = getConfigParam(P_HANDLEREDIRECTS);
    if (handleRedirectsString != null) {
      this.doHandleRedirects = Boolean.parseBoolean(handleRedirectsString);
    }

    String connectTimeoutString = getConfigParam(P_CONNECTTIMEOUT);
    if (connectTimeoutString != null) {
      this.connectTimeout = Integer.parseInt(connectTimeoutString);
    }

    String readTimeoutString = getConfigParam(P_READTIMEOUT);
    if (readTimeoutString != null) {
      this.readTimeout = Integer.parseInt(readTimeoutString);
    }

    String connectionRequestTimeout = getConfigParam(P_CONNECTIONREQUESTTIMEOUT);
    if (connectionRequestTimeout != null) {
      this.connectionRequestTimeout = Integer.parseInt(connectionRequestTimeout);
    }

    String maxConnections = getConfigParam(P_MAXCONNECTIONS);
    if (maxConnections != null) {
      this.maxConnections = Integer.parseInt(maxConnections);
    }

    String useSystemPropertiesString = getConfigParam(P_USESYSTEMPROPERTIES);
    if (useSystemPropertiesString != null) {
      this.useSystemProperties = Boolean.parseBoolean(useSystemPropertiesString);
    }

    String doHandleCompression = getConfigParam(P_HANDLECOMPRESSION);
    if (doHandleCompression != null) {
      this.doHandleCompression = Boolean.parseBoolean(doHandleCompression);
    }

    initTarget();//sets target*

    proxyClient = createHttpClient();
  }

  /**
   * Sub-classes can override specific behaviour of {@link org.apache.http.client.config.RequestConfig}.
   */
  protected RequestConfig buildRequestConfig() {
    return RequestConfig.custom()
            .setRedirectsEnabled(doHandleRedirects)
            .setCookieSpec(CookieSpecs.IGNORE_COOKIES) // we handle them in the servlet instead
            .setConnectTimeout(connectTimeout)
            .setSocketTimeout(readTimeout)
            .setConnectionRequestTimeout(connectionRequestTimeout)
            .build();
  }

  /**
   * Sub-classes can override specific behaviour of {@link org.apache.http.config.SocketConfig}.
   */
  protected SocketConfig buildSocketConfig() {

    if (readTimeout < 1) {
      return null;
    }

    return SocketConfig.custom()
            .setSoTimeout(readTimeout)
            .build();
  }

  protected void initTarget() throws ServletException {
    targetUri = getConfigParam(P_TARGET_URI);
    if (targetUri == null)
      throw new ServletException(P_TARGET_URI+" is required.");
    //test it's valid
    try {
      targetUriObj = new URI(targetUri);
    } catch (Exception e) {
      throw new ServletException("Trying to process targetUri init parameter: "+e,e);
    }
    targetHost = URIUtils.extractHost(targetUriObj);
  }

  /**
   * Called from {@link #init(jakarta.servlet.ServletConfig)}.
   * HttpClient offers many opportunities for customization.
   * In any case, it should be thread-safe.
   */
  protected HttpClient createHttpClient() {
    HttpClientBuilder clientBuilder = getHttpClientBuilder()
                                        .setDefaultRequestConfig(buildRequestConfig())
                                        .setDefaultSocketConfig(buildSocketConfig());

    clientBuilder.setMaxConnTotal(maxConnections);
    clientBuilder.setMaxConnPerRoute(maxConnections);
    if(! doHandleCompression) {
      clientBuilder.disableContentCompression();
    }

    if (useSystemProperties)
      clientBuilder = clientBuilder.useSystemProperties();
    return buildHttpClient(clientBuilder);
  }

  /**
   * Creates a HttpClient from the given builder. Meant as postprocessor
   * to possibly adapt the client builder prior to creating the
   * HttpClient.
   *
   * @param clientBuilder pre-configured client builder
   * @return HttpClient
   */
  protected HttpClient buildHttpClient(HttpClientBuilder clientBuilder) {
    return clientBuilder.build();
  }

  /**
   * Creates a {@code HttpClientBuilder}. Meant as preprocessor to possibly
   * adapt the client builder prior to any configuration got applied.
   *
   * @return HttpClient builder
   */
  protected HttpClientBuilder getHttpClientBuilder() {
    return HttpClientBuilder.create();
  }

  /**
   * The http client used.
   * @see #createHttpClient()
   */
  protected HttpClient getProxyClient() {
    return proxyClient;
  }

  @Override
  public void destroy() {
    //Usually, clients implement Closeable:
    if (proxyClient instanceof Closeable) {
      try {
        ((Closeable) proxyClient).close();
      } catch (IOException e) {
        log("While destroying servlet, shutting down HttpClient: "+e, e);
      }
    } else {
      //Older releases require we do this:
      if (proxyClient != null)
        proxyClient.getConnectionManager().shutdown();
    }
    super.destroy();
  }

  @Override
  protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
      throws ServletException, IOException {
    //initialize request attributes from caches if unset by a subclass by this point
    if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
      servletRequest.setAttribute(ATTR_TARGET_URI, targetUri);
    }
    if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
      servletRequest.setAttribute(ATTR_TARGET_HOST, targetHost);
    }

    // Make the Request
    //note: we won't transfer the protocol version because I'm not sure it would truly be compatible
    String method = servletRequest.getMethod();
    String proxyRequestUri = rewriteUrlFromRequest(servletRequest);
    HttpRequest proxyRequest;
    //spec: RFC 2616, sec 4.3: either of these two headers signal that there is a message body.
    if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
        servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) {
      proxyRequest = newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
    } else {
      proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
    }

    copyRequestHeaders(servletRequest, proxyRequest);

    setXForwardedForHeader(servletRequest, proxyRequest);

    HttpResponse proxyResponse = null;
    try {
      // Execute the request
      proxyResponse = doExecute(servletRequest, servletResponse, proxyRequest);

      // Process the response:

      int statusCode = proxyResponse.getStatusLine().getStatusCode();
      servletResponse.setStatus(statusCode);

      // Copying response headers to make sure SESSIONID or other Cookie which comes from the remote
      // server will be saved in client when the proxied url was redirected to another one.
      // See issue [#51](https://github.com/mitre/HTTP-Proxy-Servlet/issues/51)
      copyResponseHeaders(proxyResponse, servletRequest, servletResponse);

      if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {
        // 304 needs special handling.  See:
        // http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304
        // Don't send body entity/content!
        servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
      } else {
        // Send the content to the client
        copyResponseEntity(proxyResponse, servletResponse, proxyRequest, servletRequest);
      }

    } catch (Exception e) {
      handleRequestException(proxyRequest, proxyResponse, e);
    } finally {
      // make sure the entire entity was consumed, so the connection is released
      if (proxyResponse != null)
        EntityUtils.consumeQuietly(proxyResponse.getEntity());
      //Note: Don't need to close servlet outputStream:
      // http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter
    }
  }

  protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResponse, Exception e) throws ServletException, IOException {
    //abort request, according to best practice with HttpClient
    if (proxyRequest instanceof AbortableHttpRequest) {
      AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest;
      abortableHttpRequest.abort();
    }
    // If the response is a chunked response, it is read to completion when
    // #close is called. If the sending site does not timeout or keeps sending,
    // the connection will be kept open indefinitely. Closing the respone
    // object terminates the stream.
    if (proxyResponse instanceof Closeable) {
      ((Closeable) proxyResponse).close();
    }
    if (e instanceof RuntimeException)
      throw (RuntimeException)e;
    if (e instanceof ServletException)
      throw (ServletException)e;
    //noinspection ConstantConditions
    if (e instanceof IOException)
      throw (IOException) e;
    throw new RuntimeException(e);
  }

  protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
                                   HttpRequest proxyRequest) throws IOException {
    if (doLog) {
      log("proxy " + servletRequest.getMethod() + " uri: " + servletRequest.getRequestURI() + " -- " +
              proxyRequest.getRequestLine().getUri());
    }
    return proxyClient.execute(getTargetHost(servletRequest), proxyRequest);
  }

  protected HttpRequest newProxyRequestWithEntity(String method, String proxyRequestUri,
                                                HttpServletRequest servletRequest)
          throws IOException {
    HttpEntityEnclosingRequest eProxyRequest =
            new BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
    // Add the input entity (streamed)
    //  note: we don't bother ensuring we close the servletInputStream since the container handles it
    eProxyRequest.setEntity(
            new InputStreamEntity(servletRequest.getInputStream(), getContentLength(servletRequest)));
    return eProxyRequest;
  }

  // Get the header value as a long in order to more correctly proxy very large requests
  private long getContentLength(HttpServletRequest request) {
    String contentLengthHeader = request.getHeader("Content-Length");
    if (contentLengthHeader != null) {
      return Long.parseLong(contentLengthHeader);
    }
    return -1L;
  }

  protected void closeQuietly(Closeable closeable) {
    try {
      closeable.close();
    } catch (IOException e) {
      log(e.getMessage(), e);
    }
  }

  /** These are the "hop-by-hop" headers that should not be copied.
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
   * I use an HttpClient HeaderGroup class instead of Set&lt;String&gt; because this
   * approach does case insensitive lookup faster.
   */
  protected static final HeaderGroup hopByHopHeaders;
  static {
    hopByHopHeaders = new HeaderGroup();
    String[] headers = new String[] {
        "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization",
        "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
    for (String header : headers) {
      hopByHopHeaders.addHeader(new BasicHeader(header, null));
    }
  }

  /**
   * Copy request headers from the servlet client to the proxy request.
   * This is easily overridden to add your own.
   */
  protected void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest) {
    // Get an Enumeration of all of the header names sent by the client
    @SuppressWarnings("unchecked")
    Enumeration<String> enumerationOfHeaderNames = servletRequest.getHeaderNames();
    while (enumerationOfHeaderNames.hasMoreElements()) {
      String headerName = enumerationOfHeaderNames.nextElement();
      copyRequestHeader(servletRequest, proxyRequest, headerName);
    }
  }

  /**
   * Copy a request header from the servlet client to the proxy request.
   * This is easily overridden to filter out certain headers if desired.
   */
  protected void copyRequestHeader(HttpServletRequest servletRequest, HttpRequest proxyRequest,
                                   String headerName) {
    //Instead the content-length is effectively set via InputStreamEntity
    if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH))
      return;
    if (hopByHopHeaders.containsHeader(headerName))
      return;
    // If compression is handled in the servlet, apache http client needs to
    // control the Accept-Encoding header, not the client
    if (doHandleCompression && headerName.equalsIgnoreCase(HttpHeaders.ACCEPT_ENCODING))
      return;

    @SuppressWarnings("unchecked")
    Enumeration<String> headers = servletRequest.getHeaders(headerName);
    while (headers.hasMoreElements()) {//sometimes more than one value
      String headerValue = headers.nextElement();
      // In case the proxy host is running multiple virtual servers,
      // rewrite the Host header to ensure that we get content from
      // the correct virtual server
      if (!doPreserveHost && headerName.equalsIgnoreCase(HttpHeaders.HOST)) {
        HttpHost host = getTargetHost(servletRequest);
        headerValue = host.getHostName();
        if (host.getPort() != -1)
          headerValue += ":"+host.getPort();
      } else if (!doPreserveCookies && headerName.equalsIgnoreCase(org.apache.http.cookie.SM.COOKIE)) {
        headerValue = getRealCookie(headerValue);
      }
      proxyRequest.addHeader(headerName, headerValue);
    }
  }

  private void setXForwardedForHeader(HttpServletRequest servletRequest,
                                      HttpRequest proxyRequest) {
    if (doForwardIP) {
      String forHeaderName = "X-Forwarded-For";
      String forHeader = servletRequest.getRemoteAddr();
      String existingForHeader = servletRequest.getHeader(forHeaderName);
      if (existingForHeader != null) {
        forHeader = existingForHeader + ", " + forHeader;
      }
      proxyRequest.setHeader(forHeaderName, forHeader);

      String protoHeaderName = "X-Forwarded-Proto";
      String protoHeader = servletRequest.getScheme();
      proxyRequest.setHeader(protoHeaderName, protoHeader);
    }
  }

  /** Copy proxied response headers back to the servlet client. */
  protected void copyResponseHeaders(HttpResponse proxyResponse, HttpServletRequest servletRequest,
                                     HttpServletResponse servletResponse) {
    for (Header header : proxyResponse.getAllHeaders()) {
      copyResponseHeader(servletRequest, servletResponse, header);
    }
  }

  /** Copy a proxied response header back to the servlet client.
   * This is easily overwritten to filter out certain headers if desired.
   */
  protected void copyResponseHeader(HttpServletRequest servletRequest,
                                  HttpServletResponse servletResponse, Header header) {
    String headerName = header.getName();
    if (hopByHopHeaders.containsHeader(headerName))
      return;
    String headerValue = header.getValue();
    if (headerName.equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE) ||
            headerName.equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE2)) {
      copyProxyCookie(servletRequest, servletResponse, headerValue);
    } else if (headerName.equalsIgnoreCase(HttpHeaders.LOCATION)) {
      // LOCATION Header may have to be rewritten.
      servletResponse.addHeader(headerName, rewriteUrlFromResponse(servletRequest, headerValue));
    } else {
      servletResponse.addHeader(headerName, headerValue);
    }
  }

  /**
   * Copy cookie from the proxy to the servlet client.
   * Replaces cookie path to local path and renames cookie to avoid collisions.
   */
  protected void copyProxyCookie(HttpServletRequest servletRequest,
                                 HttpServletResponse servletResponse, String headerValue) {
    for (HttpCookie cookie : HttpCookie.parse(headerValue)) {
      Cookie servletCookie = createProxyCookie(servletRequest, cookie);
      servletResponse.addCookie(servletCookie);
    }
  }

  /**
   * Creates a proxy cookie from the original cookie.
   *
   * @param servletRequest original request
   * @param cookie original cookie
   * @return proxy cookie
   */
  protected Cookie createProxyCookie(HttpServletRequest servletRequest, HttpCookie cookie) {
    String proxyCookieName = getProxyCookieName(cookie);
    Cookie servletCookie = new Cookie(proxyCookieName, cookie.getValue());
    servletCookie.setPath(this.doPreserveCookiePath ?
       cookie.getPath() : // preserve original cookie path
       buildProxyCookiePath(servletRequest) //set to the path of the proxy servlet
    );
    servletCookie.setComment(cookie.getComment());
    servletCookie.setMaxAge((int) cookie.getMaxAge());
    // don't set cookie domain
    servletCookie.setSecure(servletRequest.isSecure() && cookie.getSecure());
    servletCookie.setVersion(cookie.getVersion());
    servletCookie.setHttpOnly(cookie.isHttpOnly());
    return servletCookie;
  }

  /**
   * Set cookie name prefixed with a proxy value so it won't collide with other cookies.
   *
   * @param cookie cookie to get proxy cookie name for
   * @return non-conflicting proxy cookie name
   */
  protected String getProxyCookieName(HttpCookie cookie) {
    //
    return doPreserveCookies ? cookie.getName() : getCookieNamePrefix(cookie.getName()) + cookie.getName();
  }

  /**
   * Create path for proxy cookie.
   *
   * @param servletRequest original request
   * @return proxy cookie path
   */
  protected String buildProxyCookiePath(HttpServletRequest servletRequest) {
    String path = servletRequest.getContextPath(); // path starts with / or is empty string
    path += servletRequest.getServletPath(); // servlet path starts with / or is empty string
    if (path.isEmpty()) {
      path = "/";
    }
    return path;
  }

  /**
   * Take any client cookies that were originally from the proxy and prepare them to send to the
   * proxy.  This relies on cookie headers being set correctly according to RFC 6265 Sec 5.4.
   * This also blocks any local cookies from being sent to the proxy.
   */
  protected String getRealCookie(String cookieValue) {
    StringBuilder escapedCookie = new StringBuilder();
    String cookies[] = cookieValue.split("[;,]");
    for (String cookie : cookies) {
      String cookieSplit[] = cookie.split("=");
      if (cookieSplit.length == 2) {
        String cookieName = cookieSplit[0].trim();
        if (cookieName.startsWith(getCookieNamePrefix(cookieName))) {
          cookieName = cookieName.substring(getCookieNamePrefix(cookieName).length());
          if (escapedCookie.length() > 0) {
            escapedCookie.append("; ");
          }
          escapedCookie.append(cookieName).append("=").append(cookieSplit[1].trim());
        }
      }
    }
    return escapedCookie.toString();
  }

  /** The string prefixing rewritten cookies. */
  protected String getCookieNamePrefix(String name) {
    return "!Proxy!" + getServletConfig().getServletName();
  }

  /** Copy response body data (the entity) from the proxy to the servlet client. */
  protected void copyResponseEntity(HttpResponse proxyResponse, HttpServletResponse servletResponse,
                                    HttpRequest proxyRequest, HttpServletRequest servletRequest)
          throws IOException {
    HttpEntity entity = proxyResponse.getEntity();
    if (entity != null) {
      if (entity.isChunked()) {
        // Flush intermediate results before blocking on input -- needed for SSE
        InputStream is = entity.getContent();
        OutputStream os = servletResponse.getOutputStream();
        byte[] buffer = new byte[10 * 1024];
        int read;
        while ((read = is.read(buffer)) != -1) {
          os.write(buffer, 0, read);
          /*-
           * Issue in Apache http client/JDK: if the stream from client is
           * compressed, apache http client will delegate to GzipInputStream.
           * The #available implementation of InflaterInputStream (parent of
           * GzipInputStream) return 1 until EOF is reached. This is not
           * consistent with InputStream#available, which defines:
           *
           *   A single read or skip of this many bytes will not block,
           *   but may read or skip fewer bytes.
           *
           *  To work around this, a flush is issued always if compression
            *  is handled by apache http client
           */
          if (doHandleCompression || is.available() == 0 /* next is.read will block */) {
            os.flush();
          }
        }
        // Entity closing/cleanup is done in the caller (#service)
      } else {
        OutputStream servletOutputStream = servletResponse.getOutputStream();
        entity.writeTo(servletOutputStream);
      }
    }
  }

  /**
   * Reads the request URI from {@code servletRequest} and rewrites it, considering targetUri.
   * It's used to make the new request.
   */
  protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
    StringBuilder uri = new StringBuilder(500);
    uri.append(getTargetUri(servletRequest));
    // Handle the path given to the servlet
    String pathInfo = rewritePathInfoFromRequest(servletRequest);
    if (pathInfo != null) {//ex: /my/path.html
      // getPathInfo() returns decoded string, so we need encodeUriQuery to encode "%" characters
      uri.append(encodeUriQuery(pathInfo, true));
    }
    // Handle the query string & fragment
    String queryString = servletRequest.getQueryString();//ex:(following '?'): name=value&foo=bar#fragment
    String fragment = null;
    //split off fragment from queryString, updating queryString if found
    if (queryString != null) {
      int fragIdx = queryString.indexOf('#');
      if (fragIdx >= 0) {
        fragment = queryString.substring(fragIdx + 1);
        queryString = queryString.substring(0,fragIdx);
      }
    }

    queryString = rewriteQueryStringFromRequest(servletRequest, queryString);
    if (queryString != null && queryString.length() > 0) {
      uri.append('?');
      // queryString is not decoded, so we need encodeUriQuery not to encode "%" characters, to avoid double-encoding
      uri.append(encodeUriQuery(queryString, false));
    }

    if (doSendUrlFragment && fragment != null) {
      uri.append('#');
      // fragment is not decoded, so we need encodeUriQuery not to encode "%" characters, to avoid double-encoding
      uri.append(encodeUriQuery(fragment, false));
    }
    return uri.toString();
  }

  protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
    return queryString;
  }

  /**
   * Allow overrides of {@link jakarta.servlet.http.HttpServletRequest#getPathInfo()}.
   * Useful when url-pattern of servlet-mapping (web.xml) requires manipulation.
   */
  protected String rewritePathInfoFromRequest(HttpServletRequest servletRequest) {
    return servletRequest.getPathInfo();
  }

  /**
   * For a redirect response from the target server, this translates {@code theUrl} to redirect to
   * and translates it to one the original client can use.
   */
  protected String rewriteUrlFromResponse(HttpServletRequest servletRequest, String theUrl) {
    //TODO document example paths
    final String targetUri = getTargetUri(servletRequest);
    if (theUrl.startsWith(targetUri)) {
      /*-
       * The URL points back to the back-end server.
       * Instead of returning it verbatim we replace the target path with our
       * source path in a way that should instruct the original client to
       * request the URL pointed through this Proxy.
       * We do this by taking the current request and rewriting the path part
       * using this servlet's absolute path and the path from the returned URL
       * after the base target URL.
       */
      StringBuffer curUrl = servletRequest.getRequestURL();//no query
      int pos;
      // Skip the protocol part
      if ((pos = curUrl.indexOf("://"))>=0) {
        // Skip the authority part
        // + 3 to skip the separator between protocol and authority
        if ((pos = curUrl.indexOf("/", pos + 3)) >=0) {
          // Trim everything after the authority part.
          curUrl.setLength(pos);
        }
      }
      // Context path starts with a / if it is not blank
      curUrl.append(servletRequest.getContextPath());
      // Servlet path starts with a / if it is not blank
      curUrl.append(servletRequest.getServletPath());
      curUrl.append(theUrl, targetUri.length(), theUrl.length());
      return curUrl.toString();
    }
    return theUrl;
  }

  /** The target URI as configured. Not null. */
  public String getTargetUri() { return targetUri; }

  /**
   * Encodes characters in the query or fragment part of the URI.
   *
   * <p>Unfortunately, an incoming URI sometimes has characters disallowed by the spec.  HttpClient
   * insists that the outgoing proxied request has a valid URI because it uses Java's {@link URI}.
   * To be more forgiving, we must escape the problematic characters.  See the URI class for the
   * spec.
   *
   * @param in example: name=value&amp;foo=bar#fragment
   * @param encodePercent determine whether percent characters need to be encoded
   */
  protected CharSequence encodeUriQuery(CharSequence in, boolean encodePercent) {
    //Note that I can't simply use URI.java to encode because it will escape pre-existing escaped things.
    StringBuilder outBuf = null;
    Formatter formatter = null;
    for(int i = 0; i < in.length(); i++) {
      char c = in.charAt(i);
      boolean escape = true;
      if (c < 128) {
        if (asciiQueryChars.get(c) && !(encodePercent && c == '%')) {
          escape = false;
        }
      } else if (!Character.isISOControl(c) && !Character.isSpaceChar(c)) {//not-ascii
        escape = false;
      }
      if (!escape) {
        if (outBuf != null)
          outBuf.append(c);
      } else {
        //escape
        if (outBuf == null) {
          outBuf = new StringBuilder(in.length() + 5*3);
          outBuf.append(in,0,i);
          formatter = new Formatter(outBuf);
        }
        //leading %, 0 padded, width 2, capital hex
        formatter.format("%%%02X",(int)c);//TODO
      }
    }
    return outBuf != null ? outBuf : in;
  }

  protected static final BitSet asciiQueryChars;
  static {
    char[] c_unreserved = "_-!.~'()*".toCharArray();//plus alphanum
    char[] c_punct = ",;:$&+=".toCharArray();
    char[] c_reserved = "/@".toCharArray();//plus punct.  Exclude '?'; RFC-2616 3.2.2. Exclude '[', ']'; https://www.ietf.org/rfc/rfc1738.txt, unsafe characters
    asciiQueryChars = new BitSet(128);
    for(char c = 'a'; c <= 'z'; c++) asciiQueryChars.set(c);
    for(char c = 'A'; c <= 'Z'; c++) asciiQueryChars.set(c);
    for(char c = '0'; c <= '9'; c++) asciiQueryChars.set(c);
    for(char c : c_unreserved) asciiQueryChars.set(c);
    for(char c : c_punct) asciiQueryChars.set(c);
    for(char c : c_reserved) asciiQueryChars.set(c);

    asciiQueryChars.set('%');//leave existing percent escapes in place
  }

}

新版本里面还有一个URITemplateProxyServlet.java 用来实现 URL 与查询参数匹配

如 http://mywebapp/cluster/subpath?_subHost=namenode&_port=8080&_path=monitor匹配
http://{_subHost}.behindfirewall.mycompany.com:{_port}/{_path}

URITemplateProxyServlet.java

/*
 * Copyright MITRE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mitre.dsmiley.httpproxy;

import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A proxy servlet in which the target URI is templated from incoming request parameters. The
 * format adheres to the <a href="http://tools.ietf.org/html/rfc6570">URI Template RFC</a>, "Level
 * 1". Example:
 * <pre>
 *   targetUri = http://{host}:{port}/{path}
 * </pre>
 * --which has the template variables.  The incoming request must contain query args of these
 * names.  They are removed when the request is sent to the target.
 */
@SuppressWarnings({"serial"})
public class URITemplateProxyServlet extends ProxyServlet {

  /* Rich:
  * It might be a nice addition to have some syntax that allowed a proxy arg to be "optional", that is,
  * don't fail if not present, just return the empty string or a given default. But I don't see
  * anything in the spec that supports this kind of construct.
  * Notionally, it might look like {?host:google.com} would return the value of
  * the URL parameter "?hostProxyArg=somehost.com" if defined, but if not defined, return "google.com".
  * Similarly, {?host} could return the value of hostProxyArg or empty string if not present.
  * But that's not how the spec works. So for now we will require a proxy arg to be present
  * if defined for this proxy URL.
  */
  protected static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{(.+?)\\}");
  private static final String ATTR_QUERY_STRING =
          URITemplateProxyServlet.class.getSimpleName() + ".queryString";

  protected String targetUriTemplate;//has {name} parts

  @Override
  protected void initTarget() throws ServletException {
    targetUriTemplate = getConfigParam(P_TARGET_URI);
    if (targetUriTemplate == null)
      throw new ServletException(P_TARGET_URI+" is required.");

    //leave this.target* null to prevent accidental mis-use
  }

  @Override
  protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
          throws ServletException, IOException {

    //First collect params
    /*
     * Do not use servletRequest.getParameter(arg) because that will
     * typically read and consume the servlet InputStream (where our
     * form data is stored for POST). We need the InputStream later on.
     * So we'll parse the query string ourselves. A side benefit is
     * we can keep the proxy parameters in the query string and not
     * have to add them to a URL encoded form attachment.
     */
    String requestQueryString = servletRequest.getQueryString();
    String queryString = "";
    if (requestQueryString != null) {
      queryString = "?" + requestQueryString;//no "?" but might have "#"
    }
    int hash = queryString.indexOf('#');
    if (hash >= 0) {
      queryString = queryString.substring(0, hash);
    }
    List<NameValuePair> pairs;
    try {
      //note: HttpClient 4.2 lets you parse the string without building the URI
      pairs = URLEncodedUtils.parse(new URI(queryString), "UTF-8");
    } catch (URISyntaxException e) {
      throw new ServletException("Unexpected URI parsing error on " + queryString, e);
    }
    LinkedHashMap<String, String> params = new LinkedHashMap<String, String>();
    for (NameValuePair pair : pairs) {
      params.put(pair.getName(), pair.getValue());
    }

    //Now rewrite the URL
    StringBuffer urlBuf = new StringBuffer();//note: StringBuilder isn't supported by Matcher
    Matcher matcher = TEMPLATE_PATTERN.matcher(targetUriTemplate);
    while (matcher.find()) {
      String arg = matcher.group(1);
      String replacement = params.remove(arg);//note we remove
      if (replacement == null) {
        throw new ServletException("Missing HTTP parameter "+arg+" to fill the template");
      }
      matcher.appendReplacement(urlBuf, replacement);
    }
    matcher.appendTail(urlBuf);
    String newTargetUri = urlBuf.toString();
    servletRequest.setAttribute(ATTR_TARGET_URI, newTargetUri);
    URI targetUriObj;
    try {
      targetUriObj = new URI(newTargetUri);
    } catch (Exception e) {
      throw new ServletException("Rewritten targetUri is invalid: " + newTargetUri,e);
    }
    servletRequest.setAttribute(ATTR_TARGET_HOST, URIUtils.extractHost(targetUriObj));

    //Determine the new query string based on removing the used names
    StringBuilder newQueryBuf = new StringBuilder(queryString.length());
    for (Map.Entry<String, String> nameVal : params.entrySet()) {
      if (newQueryBuf.length() > 0)
        newQueryBuf.append('&');
      newQueryBuf.append(nameVal.getKey()).append('=');
      if (nameVal.getValue() != null)
        newQueryBuf.append( URLEncoder.encode(nameVal.getValue(), "UTF-8"));
    }
    servletRequest.setAttribute(ATTR_QUERY_STRING, newQueryBuf.toString());

    super.service(servletRequest, servletResponse);
  }

  @Override
  protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
    return (String) servletRequest.getAttribute(ATTR_QUERY_STRING);
  }
}

仅仅是代理接口 默认官网示例使用即可,参考第二个接口代理。

以Nginx 代理Grafana监控平台为例,解决静态资源加载失败、访问鉴权、及websocket连接问题

import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;

/**
 * 本地代理服务
 *
 * @author Smile
 */
@Configuration
public class ProxyServletConfig {


	/**
	* 代理Grafana 监控平台
	*/	
    @Bean
    public ServletRegistrationBean<ProxyServlet> servletRegistrationBean() {
        ServletRegistrationBean<ProxyServlet> servletRegistrationBean = new ServletRegistrationBean<>(new ProxyServlet(), "/grafana/*");
        servletRegistrationBean.addInitParameter(ProxyServlet.P_TARGET_URI, "http://127.0.0.1:9999");
        servletRegistrationBean.addInitParameter(ProxyServlet.P_LOG, "true");
//       自动处理重定向
        servletRegistrationBean.addInitParameter(ProxyServlet.P_HANDLEREDIRECTS, "false");
//      保持 COOKIES 不变
        servletRegistrationBean.addInitParameter(ProxyServlet.P_PRESERVECOOKIES, "true");
//       Set-Cookie 服务器响应标头中保持 cookie 路径不变
        servletRegistrationBean.addInitParameter(ProxyServlet.P_PRESERVECOOKIEPATH, "true");
//        保持 HOST 参数不变
        servletRegistrationBean.addInitParameter(ProxyServlet.P_PRESERVEHOST, "true");
        return servletRegistrationBean;
    }

	/**
	*接口代理
	*/
    @Bean
    public ServletRegistrationBean<ProxyServlet> servletRegistration() {
        ServletRegistrationBean<ProxyServlet> servletRegistrationBean = new ServletRegistrationBean<>(new ProxyServlet(), "/one/*","/two/*","three/*");
        servletRegistrationBean.addInitParameter(ProxyServlet.P_TARGET_URI, "http://localhost:8001/api");
        servletRegistrationBean.addInitParameter(ProxyServlet.P_LOG, "true");
        return servletRegistrationBean;
    }


    /**
     * 禁用springboot 自带的 HiddenHttpMethodFilter 防止post提交的form数据流被提前消费
     * <p>
     * fix springboot中使用proxyservlet的 bug.
     * <a href="https://github.com/mitre/HTTP-Proxy-Servlet/issues/83">bugs</a>
     * <a href="https://stackoverflow.com/questions/8522568/why-is-httpservletrequest-inputstream-empty">bugs</a>
     *
     * @return 
     */
    @Bean
    public FilterRegistrationBean<HiddenHttpMethodFilter> disableHiddenHttpMethodFilter() {
        FilterRegistrationBean<HiddenHttpMethodFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new HiddenHttpMethodFilter());
        registrationBean.setEnabled(false); // 禁用过滤器
        return registrationBean;
    }
}

直接访问grafana代理 springboot项目端口8088报错,静态资源 路径不正确 加载失败,grafana live/ws链接也会失败,参考附录。

在这里插入图片描述
解决办法:
1.修改对应前端项目,使其 都通过代理路径。
2.可以新建项目,ProxyServlet使用 /*路径匹配规则。
3.如果这两种都不适合,那么变动最少的方案,使用nginx反向代理到ProxyServlet的代理路径上。这样做会增加一些性能损耗,但是换来了,前端项目无需更改,后端可增加接口鉴权,某些时候是值得的。

nginx配置参考:

 server {
            listen       8889;
            server_name  localhost;
			
			# grafana websocket地址代理
            location /api/live/ws {
                 proxy_http_version 1.1;
                 proxy_set_header Upgrade $http_upgrade;
                 proxy_set_header Connection "Upgrade";
                 proxy_set_header X-real-ip $remote_addr;
                 proxy_set_header X-Forwarded-For $remote_addr;
                 proxy_pass http://127.0.0.1:9999;
            }

            location / {
                #add_header Access-Control-Allow-Origin *;
                add_header Access-Control-Max-Age 1728000;
                add_header Access-Control-Allow-Methods 'POST,GET,OPTIONS,DELETE,PUT,HEAD,PATCH';
                add_header Access-Control-Allow-Headers 'satoken,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
                add_header Access-Control-Allow-Origin $http_origin;
                client_max_body_size 10m;

                if ($request_method = 'OPTIONS') {
                    return 204;
                }
			  	# grafana支持配置apikey 免登录访问
                set $auth 'Bearer eyJrIjoiN1pKYlk5akFDZWNoMlVSUEN1YllXdm0yd2VYN2RzZFIiLCJuIjoiYWRtaW5rZXkiLCJpZCI6MX0=';
               # apiKey设置到header grafana免密访问
                proxy_set_header Authorization $auth;

                proxy_pass http://127.0.0.1:8088/grafana//;

                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_connect_timeout 600;
                proxy_read_timeout 600;
            }


      }

划重点

springboot 代理的/grafana/* 到 http://127.0.0.1:9999
静态资源的访问失败 404或 错误的返回html首页,是因为路径不符合此规则导致代理是失败

proxy_pass http://127.0.0.1:8088/grafana//;
由nginx代理到 // 则问题解决 ,使 /grafana/* 代理规则生效

其他访问的服务调用 nginx的这个代理

ok.
在这里插入图片描述
再看后端 日志 代理已正常
在这里插入图片描述

更安全的访问

只需要限制 原服务端口的放行规则,如仅本机可访问,然后项目增加过滤器自行判断权限。

启动类添加@ServletComponentScan扫描WebFilter,增加 Filter

import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;

import java.io.IOException;
/**
 * 过滤器
 *
 * @author Smile
 */
@Order(1)
@WebFilter(filterName = "grafanaFilter", urlPatterns = "/grafana/*")
public class ProxyGrafanaFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    	//自行 实现条件判断即可
        if (StpUtil.isLogin()) {
            // 用户已登录,继续执行过滤器链
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            // 用户未登录,可以返回错误信息或重定向到登录页面
            // 例如,返回 HTTP 401 未授权状态
            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

未登录则:
在这里插入图片描述

``收工~~

附:
grafana websocket 可能需要修改custom.ini配置

# allowed_origins is a comma-separated list of origins that can establish connection with Grafana Live.
# If not set then origin will be matched over root_url. Supports wildcard symbol "*".

allowed_origins=*
或
allowed_origins = http://127.0.0.1:8889
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值