在受限 url 上使用 Reactive Netty 进行相互身份验证
Mutual Authentication with Reactive Netty on restricted urls
我正在使用 spring 云网关来处理遗留应用程序,以便我们可以开始在幕后进行迁移。应用程序托管的一些 url 是面向 public 的,有些是设备限制的。我们控制设备,他们使用浏览器客户端访问受限的 urls。我们在服务器上使用 tomcat 为设备限制 urls 设置了相互身份验证设置,并且在 web.xml:
中设置了安全限制
<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 云网关中实现类似的设置,该网关在后台使用反应式网络,并从遗留应用程序中删除 web.xml 限制。我想我可以将其切换为使用 tomcat 并可能使 web.xml 从上面开始工作,但我宁愿坚持使用 reactive netty 的性能优势。
主要目标:
- 只为应用程序部署一个 api 网关。 url 的数量
需要相互认证非常小,所以我宁愿不包括整个
管理其他容器只是为了支持它们。
- 不要在 public url 上请求客户端证书。
- 受限 url 需要有效的客户端证书。
我已经设置了相互身份验证,并且可以按预期让它与 need/want/none 一起工作(truststores 设置等),但它适用于所有 url。我还设置了 X509 安全限制,一切似乎都有效。
我认为我想要设置的是在解密 http 请求(以便我可以访问 url)之后使用 SslHandler 基于路径进行 tsl 重新协商。但是我在细节方面遇到了麻烦,而且我没能找到任何包含 spring-boot 应用程序的示例,这些应用程序使用反应性 netty 进行 tsl 重新协商。任何关于如何在 needClientAuth 设置为 true 的情况下执行 ssl 连接重新协商的提示将不胜感激。我想我需要使会话无效或其他原因,因为当我尝试手动执行时,它似乎正在跳过协商,因为连接已在 ssl 引擎中标记为已协商。
这是我尝试过的迭代之一(这不限制 urls,但我计划在我开始工作后添加它):
@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;
});
});
}
}
我看到它在调试器中执行重新协商,但它似乎仍然设置为客户端身份验证 none(在 application.properties 中设置)而不是之前代码中设置的需要重新谈判。我试过 sslEngine.getSession().invalidate();
但没有用。我也尝试过从 ssl 提供程序生成一个新的 ssl 处理程序,但这似乎真的把事情搞砸了。
感谢您提供的任何帮助。
编辑:做更多研究后发现这种方法不适合继续使用,因为 ssl 重新协商在 tsl 1.3 中被完全放弃(参见 https://security.stackexchange.com/a/230327). Is there a way to perform the equivalent of SSL verify client post handshake
as described here: https://www.openssl.org/docs/manmaster/man3/SSL_verify_client_post_handshake.html ?
Edit2:看起来这是一个问题,我正在测试的浏览器不支持 TLS1.3 post 握手。将服务器设置为仅接受 TLS 1.2 似乎可行。不确定是否有更好的方法来解决这个问题,但这是我添加到 application.properties:
中的方法
server.ssl.enabled-protocols=TLSv1.2
这是我用来让它工作的方法。我将省略它的 spring 安全方面,因为它与从客户端请求证书是分开的。
配置用于处理请求的子管道的方法有很多种。如果有更可接受的配置方式,请告诉我。
通过添加到与客户端建立连接时应用的 bootstrap 管道来配置 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
编辑:已更新,因为未正确调用处理程序网关路由代码。
我正在使用 spring 云网关来处理遗留应用程序,以便我们可以开始在幕后进行迁移。应用程序托管的一些 url 是面向 public 的,有些是设备限制的。我们控制设备,他们使用浏览器客户端访问受限的 urls。我们在服务器上使用 tomcat 为设备限制 urls 设置了相互身份验证设置,并且在 web.xml:
中设置了安全限制 <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 云网关中实现类似的设置,该网关在后台使用反应式网络,并从遗留应用程序中删除 web.xml 限制。我想我可以将其切换为使用 tomcat 并可能使 web.xml 从上面开始工作,但我宁愿坚持使用 reactive netty 的性能优势。
主要目标:
- 只为应用程序部署一个 api 网关。 url 的数量 需要相互认证非常小,所以我宁愿不包括整个 管理其他容器只是为了支持它们。
- 不要在 public url 上请求客户端证书。
- 受限 url 需要有效的客户端证书。
我已经设置了相互身份验证,并且可以按预期让它与 need/want/none 一起工作(truststores 设置等),但它适用于所有 url。我还设置了 X509 安全限制,一切似乎都有效。
我认为我想要设置的是在解密 http 请求(以便我可以访问 url)之后使用 SslHandler 基于路径进行 tsl 重新协商。但是我在细节方面遇到了麻烦,而且我没能找到任何包含 spring-boot 应用程序的示例,这些应用程序使用反应性 netty 进行 tsl 重新协商。任何关于如何在 needClientAuth 设置为 true 的情况下执行 ssl 连接重新协商的提示将不胜感激。我想我需要使会话无效或其他原因,因为当我尝试手动执行时,它似乎正在跳过协商,因为连接已在 ssl 引擎中标记为已协商。
这是我尝试过的迭代之一(这不限制 urls,但我计划在我开始工作后添加它):
@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;
});
});
}
}
我看到它在调试器中执行重新协商,但它似乎仍然设置为客户端身份验证 none(在 application.properties 中设置)而不是之前代码中设置的需要重新谈判。我试过 sslEngine.getSession().invalidate();
但没有用。我也尝试过从 ssl 提供程序生成一个新的 ssl 处理程序,但这似乎真的把事情搞砸了。
感谢您提供的任何帮助。
编辑:做更多研究后发现这种方法不适合继续使用,因为 ssl 重新协商在 tsl 1.3 中被完全放弃(参见 https://security.stackexchange.com/a/230327). Is there a way to perform the equivalent of SSL verify client post handshake
as described here: https://www.openssl.org/docs/manmaster/man3/SSL_verify_client_post_handshake.html ?
Edit2:看起来这是一个问题,我正在测试的浏览器不支持 TLS1.3 post 握手。将服务器设置为仅接受 TLS 1.2 似乎可行。不确定是否有更好的方法来解决这个问题,但这是我添加到 application.properties:
中的方法server.ssl.enabled-protocols=TLSv1.2
这是我用来让它工作的方法。我将省略它的 spring 安全方面,因为它与从客户端请求证书是分开的。
配置用于处理请求的子管道的方法有很多种。如果有更可接受的配置方式,请告诉我。
通过添加到与客户端建立连接时应用的 bootstrap 管道来配置 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
编辑:已更新,因为未正确调用处理程序网关路由代码。