Netty(7)安全增强

young 1,588 2022-05-22

安全增强

设置高低水位线

Netty OOM的根本原因

根源:进(读速度)大于出(写速度)

表象:

  • 上游发送太快:任务重
  • 自己:处理慢/不发或者发的慢:处理能力有限,流量控制等原因
  • 网速
  • 下游处理速度慢:导致不及时读取接收Buffer数据,然后反馈到这边,发送速度降速

Netty OOM - ChannelOutboundBuffer

ChannelOutboundBuffer就相当于Netty发送数据的仓库,如果存的数据过多,就会OOM

存的对象:LinkedList存ChannelOutBoundBuffer.Entry

解决方式:判断totalPendingSize>writeBufferWaterMark.high()设置unwritable,写完之后会移除entry

private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if (size == 0) {
return;
}
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
// 判断待发送的数据的size是否高于高水位线
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
setUnwritable(invokeLater);
}
}

Netty OOM - TrafficShapingHandler

以ChannelTrafficShapingHandler为例

存的对象:messageQueue存ChannelTrafficShapingHandler.ToSend

解决方式:判断queueSize>maxWriteSize或delay>maxWriteDelay,设置unwritable

void checkWriteSuspend(ChannelHandlerContext ctx, long delay, long queueSize) {
if (queueSize > maxWriteSize || delay > maxWriteDelay) {
setUserDefinedWritability(ctx, false);
}
}

unwritable

unwritable

/**
* Returns {@code true} if and only if {@linkplain #totalPendingWriteBytes() the total number of pending bytes} did
* not exceed the write watermark of the {@link Channel} and
* no {@linkplain #setUserDefinedWritability(int, boolean) user-defined writability flag} has been set to
* {@code false}.
*/
// 如果可写,unwritable==0
public boolean isWritable() {
return unwritable == 0;
}

Netty OOM的对策

设置好参数:

  • 高低水位线(默认32k到64k)
  • 启动流量整形时才需要考虑
    • maxWrite(默认4M)
    • maxGlobalWriteSize(默认400M)
    • maxWriteDelay(默认4s)

判断channel.isWirtable()

// 连接存活并且可写的时候才去写
if(ctx.channel.isActive() && ctx.channel().isWritable()){
ctx.writeAndFlush(responseMessage);
}else{
// 其他情况要么丢弃数据,要么将数据存起来,想其他方法再发送,比直接OOM要强
log.error("message dropped");
}

启用空闲检测

示例:实现一个小目标

  • 服务器加上read idle check - 服务器10s接收不到channel的请求就断掉连接
    • 保护自己、瘦身(及时清理空闲的连接)
  • 客户端加上 write idle check + keepalive - 客户端5s不发送数据就发一个keepalive
    • 避免连接被断
    • 启用不频繁的keepalive

Server端Idle

创建Server端的Idle检测,IdleStateHandler不支持共享

public class ServerIdleCheckHandler extends IdleStateHandler{
public ServerIdleCheckHandler() {
// 10秒的readIdle,不检测writeIdle,不检测allIdle
super(10,0,0,TimeUnit.SECONDS);
}
}

将ServerIdelCheckHandler加入到Server的pipeline中

serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
pipeline.addLast("TShandler", globalTrafficShapingHandler);
// 加入idea检测
pipeline.addLast("idleCheck",new ServerIdleCheckHandler());
pipeline.addLast("frameDecode", new OrderFrameDecoder());
pipeline.addLast(new OrderProtocolDecoder());
...
...
}
});

启动Server和Client,查看Server端的日志

ServerIdleCheck-1

可以看到日志中输出了USER_EVENT: IdleStateEvent(READER_IDLE, first)和USER_EVENT: IdleStateEvent(READER_IDLE),时间间隔10秒,说明我们设置的readIdle成功了,但是连接并没有断开。

修改ServerIdleCheckHandler,重写channelIdle方法

