安全增强
设置高低水位线
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
/**
* 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端的日志
可以看到日志中输出了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日志
可以看到Server端我们加入的日志已经打印出来了,再查看Client端日志
可以看到连接断开了
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:
Server:
黑白名单
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协议在传输层之上封装了应用层数据,不需要修改应用层协议的前提下提供安全保障
第二行为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
在过滤器中输入过滤条件ssl && tcp.dstport==8992 || tcp.srcport==8992
,表示ssl协议并且接收端口或者发送端口是8992的,重启Server和Client,可以看到有5组消息被筛选出来了,此时可以停止wireshare的分析和代码
Client Hello
可以看到clien-hello中有一个Random,就是之前所说的随机数,然后提供了Cipher Suites秘钥套件,类似于自己提供出来,然后让别人去选
Server Hello, Certificate, Server Hello Done
server-hello中也带了一个随机数,同时告诉client选择一个什么加密方式
同时还带了证书的公钥信息,最后执行了一个hello done
Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
客户端拿到证书的公钥信息,自己有一个随机数,server hello也给它了一个随机数,这个时候,他就产生了pre master key,通过公钥加密传给server,对端收到PreMaster之后,就可以解密出这个对称加密的秘钥。
Change Cipher Spec是告诉对方接下来要开始加密了,接着就开始进行加密了。
New Session Ticket, Change Cipher Spec, Encrypted Handshake Message, Application Data
告诉对方我也可以进行加密了
Application Data
被加密的数据,就是日志打出的信息
SSL流程
使用SSL
在Netty中使用SSL:io.netty.handler.ssl.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端就可以正常执行了