在受限 url 上使用 Reactive Netty 进行相互身份验证

问题描述

我正在使用 spring cloud gateway 来处理遗留应用程序,以便我们可以开始在幕后迁移事物。应用程序托管的某些 url 是面向公众的,而有些则受设备限制。我们控制设备,他们使用浏览器客户端访问受限网址。我们在服务器上使用 tomcat 和 web.xml 中的安全约束为设备受限 url 设置了相互身份验证:

  <security-constraint>
    <web-resource-collection>
      <web-resource-name>Certificate Content</web-resource-name>
      <!-- URL for authentication endpoint - this is locked down with the role assigned by tomcat -->
      <url-pattern>/rest/secure/url1</url-pattern>
      <url-pattern>/rest/secure/url2</url-pattern>
      <url-pattern>/rest/secure/url3</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>certificate</role-name>
    </auth-constraint>
      <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </user-data-constraint>
  </security-constraint>

  <!-- All other endpoints- force the switch from http to https with transport-guarantee -->
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>Protected Context</web-resource-name>
      <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
  </security-constraint>

  <login-config>
    <auth-method>CLIENT-CERT</auth-method>
  </login-config>

  <security-role>
    <role-name>certificate</role-name>
  </security-role>

这与 tomcat 的 server.xml 中的信任库设置相结合(我可以添加它,但我认为这与本次对话无关)。

我的目标是在 spring cloud gateway 中实现一个类似的设置,它在后台使用reactive netty 并从遗留应用程序中删除 web.xml 限制。我想我可以将它切换到使用 tomcat 并可能从上面获得 web.xml 工作,但我宁愿坚持使用反应式 netty 的性能优势。

主要目标:

  1. 仅为应用部署一个 api 网关。网址的数量 需要相互认证非常小,所以我宁愿不包括整个 管理其他容器只是为了支持它们。
  2. 不要在公共网址上索取客户端证书。
  3. 需要为受限网址提供有效的客户端证书。

我已经设置了相互身份验证,可以让它按预期使用需要/想要/无(信任库设置等),但它适用于所有 url。我还设置了 X509 安全限制,一切似乎都有效。

我想我想设置的是基于路径解密http请求(以便我可以访问url)后使用SslHandler进行tsl重新协商。但是我在细节方面遇到了麻烦,而且我找不到任何包含使用反应性 netty 进行 tsl 重新协商的 spring-boot 应用程序的示例。任何有关如何在 needClientAuth 设置为 true 的情况下重新协商 ssl 连接的提示将不胜感激。我想我需要使会话无效或其他原因,因为当我尝试手动执行此操作时,它似乎正在跳过协商,因为连接已在 ssl 引擎中标记为协商。

这是我尝试过的迭代之一(这不限制网址,但我计划在我开始工作后添加它):

@Component
public class NettyWebServerFactoryGatewayCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
  private static final Logger LOG = LoggerFactory.getLogger(NettyWebServerFactoryGatewayCustomizer.class);

  @Override
  public void customize(NettyReactiveWebServerFactory serverFactory) {
    serverFactory.addServerCustomizers(httpServer -> {
      httpServer = httpServer.wiretap(true);
      return httpServer.tcpConfiguration(tcpserver -> {
        tcpserver = tcpserver.doOnConnection(connection -> 
            connection.addHandler("request client cert",new SimpleChannelInboundHandler<HttpRequest>() {
                  @Override
                  protected void channelRead0(ChannelHandlerContext ctx,HttpRequest httpRequest) {
                    LOG.error("HttpRequest: {}",httpRequest);
                    final ChannelPipeline pipeline = ctx.pipeline();
                    final SslHandler sslHandler = pipeline.get(SslHandler.class);
                    final SSLEngine sslEngine = sslHandler.engine();
                    sslEngine.setNeedClientAuth(true);
                    sslHandler.renegotiate()
                        .addListener(future -> ctx.fireChannelRead(httpRequest));
                  }
                }
            )
        );
        return tcpserver;
      });
    });
  }
}

我看到它在调试器中执行重新协商,但它似乎仍然设置为 client auth none(在 application.properties 中设置)而不是在重新协商之前在代码中设置的需要。我试过 sslEngine.getSession().invalidate(); 但这没有帮助。我还尝试从 ssl 提供程序生成一个新的 ssl 处理程序,但这似乎真的把事情搞砸了。

感谢您提供的任何帮助。