/**
* Is called when an {@link IdleStateEvent} should be fired. This implementation calls
* {@link ChannelHandlerContext#fireUserEventTriggered(Object)}.
*/
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
@Slf4j
public class ServerIdleCheckHandler extends IdleStateHandler {
public ServerIdleCheckHandler() {
super(10, 0, 0, TimeUnit.SECONDS);
}
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
// 处理第一次ReadIdle事件
if (evt == IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT) {
// 打印日志,说明检测到了idle,连接关闭
log.info("idle check happen, so close the connection");
// 关闭连接
ctx.close();
// 不希望这个事件再触发了,就return掉
return;
}
super.channelIdle(ctx, evt);
}
}

启动Server和Client,查看Server日志

ServerIdleCheck-2

可以看到Server端我们加入的日志已经打印出来了,再查看Client端日志

ServerIdleCheck-3

可以看到连接断开了

Client端Idle+Keepalive

创建Client端的Idle

public class ClientIdleCheckHandler extends IdleStateHandler {
public ClientIdleCheckHandler() {
super(0, 5, 0);
}
}

Keepalive就是向Server发送一条信息

创建KeepaliveHandler

@Slf4j
@ChannelHandler.Sharable
public class KeepaliveHandler extends ChannelDuplexHandler {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 如果是第一次WriteIdle事件
if (evt == IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT){
// 打印日志
log.info("write idle happen. so need to send keepalive to keep connection not closed by server");
// 创建keepalive的message
KeepaliveOperation keepaliveOperation = new KeepaliveOperation();
RequestMessage requestMessage = new RequestMessage(IdUtil.nextId(), keepaliveOperation);
// 发送message
ctx.writeAndFlush(requestMessage);
}
super.userEventTriggered(ctx, evt);
}
}

将ClientIdleCheckHandler和KeepaliveHandler都加入到Client的pipeline中。

keepalive可以共享,所以可以添加@Sharable注解

Keepalive要处理编解码,所以要放在比较靠后的位置。

KeepaliveHandler keepaliveHandler = new KeepaliveHandler();
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new ClientIdleCheckHandler());
pipeline.addLast(new ClientOrderFrameDecoder());
pipeline.addLast(new ClientOrderFrameEncoder());
pipeline.addLast(new ClientOrderProtocolEncoder());
pipeline.addLast(new ClientOrderProtocolDecoder());
// keepalive需要编解码,所以放在后面
pipeline.addLast(keepaliveHandler);
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
}
});

启动Server和Client,查看日志

Client:

ClientIdleKeepalive-1
Server:
ClientIdleKeepalive-2

黑白名单

Netty中的”cidrPrefix“

比如判断两台主机的网络是不是在同一个局域网内,将IP地址划分为两部分:网络位和主机位

IP地址: 11000000.10101000.00000001.00000001

子网掩码: 11111111.11111111.11111111.00000000

前24位为网络位,后8位为主机位

A/B/C…类

网络 格式 子网掩码
A类(前8位为网络位) network.node.node.node 255.0.0.0
B类(前16位网络位) network.network.node.node 255.255.0.0
C类(前24位为网络位) network.network.network.node 255.255.255.0

这样的切分就很浪费

CIDR:无分类域间路由选择,又常被称为无分类编址,表示前多少位为网络位

子网掩码 CIDR值
255.0.0.0 /8
255.128.0.0 /9
255.192.0.0 /10

Netty地址过滤功能源码

  • 同一个IP只能有一个连接
  • IP地址过滤:黑名单、白名单

IP过滤

AbstractRemoteAddressFilter

