前言
我们在上一篇文章中已经讲了Closs Session
以及其和临时节点的关系,我们讲了在客户端输入quit
命令的时候Zookeeper
的源码是怎么一个流程。所以我们这一篇博客就是要讲解最后一个小点:Zookeeper
的ACL
机制是什么样的,在源码中又是一个什么过程。同时笔者认为有必要说明一下,本篇内容主要是为了解读源码服务的,所以Zookeeper的权限操作的内容,以及权限分分类组成就不会过多的涉及,这些网络上已经有了很多帖子去讲解。所以笔者这里除非是很有必要,否则不会过多讲解,如果以后有时间再补上一篇,请想要了解的同学自行查找资料学习。
此篇笔者希望尽量减少博客的长度,而且也前面的几篇博客为了跳转需要,已经贴了太多的冗余代码这也导致文章冗长。从这篇往后的博客中如果涉及到前期直接的代码跳转,笔者就全部放在一个代码块里用来减少文章的篇幅。同时本篇也将会是单机模式的最后一节内容,过了这一小节,我们就可以开始正式的探究Zookeeper
的集群模式是什么样的流程了。本篇也会被收录到【Zookeeper 源码解读系列目录】中。
Zookeeper的权限操作简介
为了更好的讲解权限问题,我们找一个老熟人作为我们的主角:xiaoming。再说具体例子之前,我们先说一下Zookeeper的权限,Zookeeper一共有5大权限:
权限名称 | 命令简写 | 命令说明 |
---|---|---|
Admin | a | 设置节点acl的权限 |
Create | c | 创建节点的权限 |
Read | r | 读节点权限 |
Write | w | 写节点权限,包括set等等 |
Delete | d | 删除节点,如果节点有子节点,则只能删除子节点。清空子节点以后才能删除当前节点。 |
那么设置权限的命令格式就是:setAcl /节点名字 auth:用户名:密码:权限
。具体到示例就是:比如要给xiaoming
管理、删除、读的权限,没有写权限,就是setAcl /node auth:xiaoming:123123:adr
。如果我们这样给了xiaoming
权限,那么xiaoming
就不能写操作,这样当setData
修改节点的值得时候,肯定有一个checkAcl
得操作检查当前账号有没有权限。然后getAcl
查看权限,还有添加权限addauth digest xiaoming:123123
。也不仅仅是账号名字,也可以是ip
,比如SetAcl /node auth:192.168.0.1:adr
,中间的账号名字可以替换为ip
,这就是给了客户端ip
为192.168.0.1
的adr
权限。
客户端中的addauth
我们找addauth
为例子进行讲解,还是一层层的进入,但是为了减少篇幅,这次要更加的简略:
----入口----> ZooKeeperMain.main();
----转到----> ZooKeeperMain.run();
----转到----> ZooKeeperMain.processCmd(cl);
----转到----> ZooKeeperMain.processZKCmd(co);
所以我们跟着代码一路就走到了processZKCmd(co)
这个方法里,删去冗余代码:
protected boolean processZKCmd(MyCommandOptions co)
throws KeeperException, IOException, InterruptedException
{
/**略**/
if (cmd.equals("***") && args.length >= **) {
/**略**/
}else if (cmd.equals("addauth") && args.length >=2 ) {
byte[] b = null;
if (args.length >= 3)
b = args[2].getBytes();
zk.addAuthInfo(args[1], b);
}
/**略**/
return watch;
}
然后找到if (cmd.equals("addauth")
这个处理addauth
的逻辑分支里,这里会用Zookeeper
对象调用addAuthInfo(args[1], b)
这个方法,那就点进去:
public void addAuthInfo(String scheme, byte auth[]) {
cnxn.addAuthInfo(scheme, auth);
}
进入以后,这里又交给cnxn
这个对象处理了,其实到了这里读过以前系列的同学应该可以才想到,下面客户=端的逻辑基本上没什么了,因为这个时候就是NIOClientCnxn
处理了,这个类简单了说它就干了一个事儿:通过socket
发送消息给到服务端,那我们进入看:
public void addAuthInfo(String scheme, byte auth[]) {
if (!state.isAlive()) {
return;
}
authInfo.add(new AuthData(scheme, auth)); //添加到队列里
queuePacket(new RequestHeader(-4, OpCode.auth), null,
new AuthPacket(0, scheme, auth), null, null, null, null,
null, null); //构造packet,存入发送队列
}
果然发现authInfo.add(new AuthData(scheme, auth));
先构造一个AuthData
的实例对象,添加到本地队列authInfo
,然后仍然是调用queuePacket(***)
构造packet
,加到outgoingQueue
队列里,最后和别的命令一样传到服务端。但是这里要注意,我们输入的是addauth
命令,但是封装的是OpCode.auth
验证的操作,所以转到服务端的时候,我们要去看的分支是OpCode.auth
。
服务端中的addauth
我们接下来就要看服务端是怎么处理这个请求的了,按照之前的命令传递流程:
----入口----> QuorumPeerMain.main();
----转到----> ZooKeeperServerMain.main(args);
----转到----> ZooKeeperServerMain.initializeAndRun(args);
----转到----> ZooKeeperServerMain.runFromConfig(config);
----转到----> NIOServerCnxnFactory.startup(zkServer);
----转到----> NIOServerCnxnFactory.start();
----转到----> NIOServerCnxnFactory.run();
----转到----> NIOServerCnxn.doIO(k);
----转到----> NIOServerCnxn.readPayload();
----转到----> NIOServerCnxn.readRequest();
----转到----> ZooKeeperServer.processPacket(this, incomingBuffer);
到这里我们停下来分析AddAuth
命令在这个方法里面是怎么走的:
public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
/**略**/
if (h.getType() == OpCode.auth) {//addauth命令
LOG.info("got auth packet " + cnxn.getRemoteSocketAddress());
AuthPacket authPacket = new AuthPacket();//创建一个authPacket实例
//反序列化给authPacket赋值
ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket);
String scheme = authPacket.getScheme(); //获取scheme
AuthenticationProvider ap = ProviderRegistry.getProvider(scheme);
Code authReturn = KeeperException.Code.AUTHFAILED;
if(ap != null) {
try {
//这里调用生成的实例,以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**/
} else {
LOG.warn("Authentication failed for scheme: " + scheme);
}
ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
KeeperException.Code.AUTHFAILED.intValue());
cnxn.sendResponse(rh, null, null);
cnxn.sendBuffer(ServerCnxnFactory.closeConn);
cnxn.disableRecv();
} else {
/**打印LOG**/
ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
KeeperException.Code.OK.intValue());
cnxn.sendResponse(rh, null, null);
}
return;
} else {//不是auth,就直接来到这里
/**略**/
submitRequest(si); //提交,走处理器链逻辑
}
/**略**/
}
这个方法其实也是我们的老熟人儿了,之前我们一直在讲的是非OpCode.auth
的情况,所以直接就到了下面的else
里,提交请求submitRequest(si);
直接走处理器链了。但是OpCode.auth
就不是这么处理的了。这次我们定位到if (h.getType() == OpCode.auth)
。首先代码里构造了一个AuthPacket的空对象,然后从incomingBuffer
里反序列化出来数据给authPacket
赋值,再往后构造String
,获取传递进来的scheme = authPacket.getScheme();
,这里scheme
应该怎么理解呢?其实就是几种不同的Auth
的方式一共有四种,分别是world、auth、ip
和digest
,这里就不细说区别了。接着根据传递进来的不同scheme
生成不同的AuthenticationProvider
,这个接口有三个继承类DigestAuthenticationProvider
,IPAuthenticationProvider
和SASLAuthenticationProvider
会根据传入来生成不同的实例。比如我们传入的是digest
则生成DigestAuthenticationProvider
实例,我们就以这个为例子接着往下看。此时我们的ap
已经是有值的了,那么就进入这个if(ap != null)
块里,就直接去直接去调用DigestAP
里面的方法ap.handleAuthentication(cnxn, authPacket.getAuth());
,所以就去DigestAuthenticationProvider
类里面找到这个handleAuthentication()
方法:
public KeeperException.Code
handleAuthentication(ServerCnxn cnxn, byte[] authData)
{
String id = new String(authData);//拿出传入的数据
try {
String digest = generateDigest(id);//构造签名
if (digest.equals(superDigest)) { //认证超级管理员
cnxn.addAuthInfo(new Id("super", ""));
}
cnxn.addAuthInfo(new Id(getScheme(), digest));//把addauth后面的信息存到authInfo里去
return KeeperException.Code.OK;
} catch (NoSuchAlgorithmException e) {
LOG.error("Missing algorithm",e);
}
return KeeperException.Code.AUTHFAILED;
}
进入后,首先拿出传入的数据authData
包装成为一个叫做id
字符串,这个authData
就是我们刚才输入的xiaoming:123123
,再下面发现了一个生成签名的方法generateDigest(id);
,这个生成签名只是给每一个用户一个特殊码用来区分用户的,里面是用的id
加上SHA1
生成唯一码,再用base64
构建的,这里没什么可说的,想看的同学自己点进去看一眼就明白了,就是对用户名和密码进行一些字符串转换和加密。然后就是判断超级管理员账户了if (digest.equals(superDigest))
,这个superDigest
就是超级管理员,其默认值是从配置中读取出来的,也就是说我们启动前配置的超级管理员,就是在这里被加入服务器的。当然你执行addauth
命令的时候直接指定用户名super
也是可以的构造一个超级管理员的。如果是个普通用户,就直接存到authInfo
里去cnxn.addAuthInfo(new Id(getScheme(), digest));
,在后面就是直接return Code.OK
,跳出继续走到cnxn.sendResponse(rh, null, null);
返回给客户端,那么自此addauth
命令彻底结束。总结来说,addauth
命令就是在ServerCnxn
类里面的authInfo
这个List里面存把addauth
里面的数据存入进入,这个ServerCnxn.authInfo
属性我们一会儿还要说到,这里先记下。那么最终存入的形式就是
xiaoming:123123
zhangsan:1234124
ip:192.168.0.22 //如果auth是ip的话,我们放到后面讲
addauth ip
当执行addauth
后面加的是一个ip
又是什么样的情况呢?省略掉之前的步骤,这次方法的提供方就要换成IPAuthenticationProvider.handleAuthentication(cnxn, authPacket.getAuth())
:
public KeeperException.Code
handleAuthentication(ServerCnxn cnxn, byte[] authData)
{
String id = cnxn.getRemoteSocketAddress().getAddress().getHostAddress();
cnxn.addAuthInfo(new Id(getc(), id));
return KeeperException.Code.OK;
}
进入方法以后,注意cnxn.addAuthInfo(new Id(getScheme(), id));
这里。我们看new Id(getScheme(), id)
传进入的参数有两个,第一个是Scheme
就是我们传入的"ip"
这个字符串,第二个是id
并不是我们要传入的数据,而是取得客户端host地址的一个列表元素,而自己写入的192.168.0.1
反而是无效的。所以如果你输入addauth ip 192.168.0.1
命令是无效的,Zookeeper
会自动把你输入的ip
忽略掉而替换为连接的ip
。这一点其实不太明白Zookeeper
的开发人员是怎么想的。但是要加ip
白名单应该怎么办呢?只有用setAcl
命令才能给ip
加入白名单列表,就是 setAcl /nodename ip:192.168.1.33:cdrwa
。这样这个节点就可以对特定的ip打开所有的权限了,如下图所示。
可以看到这里其实也是把ip
和对应的数值绑定一个类似的map
结构中,这点和用户名密码是一致的。但是这里要提醒一点,如果想要给某个ip
加上权限,一定要首先把本机添加进去,否则就会尴尬的发现除了你刚刚添加的ip
,其余的客户端都无法操作这个node
了。可以看出Zookeeper
这里的权限控制的其实非常的不好。
ACL的验证与设置
通过上述addauth
的代码分析,我们输入的信息都被存入了ServerCnxn.authinfo
这个对象存里面。而authinfo
对象的内容就是账户名字和密码,构造的id这些信息,但是这个到底有什么用呢?那么我们就得看setACL
这个方法,但是我们做这些操作的时候会首先验证权限,setACL
其实就是对某一个节点存一个ACL
的记录,所以我们得先关注checkACL
方法。所以我们就去服务端去看下,到底是怎么checkACL以及怎么setACL的,首先是客户端:
----入口----> ZooKeeperMain.main();
----转到----> ZooKeeperMain.run();
----转到----> ZooKeeperMain.processCmd(cl);
----转到----> ZooKeeperMain.processZKCmd(co);
还是走到了processZKCmd(co)
这个方法里,这回我们找到if (cmd.equals("setAcl") && args.length >= 3)
这个逻辑块里面:
protected boolean processZKCmd(MyCommandOptions co)
throws KeeperException, IOException, InterruptedException
{
/**略**/
if (cmd.equals("***") && args.length >= **) {
/**略**/
}else if (cmd.equals("setAcl") && args.length >= 3) {
path = args[1];
stat = zk.setACL(path, parseACLs(args[2]),
args.length > 4 ? Integer.parseInt(args[3]) : -1);
printStat(stat);
}
/**略**/
return watch;
}
进入以后找到zk.setACL(path, parseACLs(args[2]), args.length > 4 ? Integer.parseInt(args[3]) : -1);
方法,点进去:
public Stat setACL(final String path, List<ACL> acl, int aclVersion)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath); //验证路径
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.setACL); //OpCode.setACL
SetACLRequest request = new SetACLRequest();
request.setPath(serverPath);
if (acl != null && acl.size() == 0) {
throw new KeeperException.InvalidACLException(clientPath);
}
request.setAcl(acl);
request.setVersion(aclVersion);
SetACLResponse response = new SetACLResponse();
ReplyHeader r = cnxn.submitRequest(h, request, response, null); //提交
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
return response.getStat();
}
这里有什么就不分析了,主要是看h.setType(ZooDefs.OpCode.setACL);
这句话,setAcl
命令的操作码就是OpCode.setACL
所以我们去服务端找的就是OpCode.setACL
,而不是OpCode.auth
了。提交请求以后直接转到服务端:
----入口----> QuorumPeerMain.main();
----转到----> ZooKeeperServerMain.main(args);
----转到----> ZooKeeperServerMain.initializeAndRun(args);
----转到----> ZooKeeperServerMain.runFromConfig(config);
----转到----> NIOServerCnxnFactory.startup(zkServer);
----转到----> NIOServerCnxnFactory.start();
----转到----> NIOServerCnxnFactory.run();
----转到----> NIOServerCnxn.doIO(k);
----转到----> NIOServerCnxn.readPayload();
----转到----> NIOServerCnxn.readRequest();
----转到----> ZooKeeperServer.processPacket(this, incomingBuffer);
----转到----> ZooKeeperServer.submitRequest(si);
----转到----> PrepRequestProcessor.processRequest(si); //添加请求到队列
----转到----> PrepRequestProcessor.run();
----转到----> PrepRequestProcessor.pRequest(request);
我们就直接来到了请求处理器链这里,进入pRequest(request)
方法,找到OpCode.setACL
的分支:
protected void pRequest(Request request) throws RequestProcessorException {
/**略**/
try {
switch (request.type) {
case OpCode.***:
/**略**/
break;
case OpCode.setACL: //到这里来
SetACLRequest setAclRequest = new SetACLRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
break;
default:
LOG.warn("unknown type " + request.type);
break;
}
} catch (***Exception e) {
/**Exceptions**/
}
request.zxid = zks.getZxid();
nextProcessor.processRequest(request);
}
继续进入pRequest2Txn(***)
方法,一样找到OpCode.setACL
的分支:
protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize)
throws KeeperException, IOException, RequestProcessorException
{
/**略**/
switch (type) {
/**略**/
case OpCode.***:
/**略**/
break;
case OpCode.setACL:
zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
SetACLRequest setAclRequest = (SetACLRequest)record;//拿出数据
if(deserialize)//反序列化
ByteBufferInputStream.byteBuffer2Record(request.request, setAclRequest);
path = setAclRequest.getPath(); //拿出路径
validatePath(path, request.sessionId);//验证路径
listACL = removeDuplicates(setAclRequest.getAcl());//去重
if (!fixupACL(request.authInfo, listACL)) {
throw new KeeperException.InvalidACLException(path);
}
nodeRecord = getRecordForPath(path);
checkACL(zks, nodeRecord.acl, ZooDefs.Perms.ADMIN,
request.authInfo); //验证ACL
version = setAclRequest.getVersion();
currentVersion = nodeRecord.stat.getAversion();
if (version != -1 && version != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;
request.txn = new SetACLTxn(path, listACL, version);//列表事务化,为持久化准备
nodeRecord = nodeRecord.duplicate(request.hdr.getZxid());
nodeRecord.stat.setAversion(version);
addChangeRecord(nodeRecord);
break;
default:
LOG.error("Invalid OpCode: {} received by PrepRequestProcessor", type);
}
}
进入这个分支以后,略过拿数据、验证等等操作这些都和以前一样,不多说。直接找到listACL = removeDuplicates(setAclRequest.getAcl());
这句话是用来去重的,比如输入权限是addddr
,经过这个方法以后就会简化为adr
。再往下看到一个if判断里面有一个方法fixupACL(request.authInfo, listACL)
,这方法其实就是真正修改权限的地方,这里的两个参数request.authInfo
是需要被验证的id
信息,就是刚刚设置的请求赋权的账户列表,里面寸的就是我们之前小节里面说的id,listACL
就是分配给节点的acl列表,就是id和其对应的权限的列表。这个我们一会儿再看,我们先看验权的部分:checkACL(zks, nodeRecord.acl, ZooDefs.Perms.ADMIN, request.authInfo);
这里,这个方法一共传入了四个参数:zks
是连接信息;acl
是节点已经存在的节点规则;ZooDefs.Perms.ADMIN
是当前这个操作需要的权限,这里是要修改,所以我们要的权限就是ADMIN
;request.authInfo
是需要被验证的id
信息,这里的数据就是我们调用addauth
加入进去的那些信息,用来看是谁要被验证:
static void checkACL(ZooKeeperServer zks, List<ACL> acl, int perm,
List<Id> ids) throws KeeperException.NoAuthException {
if (skipACL) {//跳过ACL的配置
return;
}
if (acl == null || acl.size() == 0) {//当前节点是否没有任何acl规则
return;
}
for (Id authId : ids) {
if (authId.getScheme().equals("super")) {//如果传入super,就直接返回通过
return;
}
}
for (ACL a : acl) { //开始循环验证规则
Id id = a.getId(); //当前节点上的id
if ((a.getPerms() & perm) != 0) {
if (id.getScheme().equals("world")
&& id.getId().equals("anyone")) {
return;
}
AuthenticationProvider ap = ProviderRegistry.getProvider(id
.getScheme());
if (ap != null) {
for (Id authId : ids) {
if (authId.getScheme().equals(id.getScheme())
&& ap.matches(authId.getId(), id.getId())) {
return;
}
}
}
}
}
throw new KeeperException.NoAuthException();
}
进入这个方法以后,首先看到if (skipACL)
,这个是检查有没有配置过跳过ACL
的配置,就是在配置文件中配置skipACL=yes
的参数了,这里就会直接跳过,不在验证ACL
了,这个也是Zookeeper验权的第一个后门,只要你知道zoo.cfg
文件在哪里,重启了就可以绕过验证。然后第二个验证if (acl == null || acl.size() == 0)
如果当前节点没有任何acl
规则,那就不需要验证了,直接返回。再往下for (Id authId : ids)
循环,如果发现传入的id
列表里面有super
用户,也是直接跳过。这里就是第二个验权的后门,我只要在setACL
之前,运行一个addauth super
的命令,就不用验证了。因为这里的逻辑就是只要ids
里面发现一个super
用户,所有的验证都会被跳过,虽然我们知道这里是想实现超级管理员用户的作用的,但是确实逻辑上有些不妥当。再往下的for (ACL a : acl)
循环就是开始验证真正我们传入的规则了,进入循环以后首先把当前节点上的id
拿出来Id id = a.getId();
,要注意这个a
也是当前节点上的acl
,不要搞混了。接着取出节点上的权限a.getPerms()
和验证的权限perm
做与&
操作,如果结果不等于0,说明符合权限,就是“我”
这个节点有“你”
想要的权限,至于为什么要&
操作我们最后再讲,涉及到位移操作比较麻烦,大家先记住就好。进入以后,如果发现拿到的scheme
是world
或者anyone
直接验权过。再往下拿到验证器AuthenticationProvider
对每一个请求authId
的scheme
和当前节点id
的scheme
进行对比,两者都通过才会通过验证,如果不通过,抛出异常。就是这个if
语句里面的条件:先authId.getScheme().equals(id.getScheme()
对比各自的scheme
,再对比是不是匹配ap.matches(authId.getId(), id.getId())
。这里就是checkAcl
的逻辑。
Zookeeper原生ACL验证的缺陷
经过刚才我们分析了,除了有后门以外,其实非常的不好用,因为首先要验权就得addauth
把所有的账户添加进去,然后再进行其他的操作。但是addauth
命令就像一个登陆指令,只要执行了,就像已经在这个账户下登陆的,所以如果权限不够,那基本上干不成其他操作了。就比如我刚才说的,设置了一个ip
白名单,但是我本身的客户端却无法再操作那个node
了。
除了这些以外,其实还有一个bug,那就是它会把所有的用户给同步统一成一种权限。比如说我们本身有user1:ad
,就是说user1
对这个节点有admin
和delete
的权限。我们现在想给这个节点添加两个用户user2
和user3
,由于这个节点已经有了user1
,且user1
还有admin
权限。那么我如果不加上user1
,也就是addauth user1
,那么验证就不通过。所以必须加上user1
,然后我们想要只修改user2
的账户给adrw
的权限, 用(也只能用)setAcl /node auth:user2:123123:adrw
命令,那么运行后我们会神奇的发现:user1
、user2
、user3
三个账号同时都有adrw的权限。怎么就出现这样的问题呢?问题其实就在fixupACL(request.authInfo, listACL)
这个方法里,那么我们进入看一下这个参数,首先authInfo
里面包含了所有addauth
的id
对吧,此时user1
、user2
、user3
里面都有。然后listACL
就是我想要修改的权限。那么我们进入方法:
private boolean fixupACL(List<Id> authInfo, List<ACL> acl) {
if (skipACL) {
return true;
}
if (acl == null || acl.size() == 0) {
return false;
}
Iterator<ACL> it = acl.iterator(); //这里的acl就是addauth后所有的内容
LinkedList<ACL> toAdd = null;
while (it.hasNext()) {
ACL a = it.next();
Id id = a.getId();
if (id.getScheme().equals("world") && id.getId().equals("anyone")) {
// wide open
} else if (id.getScheme().equals("auth")) {
it.remove();
if (toAdd == null) {
toAdd = new LinkedList<ACL>();
}
boolean authIdValid = false;
for (Id cid : authInfo) {//这里循环的cid不包括权限
AuthenticationProvider ap =
ProviderRegistry.getProvider(cid.getScheme());
if (ap == null) {
LOG.error("Missing AuthenticationProvider for "
+ cid.getScheme());
} else if (ap.isAuthenticated()) {
authIdValid = true;
//对toAdd里面的cid添加a.getPerms()的权限
toAdd.add(new ACL(a.getPerms(), cid));
}
}
if (!authIdValid) {
return false;
}
} else {
AuthenticationProvider ap = ProviderRegistry.getProvider(id
.getScheme());
if (ap == null) {
return false;
}
if (!ap.isValid(id.getId())) {
return false;
}
}
}
if (toAdd != null) {
for (ACL a : toAdd) {//把这个add循环又加到了acl中
acl.add(a);
}
}
return acl.size() > 0;
}
方法比较长,但是关注点其实没多少,首先一个点就是Iterator<ACL> it = acl.iterator();
,这个it取的就是我们要添加的权限,也就是addauth
后所有的内容。然后对每一个id
进行for (Id cid : authInfo)
循环,在循环里做了一个什么事儿呢?对toAdd
这个权限列表里面的cid
添加a.getPerms()
的权限,而这个a
就是传进来的acl
列表里面的元素,这里全部的id
被全部循环了。有点绕,简单来说就是把adrw
这个四个权限给user1、user2、user3
循环了一遍。这个时候user1、user2、user3
已经都有了adrw
权限,最后再用一个for
循环把toAdd
里面的内容全部添加到acl
里面,最终返回的用户都有同样的权限。这里为什么Zookeeper的作者要这么设计其实笔者也很困惑,也可能国外的大佬们确实想法独特,所以只能说,Zookeeper的ACL
机制能少用则少用,还是在我们自己的程序外面做好控制比较好。
Perms机制
我们先把源码贴出来
public interface Perms {
int READ = 1 << 0; //0000 0001 1
int WRITE = 1 << 1; //0000 0010 2
int CREATE = 1 << 2; //0000 0100 4
int DELETE = 1 << 3;//0000 1000 8
int ADMIN = 1 << 4;//0001 0000 16
int ALL = READ | WRITE | CREATE | DELETE | ADMIN; // 0001 1111 31
}
这里的权限标识方法其实是用16进制标识的,比如我们的READ权限就是0000 0001,转化为10进制就是1,同理ADMIN是0001 0000,转化为10进制就是16。那么我们的adr权限是什么呢?就是这些权限的16进制加在一起0001 1001,转化为10进制就是25。所以如果我要运算一个节点是不是有ADMIN权限,最快的方法就是&操作,只要&的结果不是0,那就说明权限是有的。比如25 & 16
就是0001 1001 & 0001 0000 = 0001 0000
就不是0,说明有权限。反之25 & 2
就是0001 1001 & 0000 0010 = 0000 0000
结果就是0,说明没有权限。其实很多架构和程序都有这样的操作,因为直接操作数字码是最快的。如果这里也看不懂,还是自己百度一下逻辑运算是个什么东西,真的没有办法再解释了。
总结
讲完ACL以后,我们看到ACL这里其实还是非常不好用的,但是瑕不掩瑜,Zookeeper还是非常牛的一款分布式架构,当然也可能是笔者道行比较肤浅,没有办参透其中的奥秘。但是不管如何的Zookeeper的单机模式终于告一段落。下面就将是集群模式的源码分析的内容了,感谢大家不吝时间的支持,看我啰嗦到现在,我们集群再见。