2020-02-08 补充流程图
如果对您有帮助,欢迎点赞支持, 如果有不对的地方,欢迎指出批评
什么是ACL(Access Control List)#
zookeeper在分布式系统中承担中间件的作用,它管理的每一个节点上可能都存储这重要的信息,因为应用可以读取到任意节点,这就可能造成安全问题,ACL的作用就是帮助zookeeper实现权限控制, 比如对节点的增删改查
addAuth客户端源码追踪入口#
通过前几篇博客的追踪我们知道了,客户端启动三条线程,如下
- 守护线程 sendThread 负责客户端和服务端的IO通信
- 守护线程 EventThread 负责处理服务端和客户端有关事务的事件
- 主线程 负责解析处理用户在控制台的输入
所以本篇博客的客户端入口选取的是客户端的主程序processZKCmd(MyCommandOptions co)
, 源码如下
protected boolean processZKCmd(MyCommandOptions co) throws KeeperException, IOException, InterruptedException { // todo 在这个方法中可以看到很多的命令行所支持的命令 Stat stat = new Stat(); // todo 获取命令行输入中 0 1 2 3 ... 位置的内容, 比如 0 位置是命令 1 2 3 位置可能就是不同的参数 String[] args = co.getArgArray(); String cmd = co.getCommand(); if (args.length < 1) { usage(); return false; } if (!commandMap.containsKey(cmd)) { usage(); return false; } boolean watch = args.length > 2; String path = null; List<ACL> acl = Ids.OPEN_ACL_UNSAFE; LOG.debug("Processing " + cmd); if (cmd.equals("quit")) { System.out.println("Quitting..."); zk.close(); System.exit(0); } . . . . } else if (cmd.equals("addauth") && args.length >= 2) { byte[] b = null; if (args.length >= 3) b = args[2].getBytes(); zk.addAuthInfo(args[1], b); } else if (!commandMap.containsKey(cmd)) { usage(); } return watch; }
假如说我们是想在服务端的上下文中添加一个授权的信息, 假设我们这样写addauth digest lisi:123123
,这条命令经过主线程处理之后就来到上述源码的else if (cmd.equals("addauth") && args.length >= 2)
部分, 然后调用了ZooKeeper.java的zk.addAuthInfo(args[1], b);
源码如下:
public void addAuthInfo(String scheme, byte auth[]) { cnxn.addAuthInfo(scheme, auth); }
继续跟进ClientCnxn
的addAuthInfo()
方法,源码如下 它主要做了两件事:
- 将sheme + auth 进行了封装
- 然后将seheme + auth 封装进了封装进Request,在经过
queuePacket()
方法封装进packet,添加到outgoingQueue中等待sendThread将其消费发送服务端
public void addAuthInfo(String scheme, byte auth[]) { if (!state.isAlive()) { return; } // todo 将用户输入的权限封装进 AuthData // todo 这也是ClientCnxn的内部类 authInfo.add(new AuthData(scheme, auth)); // todo 封装进一个request中 queuePacket(new RequestHeader(-4, OpCode.auth), null, new AuthPacket(0, scheme, auth), null, null, null, null, null, null); }
addAuth服务端的入口#
在服务端去处理客户端请求的是三个Processor
分别是:
PrepRequestProcessor
负责更新状态SyncRequestProcessor
同步处理器,主要负责将事务持久化FinalRequestProcessor
主要负责响应客户端
服务端选取的入口是 NIOServerCnxn.java
的readRequest()
, 源码如下:
// todo 解析客户端传递过来的packet private void readRequest() throws IOException { // todo ,跟进去看zkserver 如何处理packet zkServer.processPacket(this, incomingBuffer); }
继续跟进processPacket()
,源码如下:
虽然这段代码也挺长的,但是它的逻辑很清楚,
- 将客户端发送过来的数据反序列化进new出来的RequestHeader
- 跟进RequestHeader判断是否需要auth鉴定
- 需要:
- 创建AuthPacket对象,将数据反序列化进它里面
- 使用
AuthenticationProvider
进行权限验证 - 如果成功了返回
KeeperException.Code.OK
其他的状态是抛出异常中断操作
- 不需要
- 将客户端端发送过来的数据封装进Request
- 将Request扔向请求处理链进一步处理
其中AuthenticationProvider
在这里设计的很好,他是个接口,针对不同的schme它有不同的实现子类,这样当前的ap.handleAuthentication(cnxn, authPacket.getAuth());
一种写法,就可以实现多种不同的动作
// todo 在ZKserver中解析客户端发送过来的request public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException { // We have the request, now process and setup for next // todo 从bytebuffer中读取数据, 解析封装成 RequestHeader InputStream bais = new ByteBufferInputStream(incomingBuffer); BinaryInputArchive bia = BinaryInputArchive.getArchive(bais); RequestHeader h = new RequestHeader(); // todo 对RequestHeader 进行反序列化 h.deserialize(bia, "header"); // Through the magic of byte buffers, txn will not be pointing to the start of the txn // todo incomingBuffer = incomingBuffer.slice(); // todo 对应用户在命令行敲的 addauth命令 // todo 这次专程为了 探究auth而来 if (h.getType() == OpCode.auth) { LOG.info("got auth packet " + cnxn.getRemoteSocketAddress()); // todo 创建AuthPacket,将客户端发送过来的数据反序列化进 authPacket对象中 /** 下面的authPacket的属性 * private int type; * private String scheme; * private byte[] auth; */ AuthPacket authPacket = new AuthPacket(); ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket); String scheme = authPacket.getScheme(); AuthenticationProvider ap = ProviderRegistry.getProvider(scheme); Code authReturn = KeeperException.Code.AUTHFAILED; if(ap != null) { try { // todo 来到这里进一步处理, 跟进去 // todo AuthenticationProvider 有很多三个实现实现类, 分别处理不同的 Auth , 我们直接跟进去digest类中 authReturn = ap.handleAuthentication(cnxn, authPacket.getAuth()); } catch(RuntimeException e) { LOG.warn("Caught runtime exception from AuthenticationProvider: " + scheme + " due to " + e); authReturn = KeeperException.Code.AUTHFAILED; } } if (authReturn!= KeeperException.Code.OK) { if (ap == null) { LOG.warn("No authentication provider for scheme: " + scheme + " has " + ProviderRegistry.listProviders()); } else { LOG.warn("Authentication failed for scheme: " + scheme); } // send a response... ReplyHeader rh = new ReplyHeader(h.getXid(), 0, KeeperException.Code.AUTHFAILED.intValue()); cnxn.sendResponse(rh, null, null); // ... and close connection cnxn.sendBuffer(ServerCnxnFactory.closeConn); cnxn.disableRecv(); } else { if (LOG.isDebugEnabled()) { LOG.debug("Authentication succeeded for scheme: " + scheme); } LOG.info("auth success " + cnxn.getRemoteSocketAddress()); ReplyHeader rh = new ReplyHeader(h.getXid(), 0, KeeperException.Code.OK.intValue()); cnxn.sendResponse(rh, null, null); } return; } else { if (h.getType() == OpCode.sasl) { Record rsp = processSasl(incomingBuffer,cnxn); ReplyHeader rh = new ReplyHeader(h.getXid(), 0, KeeperException.Code.OK.intValue()); cnxn.sendResponse(rh,rsp, "response"); // not sure about 3rd arg..what is it? return; } else { // todo 将上面的信息包装成 request Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(), h.getType(), incomingBuffer, cnxn.getAuthInfo()); si.setOwner(ServerCnxn.me); // todo 提交request, 其实就是提交给服务端的 process处理器进行处理 submitRequest(si); } } cnxn.incrOutstandingRequests(h); }
因为我们的重点是查看ACL的实现机制,所以继续跟进 ap.handleAuthentication(cnxn, authPacket.getAuth());
(选择DigestAuthenticationProvier的实现) 源码如下:
这个方法算是核心方法, 主要了做了如下几件事
- 我们选择的是Digest模式,针对用户的输入
lisi:123123
这部分信息生成数字签名 - 如果这个用户是超级用户的话,在ServerCnxn维护的authInfo中添加
super : ''
比较是超级管理员 - 将当前的信息封装进Id对象,添加到 authInfo
- 认证成功?
- 返回
KeeperException.Code.OK;
- 认证失败
- 返回
KeeperException.Code.AUTHFAILED;
public KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte[] authData) { String id = new String(authData); try { // todo 生成一个签名, 跟进去看看下 签名的处理步骤, 就在上面 String digest = generateDigest(id); if (digest.equals(superDigest)) { // todo 从这个可以看出, zookeeper是存在超级管理员用户的, 跟进去看看 superDigest 其实就是读取配置文件得来的 //todo 满足这个条件就会在这个list中多存一个权限 cnxn.addAuthInfo(new Id("super", "")); } // todo 将scheme + digest 添加到cnxn的AuthInfo中 , cnxn.addAuthInfo(new Id(getScheme(), digest)); // todo 返回认证成功的标识 return KeeperException.Code.OK; } catch (NoSuchAlgorithmException e) { LOG.error("Missing algorithm", e); } return KeeperException.Code.AUTHFAILED; }
authInfo有啥用?#
它其实是一个List数组,存在于内存中,一旦客户端关闭了这个数组中存放的内容就全部丢失了
一般我们是这么玩的,比如,我们创建了一个node,但是不想让任何一个人都能访问他里面的数据,于是我们就他给添加一组ACL权限, 就像下面这样
# 创建节点 [zk: localhost:2181(CONNECTED) 0] create /node2 2 Created /node2 # 添加一个用户 [zk: localhost:2181(CONNECTED) 1] addauth digest lisi:123123 # 给这个node2节点设置一个;lisi的用户,只有这个lisi才拥有node的全部权限 [zk: localhost:2181(CONNECTED) 2] setAcl /node2 auth:lisi:cdrwa cZxid = 0x2d7 ctime = Fri Sep 27 08:19:58 CST 2019 mZxid = 0x2d7 mtime = Fri Sep 27 08:19:58 CST 2019 pZxid = 0x2d7 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 1 numChildren = 0 [zk: localhost:2181(CONNECTED) 3] getAcl /node2 'digest,'lisi:dcaK2UREXUmcqg6z9noXkh1bFaM= : cdrwa
这时候断开客户端的连接, 打开一个新的连接,重试get
# 会发现已经没有权限了 [zk: localhost:2181(CONNECTED) 1] getAcl /node2 Authentication is not valid : /node2 # 重新添加auth [zk: localhost:2181(CONNECTED) 2] addauth digest lisi:123123 [zk: localhost:2181(CONNECTED) 3] getAcl /node2 'digest,'lisi:dcaK2UREXUmcqg6z9noXkh1bFaM= : cdrwa
可以看到,经过本轮操作后,node2节点有了已经被持久化的特征,lisi才能对他有全部权限,这么看addauth digest lisi:123123就有点添加了一个用户的概念,只不过这个信息最终会存放在上面提到的authInfo中, 这也是为啥一旦重启了,想要访问得重新添加权限的原因
言归正传,接着看上面的函数,我们看它是如何进行签名的, 拿lisi:123123举例子
- 使用:分隔
- 将后半部分的123123经过SHA1加密
- 再进行BASE64加密
- 最后拼接 lisi:sugsduyfgyuadgfuyadadfgba...
// todo 签名的处理步骤 static public String generateDigest(String idPassword) throws NoSuchAlgorithmException { //todo 根据: 分隔 String parts[] = idPassword.split(":", 2); //todo 先用SHA1进行加密 byte digest[] = MessageDigest.getInstance("SHA1").digest( idPassword.getBytes()); //todo 再用BASE64进行加密 // todo username:签名 return parts[0] + ":" + base64Encode(digest); }
加密完成后有样的判断,证明zookeeper中是有超级管理员角色存在的
if (digest.equals(superDigest)) { // todo 从这个可以看出, zookeeper是存在超级管理员用户的, 跟进去看看 superDigest 其实就是读取配置文件得来的 //todo 满足这个条件就会在这个list中多存一个权限 cnxn.addAuthInfo(new Id("super", "")); }
点击superDisgest,他是这样介绍的
/** specify a command line property with key of * "zookeeper.DigestAuthenticationProvider.superDigest" * and value of "super:<base64encoded(SHA1(password))>" to enable * super user access (i.e. acls disabled) */ // todo 在命令行中指定 key = zookeeper.DigestAuthenticationProvider.superDigest // todo 指定value = super:<base64encoded(SHA1(password))> // todo 就可以开启超级管理员用户 private final static String superDigest = System.getProperty( "zookeeper.DigestAuthenticationProvider.superDigest");
小结:#
到目前为止,我们就知道了addauth在底层源码做出了哪些动作,以及服务端将我们手动添加进来的权限信息都放在内存中