在channelRegistered()和channelActive()的时候,调用了handleNewChannel方法

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
handleNewChannel(ctx);
ctx.fireChannelRegistered();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (!handleNewChannel(ctx)) {
throw new IllegalStateException("cannot determine to accept or reject a channel: " + ctx.channel());
} else {
ctx.fireChannelActive();
}
}
// 判断连接的远程地址是否符合需求,不符合断掉
private boolean handleNewChannel(ChannelHandlerContext ctx) throws Exception {
@SuppressWarnings("unchecked")
// 获取远程地址
T remoteAddress = (T) ctx.channel().remoteAddress();
// If the remote address is not available yet, defer the decision.
if (remoteAddress == null) {
return false;
}
// No need to keep this handler in the pipeline anymore because the decision is going to be made now.
// Also, this will prevent the subsequent events from being handled by this handler.
// 只判断一次
ctx.pipeline().remove(this);
// 判断是否接受这个地址
if (accept(ctx, remoteAddress)) {
// 当前已有channelAccepted实现返回都是{},所以什么都不做,不会对已建好的连接进行处理
channelAccepted(ctx, remoteAddress);
} else {
// 当前已有channelRejected实现返回都是null,所以执行关闭
ChannelFuture rejectedFuture = channelRejected(ctx, remoteAddress);
if (rejectedFuture != null) {
rejectedFuture.addListener(ChannelFutureListener.CLOSE);
} else {
// 关闭连接
ctx.close();
}
}
return true;
}
@ChannelHandler.Sharable
public class UniqueIpFilter extends AbstractRemoteAddressFilter<InetSocketAddress> {
// 对同一个IP只能建立一个连接
private final Set<InetAddress> connected = new ConcurrentSet<InetAddress>();
@Override
protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) throws Exception {
final InetAddress remoteIp = remoteAddress.getAddress();
// 判断这个IP有没有在连接了
// 建立连接的时候会把ip加入到set中,如果没有断开连接的时候,再次创建连接,就会add失败
if (!connected.add(remoteIp)) {
return false;
} else {
// 连接关闭时,从connected中移除remote ip
ctx.channel().closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
connected.remove(remoteIp);
}
});
return true;
}
}
}
/**
* <p>
* This class allows one to filter new {@link Channel}s based on the
* {@link IpFilterRule}s passed to its constructor. If no rules are provided, all connections
* will be accepted.
* </p>
*
* <p>
* If you would like to explicitly take action on rejected {@link Channel}s, you should override
* {@link AbstractRemoteAddressFilter#channelRejected(ChannelHandlerContext, SocketAddress)}.
* </p>
*
* <p> Consider using {@link IpSubnetFilter} for better performance while not as
* general purpose as this filter. </p>
*/
@Sharable
public class RuleBasedIpFilter extends AbstractRemoteAddressFilter<InetSocketAddress> {
private final boolean acceptIfNotFound;
private final List<IpFilterRule> rules;
/**
* <p> Create new Instance of {@link RuleBasedIpFilter} and filter incoming connections
* based on their IP address and {@code rules} applied. </p>
*
* <p> {@code acceptIfNotFound} is set to {@code true}. </p>
*
* @param rules An array of {@link IpFilterRule} containing all rules.
*/
// 基于多个规则的
public RuleBasedIpFilter(IpFilterRule... rules) {
this(true, rules);
}
/**
* Create new Instance of {@link RuleBasedIpFilter} and filter incoming connections
* based on their IP address and {@code rules} applied.
*
* @param acceptIfNotFound If {@code true} then accept connection from IP Address if it
* doesn't match any rule.
* @param rules An array of {@link IpFilterRule} containing all rules.
*/
public RuleBasedIpFilter(boolean acceptIfNotFound, IpFilterRule... rules) {
ObjectUtil.checkNotNull(rules, "rules");
this.acceptIfNotFound = acceptIfNotFound;
this.rules = new ArrayList<IpFilterRule>(rules.length);
for (IpFilterRule rule : rules) {
if (rule != null) {
this.rules.add(rule);
}
}
}
@Override
protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) throws Exception {
// 遍历规则,看规则能否匹配上地址
for (IpFilterRule rule : rules) {
if (rule.matches(remoteAddress)) {
// 如果能匹配上,就判断rule的ruleType是不是为ACCEPT
return rule.ruleType() == IpFilterRuleType.ACCEPT;
}
}
return acceptIfNotFound;
}
}