编辑:进行更多研究后,似乎这种方法不适合继续使用,因为 ssl 重新协商在 tsl 1.3 中已完全取消(请参阅 https://security.stackexchange.com/a/230327)。有没有办法按照此处所述执行等效于 SSL verify client post handshake 的操作:https://www.openssl.org/docs/manmaster/man3/SSL_verify_client_post_handshake.html

Edit2:看起来这是我正在测试的浏览器不支持 TLS1.3 握手后的问题。将服务器设置为仅接受 TLS 1.2 似乎可行。不确定是否有更好的方法解决这个问题,但这是我添加到我的 application.properties 中的内容

server.ssl.enabled-protocols=TLSv1.2

解决方法

这是我用来让它工作的东西。我将省略它的 spring 安全方面,因为这与从客户端请求证书是分开的。

有很多方法可以配置用于处理请求的子管道。如果有更可接受的配置方式,请告诉我。

通过添加到与客户端建立连接时应用的引导管道来配置 HttpServer:

@Component
public class NettyWebServerFactoryGatewayCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
  private static final HttpRenegotiateClientCertHandler HTTP_RENEGOTIATE_CLIENT_CERT_HANDLER =
      new HttpRenegotiateClientCertHandler(SecurityConfig.X509_PROTECTED_ENDPOINTS);

  @Override
  public void customize(NettyReactiveWebServerFactory serverFactory) {
    serverFactory.addServerCustomizers(NettyWebServerFactoryGatewayCustomizer::addRenegotiateHandlerToHttpServer);
  }

  private static HttpServer addRenegotiateHandlerToHttpServer(HttpServer httpServer) {
    return httpServer.tcpConfiguration(NettyWebServerFactoryGatewayCustomizer::addRenegotiateHandlerToTcpServer);
  }

  private static TcpServer addRenegotiateHandlerToTcpServer(TcpServer server) {
    return server.doOnBind(NettyWebServerFactoryGatewayCustomizer::addRenegotiateHandlerToServerBootstrap);
  }

  private static void addRenegotiateHandlerToServerBootstrap(ServerBootstrap serverBootstrap) {
    BootstrapHandlers.updateConfiguration(
        serverBootstrap,HttpRenegotiateClientCertHandler.NAME,NettyWebServerFactoryGatewayCustomizer::addRenegotiateHandlerToChannel
    );
  }

  private static void addRenegotiateHandlerToChannel(ConnectionObserver connectionObserver,Channel channel) {
    final ChannelPipeline pipeline = channel.pipeline();
    pipeline.addLast(HttpRenegotiateClientCertHandler.NAME,HTTP_RENEGOTIATE_CLIENT_CERT_HANDLER);
  }
}

执行重新协商的子处理程序:

@ChannelHandler.Sharable
public class HttpRenegotiateClientCertHandler extends SimpleChannelInboundHandler<HttpRequest> {
  public static final String NAME = NettyPipeline.LEFT + "clientRenegotiate";

  private static final PathPatternParser DEFAULT_PATTERN_PARSER = new PathPatternParser();

  private final Collection<PathPattern> pathPatterns;

  public HttpRenegotiateClientCertHandler(String ... antPatterns) {
    Assert.notNull(antPatterns,"patterns cannot be null");
    Assert.notEmpty(antPatterns,"patterns cannot be empty");
    Assert.noNullElements(antPatterns,"patterns cannot have null items");
    pathPatterns = Arrays.stream(antPatterns)
        .map(DEFAULT_PATTERN_PARSER::parse)
        .collect(Collectors.toSet());
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx,HttpRequest request) {
    if (shouldNotRenegotiate(request)) {
      ctx.fireChannelRead(request);
      return;
    }

    final ChannelPipeline pipeline = ctx.pipeline();
    final SslHandler sslHandler = pipeline.get(SslHandler.class);
    final SSLEngine sslEngine = sslHandler.engine();
    sslEngine.setNeedClientAuth(true);

    sslHandler.renegotiate()
        .addListener(renegotiateFuture -> ctx.fireChannelRead(request));
  }

  /**
   * Determine if the request uri matches the configured uris for this handler.
   * @param request to match the path from.
   * @return true if any of the path patterns are matched.
   */
  private boolean shouldNotRenegotiate(HttpRequest request) {
    final String requestUri = request.uri();
    final PathContainer path = PathContainer.parsePath(requestUri);
    return pathPatterns.stream()
        .noneMatch(matcher -> matcher.matches(path));
  }
}

以及 application.properties 中的这些配置:

# Setup Client Auth Truststore:
server.ssl.trust-store=<path to truststore>
server.ssl.trust-store-password=<truststore password>
server.ssl.trust-store-type=<truststore type>
# Set to none by default so we do not ask for client auth until needed.
server.ssl.client-auth=none
# This is specifically not including TLSv1.3 because there are issues
# with older browsers' implementation of TLSv1.3 that prevent verify
# client post handshake client from working.
server.ssl.enabled-protocols=TLSv1.2

编辑:更新,因为处理程序网关路由代码没有被正确调用。