IpSubnetFilterRule:用于判断ip是不是在一个局域网内

private static IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
ObjectUtil.checkNotNull(ipAddress, "ipAddress");
ObjectUtil.checkNotNull(ruleType, "ruleType");
// 判断地址是ip4还是ip6
if (ipAddress instanceof Inet4Address) {
return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType);
} else if (ipAddress instanceof Inet6Address) {
return new Ip6SubnetFilterRule((Inet6Address) ipAddress, cidrPrefix, ruleType);
} else {
throw new IllegalArgumentException("Only IPv4 and IPv6 addresses are supported");
}
}
private Ip4SubnetFilterRule(Inet4Address ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
if (cidrPrefix < 0 || cidrPrefix > 32) {
throw new IllegalArgumentException(String.format("IPv4 requires the subnet prefix to be in range of "
+ "[0,32]. The prefix was: %d", cidrPrefix));
}
// 根据cidrPrefix计算子网掩码
subnetMask = prefixToSubnetMask(cidrPrefix);
// ip地址 & 子网掩码 就能得到网络位
networkAddress = NetUtil.ipv4AddressToInt(ipAddress) & subnetMask;
this.ruleType = ruleType;
}
@Override
public boolean matches(InetSocketAddress remoteAddress) {
// 远程地址
final InetAddress inetAddress = remoteAddress.getAddress();
if (inetAddress instanceof Inet4Address) {
int ipAddress = NetUtil.ipv4AddressToInt((Inet4Address) inetAddress);
// 用远程地址的ip地址 & 子网掩码,计算网络位,看网络位是否相同,即是不是在同一个网段内
return (ipAddress & subnetMask) == networkAddress;
}
return false;
}

示例:使用黑名单

在Server端代码中使用RuleBasedIpFilter

// 创建过滤规则,远程IP是127.0.0.1(转换之后networkAddress为2130706433), cidrPrefix为8(子网掩码为255.0.0.0,转换之后subnetMask为-16777216),策略为拒绝,IP & 子网掩码 = 2130706432,地址描述为127.0.0.0
IpSubnetFilterRule ipSubnetFilterRule = new IpSubnetFilterRule("127.0.0.1", 8,
IpFilterRuleType.REJECT);
// RuleBasedIpFilter有@Sharable注解,支持共享
RuleBasedIpFilter ruleBasedIpFilter = new RuleBasedIpFilter(ipSubnetFilterRule);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
// 添加到pipeline中
pipeline.addLast("ipfilter", ruleBasedIpFilter);
pipeline.addLast("TShandler", globalTrafficShapingHandler);
...
...
}
});

运行Server和Client,可以看到Client端显示连接断开。

将ipSubnetFilterRule修改为new IpSubnetFilterRule(“127.1.0.1”, 16, IpFilterRuleType.REJECT)。

远程IP是127.1.0.1(转换之后networkAddress为2130771969), cidrPrefix为16(子网掩码为255.255.0.0,转换之后subnetMask为-65536),策略为拒绝,IP & 子网掩码 = 2130771968,地址描述为127.1.0.0。而我们请求时,IP为127.0.0.1,计算出的IP & 子网掩码 = 2130706432,地址描述为127.0.0.0,就不会被拦截了。

01111111.00000000.00000000.00000001 127.0.0.1
11111111.00000000.00000000.00000000 255.0.0.0(/8)
===================================
01111111.00000000.00000000.00000000 127.0.0.0
01111111.00000001.00000000.00000001 127.1.0.1
11111111.11111111.00000000.00000000 255.255.0.0(/16)
===================================
01111111.00000001.00000000.00000000 127.1.0.0

自定义授权

在Server端创建授权Handler

// 没有资源竞争,可以共享
@Slf4j
@ChannelHandler.Sharable
public class AuthHandler extends SimpleChannelInboundHandler<RequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RequestMessage msg) throws Exception {
try {
Operation operation = msg.getMessageBody();
// 判断是不是鉴权Handler
if (operation instanceof AuthOperation){
AuthOperation authOperation = AuthOperation.class.cast(operation);
AuthOperationResult authOperationResult = authOperation.execute();
// 判断是否授权验证通过
if (authOperationResult.isPassAuth()){
log.info("pass auth");
}else {
log.error("fail to auth");
ctx.close();
}
}else {
// 不是鉴权Handler,则直接关闭连接
log.error("expect first msg is auth");
ctx.close();
}
}catch (Exception e){
// 如果抛出异常,关闭连接
log.error("exception happen");
ctx.close();
}finally {
// 验证通过,后续不需要继续验证授权;验证不通过,连接关闭了,所以也不需要这个授权验证了;移除handler
ctx.pipeline().remove(this);
}
}
}
AuthHandler authHandler = new AuthHandler();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
pipeline.addLast("ipfilter", ruleBasedIpFilter);
pipeline.addLast("TShandler", globalTrafficShapingHandler);
pipeline.addLast("idleCheck",new ServerIdleCheckHandler());
pipeline.addLast("frameDecode", new OrderFrameDecoder());
pipeline.addLast(new OrderProtocolDecoder());
pipeline.addLast(new OrderFrameEncoder());
pipeline.addLast(new OrderProtocolEncoder());
pipeline.addLast("metricsHandler", metricsHandler);
// 添加授权handler,因为接收到的参数是RequestMessage,为二次解码后的数据,所以要放在ProtocolDecoder之后
pipeline.addLast("auth", authHandler);
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
...
...
}
});

此时启动Server和Client,可以看到Client断开了连接,Server端则打出了第一个handler不是auth的日志

[worker-3-1][ERROR][org.example.server.codec.handler.AuthHandler:29] expect first msg is auth

在Client添加Auth的请求

RequestMessage authRequest = new RequestMessage(IdUtil.nextId(), new AuthOperation("admin", "password"));
channelFuture.channel().writeAndFlush(authRequest);
RequestMessage requestMessage = new RequestMessage(IdUtil.nextId(), new OrderOperation(1001, "tudou"));
channelFuture.channel().writeAndFlush(requestMessage);

此时启动Server和Client,可以看到Client没有被断开,Server的日志中也出现了pass auth的日志,如果将userName改为admin2,再次执行,可以看到Client被断开连接了,Server中出现了fail to auth的日志

使用SSL

什么是SSL

SSL/TLS协议在传输层之上封装了应用层数据,不需要修改应用层协议的前提下提供安全保障

SSL:TLS

第二行为IP层,第三行为TCP层,第四层为应用层,但是这里可以看到是一个SSL,比如说http,那http的数据可以放到加密的application data里面。

应用层的数据,通过加密,存储到ssl协议中的application data。

TLS(传输层安全)是更为安全的升级版SSL。

SSL的功能与设计

表象

内容加密方式:对称加密

对称加密秘钥传递:非对称加密

对称加密的秘钥产生:三个随机数一起产生,client hello是携带随机数,server hello时携带随机数,client产生并发送给server的pre master key用非对称加密的方式传递

Note:本节示例基于”单向验证+交换秘钥方式为RSA方式“是简单,最雏形的SSL

为什么采用对称加密而不是非对称加密:因为对称加密简单,效率高。

非对称加密的公钥信息是放在证书上的。

证书的来源:自己做证书或者购买授权的证书。

SSL的抓包演示与解析

抓包工具:wireshark https://www.wireshark.org/#download

低版本需要安装Npcap https://wiki.wireshark.org/CaptureSetup/Loopback

抓包案例

  • io.netty.example.securechat.SecureChatClient:

    final SslContext sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).ciphers(Arrays.asList("TLS_RSA_WITH_AES_256_CBC_SHA")).build();

  • io.netty.example.securechat.SecureChatServer:

    SelffSignedCertificate ssc = new SelfSignedCertificate(); System.out.pintln(ssc.privateKey());

修改SecureChatClient

public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).ciphers(Arrays.asList("TLS_RSA_WITH_AES_256_CBC_SHA")).build();
EventLoopGroup group = new NioEventLoopGroup();
...
...
}

修改SecureChatServer

public static void main(String[] args) throws Exception {
SelfSignedCertificate ssc = new SelfSignedCertificate();
System.out.println(ssc.certificate());
System.out.println(ssc.privateKey());
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.build();
...
...
}

启动Server和Client,Server端可以看到证书地址和秘钥地址,Client端可以看到日志

Welcome to 192.168.0.100 secure chat service!
Your session is protected by TLS_AES_128_GCM_SHA256 cipher suite.

启动wireshark,并选择Loopback

wireshark-1

在过滤器中输入过滤条件ssl && tcp.dstport==8992 || tcp.srcport==8992 ,表示ssl协议并且接收端口或者发送端口是8992的,重启Server和Client,可以看到有5组消息被筛选出来了,此时可以停止wireshare的分析和代码

wireshark-2

Client Hello

ssl-client-hello

可以看到clien-hello中有一个Random,就是之前所说的随机数,然后提供了Cipher Suites秘钥套件,类似于自己提供出来,然后让别人去选

Server Hello, Certificate, Server Hello Done

server-hello中也带了一个随机数,同时告诉client选择一个什么加密方式

ssl-server-hello-1

同时还带了证书的公钥信息,最后执行了一个hello done

ssl-server-hello-2

Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message

客户端拿到证书的公钥信息,自己有一个随机数,server hello也给它了一个随机数,这个时候,他就产生了pre master key,通过公钥加密传给server,对端收到PreMaster之后,就可以解密出这个对称加密的秘钥。

Change Cipher Spec是告诉对方接下来要开始加密了,接着就开始进行加密了。

clientKeyExchange

New Session Ticket, Change Cipher Spec, Encrypted Handshake Message, Application Data

告诉对方我也可以进行加密了

newSessionTicket

Application Data

被加密的数据,就是日志打出的信息

applicationData

SSL流程

SSL流程

使用SSL

在Netty中使用SSL:io.netty.handler.ssl.SslHandler

SslHandler

在Netty中使用SSL:单向认证

  • 服务器端准备证书:自签或购买
  • 服务器端加上SSL功能
  • 导入证书到客户端
  • 客户端加入SSL功能

Server端

// 创建自签证书
SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
// 打印证书路径
System.out.println(selfSignedCertificate.certificate());
// 创建sslContext
SslContext sslContext = SslContextBuilder.forServer(selfSignedCertificate.certificate(),
selfSignedCertificate.privateKey()).build();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
pipeline.addLast("ipfilter", ruleBasedIpFilter);
pipeline.addLast("TShandler", globalTrafficShapingHandler);
pipeline.addLast("idleCheck", new ServerIdleCheckHandler());
// 添加handler
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
pipeline.addLast("ssl", sslHandler);
pipeline.addLast("frameDecode", new OrderFrameDecoder());
...
...
}
});

Client端:

// 创建SslContext
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
SslContext sslContext = sslContextBuilder.build();
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 注意顺序
pipeline.addLast(new ClientIdleCheckHandler());
// 加入handler
pipeline.addLast(sslContext.newHandler(ch.alloc()));
pipeline.addLast(new ClientOrderFrameDecoder());
...
...
}
});

启动Server端和Client端

此时会发现Client端报错了,不信任证书,所以将证书加入信任

# 进去JAVA_HOME目录
cd ${JAVA_HOME}
# 添加 -file 命令之后是Server端打印出的证书位置
keytool -import -alias netty -keystore "jre/lib/security/cacerts" -file "/var/folders/k2/n1jx0n1s7p5f_x2d3gpyzgcr0000gn/T/keyutil_localhost_1284024405885253963.crt" -storepass changeit
# 移除
keytool -delete -alias netty -keystore "jre/lib/security/cacerts" -storepass changeit

添加信任之后,Client端就可以正常执行了