【分布式】ZooKeeper权限控制之ACL(Access Control List)访问控制列表

zk做为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK通过ACL机制来解决访问权限问题,详见官网文档:http://zookeeper.apache.org/doc/r3.4.6/zookeeperProgrammers.html#sc_ZooKeeperAccessControl

总体来说,ZK的节点有5种操作权限

CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为crwda(即:每个单词的首字符缩写)

注:这5种权限中,delete是指对子节点的删除权限,其它4种权限指对自身节点的操作权限

 

身份的认证有4种方式

world:默认方式,相当于全世界都能访问
auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
ip:使用Ip地址认证

 

Cli命令行下可以这样测试:

通过getAcl命令可以发现,刚创建的节点,默认是 world,anyone的认证方式,具有cdrwa所有权限

 

继续捣鼓:

先给/test增加了user1:+owfoSBn/am19roBPzR1/MfCblE的只读(r)权限控制,

说明:setAcl /test digest:用户名:密码:权限 给节点设置ACL访问权限时,密码必须是加密后的内容,这里的+owfoSBn/am19roBPzR1/MfCblE=,对应的原文是12345 (至于这个密文怎么得来的,后面会讲到,这里先不管这个),设置完Acl后,可以通过

getAcl /节点路径 查看Acl设置

然后get /test时,提示认证无效,说明访问控制起作用了,接下来:

addauth digest user1:12345 给"上下文"增加了一个认证用户,即对应刚才setAcl的设置

然后再 get /test 就能取到数据了

最后 delete /test 成功了!原因是:根节点/默认是world:anyone:crdwa(即:全世界都能随便折腾),所以也就是说任何人,都能对根节点/进行读、写、创建子节点、管理acl、以及删除子节点(再次映证了ACL中的delete权限应该理解为对子节点的delete权限)

 

刚才也提到了,setAcl /path digest这种方式,必须输入密码加密后的值,这在cli控制台上很不方便,所以下面这种方式更常用:

注意加框的部分,先用addauth digest user1:12345 增加一个认证用户,然后用 setAcl /test auth:user1:12345:r 设置权限,跟刚才的效果一样,但是密码这里输入的是明文,控制台模式下手动输入更方便。

 

好了,揭开加密规则:

1
2
3
4
5
6
7
static public String generateDigest(String idPassword)
        throws NoSuchAlgorithmException {
    String parts[] = idPassword.split(":"2);
    byte digest[] = MessageDigest.getInstance("SHA1").digest(
            idPassword.getBytes());
    return parts[0] + ":" + base64Encode(digest);
}

就是SHA1加密,然后base64编码  

 

代码使用:

zookeeper有一个很好用的客户端开源项目zkclient,官网地址为:http://github.com/zkclient ,其最新片0.7-dev已经支持ACL了(旧0.1版无此功能,所以推荐使用最新版),使用方法:

git clone https://github.com/sgroschupf/zkclient (把代码拉到本地)

修改

build.gradle 找到92行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
uploadArchives {
 
    repositories.mavenDeployer {
        //repository(url: "file:///tmp/mavenRepo")
        repository(url: "http://172.21.129.56:8081/nexus/content/repositories/thirdparty/") {
            authentication(userName: admin, password: admin123)
        }
        beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
        pom.project {
            name 'ZkClient'
            packaging 'jar'
            description 'A zookeeper client, that makes life a little easier.'
            url 'https://github.com/sgroschupf/zkclient'
            licenses {
                license {
                    name 'The Apache Software License, Version 2.0'
                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    distribution 'repo'
                }
            }
            scm {
                url 'https://github.com/sgroschupf/zkclient'
                connection 'scm:git:git://github.com/sgroschupf/zkclient.git'
                developerConnection 'scm:git:https://github.com/sgroschupf/zkclient.git'
            }
            developers {
                developer {
                    id 'sgroschupf'
                    name 'Stefan Groshupf'
                }
                developer {
                    id 'pvoss'
                    name 'Peter Voss'
                }
                developer {
                    id 'jzillmann'
                    name 'Johannes Zillmann'
                }
            }
        }
    }
}

把这一段干掉,否则编译时会出错

然后(windows环境,把./gradew 换成gradlew)

./gradlew test (测试)

./gradlew jars (编译生成jar包)

./gradlew install (安装到本机maven仓库)  

 

新建一个maven项目,pom.xml参考下面设置:

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5     <modelVersion>4.0.0</modelVersion>
 6 
 7     <groupId>yjmyzz</groupId>
 8     <artifactId>zkclient-demo</artifactId>
 9     <version>1.0</version>
10 
11     <dependencies>
12         <dependency>
13             <groupId>org.apache.zookeeper</groupId>
14             <artifactId>zookeeper</artifactId>
15             <version>3.4.6</version>
16         </dependency>
17 
18         <dependency>
19             <groupId>com.101te</groupId>
20             <artifactId>zkclient</artifactId>
21             <version>0.7</version>
22             <classifier>dev</classifier>
23         </dependency>
24 
25         <dependency>
26             <groupId>log4j</groupId>
27             <artifactId>log4j</artifactId>
28             <version>1.2.17</version>
29         </dependency>
30 
31     </dependencies>
32 
33 
34 </project>
复制代码

 

然后写一段代码测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package yjmyzz.zk;
 
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
 
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
 
 
public class Main {
 
    private static final String zkAddress = "localhost:2181";
    private static final String testNode = "/test";
    private static final String readAuth = "read-user:123456";
    private static final String writeAuth = "write-user:123456";
    private static final String deleteAuth = "delete-user:123456";
    private static final String allAuth = "super-user:123456";
    private static final String adminAuth = "admin-user:123456";
    private static final String digest = "digest";
 
    private static void initNode() throws NoSuchAlgorithmException {
        ZkClient zkClient = new ZkClient(zkAddress);
        zkClient.addAuthInfo(digest, allAuth.getBytes());
 
        if (zkClient.exists(testNode)) {
            zkClient.delete(testNode);
            System.out.println("节点删除成功!");
        }
 
        List<ACL> acls = new ArrayList<ACL>();
        acls.add(new ACL(ZooDefs.Perms.ALL, new Id(digest, DigestAuthenticationProvider.generateDigest(allAuth))));
        acls.add(new ACL(ZooDefs.Perms.READ, new Id(digest, DigestAuthenticationProvider.generateDigest(readAuth))));
        acls.add(new ACL(ZooDefs.Perms.WRITE, new Id(digest, DigestAuthenticationProvider.generateDigest(writeAuth))));
        acls.add(new ACL(ZooDefs.Perms.DELETE, new Id(digest, DigestAuthenticationProvider.generateDigest(deleteAuth))));
        acls.add(new ACL(ZooDefs.Perms.ADMIN, new Id(digest, DigestAuthenticationProvider.generateDigest(adminAuth))));
        zkClient.createPersistent(testNode, "test-data", acls);
 
        System.out.println(zkClient.readData(testNode));
        System.out.println("节点创建成功!");
        zkClient.close();
    }
 
    private static void readTest() {
        ZkClient zkClient = new ZkClient(zkAddress);
 
        try {
            System.out.println(zkClient.readData(testNode));//没有认证信息,读取会出错
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        try {
            zkClient.addAuthInfo(digest, adminAuth.getBytes());
            System.out.println(zkClient.readData(testNode));//admin权限与read权限不匹配,读取也会出错
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        try {
            zkClient.addAuthInfo(digest, readAuth.getBytes());
            System.out.println(zkClient.readData(testNode));//只有read权限的认证信息,才能正常读取
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        zkClient.close();
    }
 
    private static void writeTest() {
        ZkClient zkClient = new ZkClient(zkAddress);
 
        try {
            zkClient.writeData(testNode, "new-data");//没有认证信息,写入会失败
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        try {
            zkClient.addAuthInfo(digest, writeAuth.getBytes());
            zkClient.writeData(testNode, "new-data");//加入认证信息后,写入正常
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        try {
            zkClient.addAuthInfo(digest, readAuth.getBytes());
            System.out.println(zkClient.readData(testNode));//读取新值验证
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
 
        zkClient.close();
    }
 
    private static void deleteTest() {
        ZkClient zkClient = new ZkClient(zkAddress);
        //zkClient.addAuthInfo(digest, deleteAuth.getBytes());
        try {
            //System.out.println(zkClient.readData(testNode));
            zkClient.delete(testNode);
            System.out.println("节点删除成功!");
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
        zkClient.close();
    }
 
//    private static void deleteTest2() throws IOException, InterruptedException, KeeperException {
//        //使用zookeeper原生的API进行删除(注:delete权限指有没有权限删除子节点)
//        ZooKeeper zk = new ZooKeeper(zkAddress, 300000, new DemoWatcher());
//        zk.delete(testNode, -1);
//        System.out.println("节点删除成功");
//        zk.close();
//    }
//
//    static class DemoWatcher implements Watcher {
//        @Override
//        public void process(WatchedEvent event) {
//            System.out.println("----------->");
//            System.out.println("path:" + event.getPath());
//            System.out.println("type:" + event.getType());
//            System.out.println("stat:" + event.getState());
//            System.out.println("<-----------");
//        }
//    }
 
    private static void changeACLTest() {
        ZkClient zkClient = new ZkClient(zkAddress);
        //注:zkClient.setAcl方法查看源码可以发现,调用了readData、setAcl二个方法
        //所以要修改节点的ACL属性,必须同时具备read、admin二种权限
        zkClient.addAuthInfo(digest, adminAuth.getBytes());
        zkClient.addAuthInfo(digest, readAuth.getBytes());
        try {
            List<ACL> acls = new ArrayList<ACL>();
            acls.add(new ACL(ZooDefs.Perms.ALL, new Id(digest, DigestAuthenticationProvider.generateDigest(adminAuth))));
            zkClient.setAcl(testNode, acls);
            Map.Entry<List<ACL>, Stat> aclResult = zkClient.getAcl(testNode);
            System.out.println(aclResult.getKey());
        catch (Exception e) {
            System.err.println(e.getMessage());
        }
        zkClient.close();
    }
 
 
    public static void main(String[] args) throws Exception {
 
        initNode();
 
        System.out.println("---------------------");
 
        readTest();
 
        System.out.println("---------------------");
 
        writeTest();
 
        System.out.println("---------------------");
 
        changeACLTest();
 
        System.out.println("---------------------");
 
        deleteTest();
 
        //deleteTest2();
 
    }
}

 

输出结果:

复制代码
test-data
节点创建成功!
---------------------
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /test
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /test
test-data
---------------------
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /test
new-data
---------------------
[31,s{'digest,'admin-user:mAlW21Phn07yOvWnKJYq2sCMoZw=}
]
---------------------
节点删除成功!
复制代码

从zkclient的使用结果看,与cli操作效果一样。  

 

最后:关于多级节点之间的ACL,并非继承关系,但是也有些一联系,这是初次接触ACL中比较难理解的地方:

从这张图上可以发现,子节点/a/b的控制权限范围(全世界都能做任何事)可以超出父节点的范围(仅限:user-a:pwd:a具有read/admin权限)

继续,看上面的这4条红线标注的地方,从上向下一个个解释:

红线1:因为/a只有user-a:pwd-a有ra权限,即:没用户具有c(create)权限,所以不能创建子节点

红线2:因为/a/b为world:anyone:cdrwa权限,即无限制,所以在/a/b下创建子节点b1,地球人已经无法阻止,创建成功

红线3:给/a/b/b1指定了user-b1:pwd-b1的da权限(即:delete+admin)

(注:重温下前面提到的setAcl 二种模式,

一种是setAcl /path digest:username:encrypedpwd:crwda 用这种方式时,encrypedpwd用户必须是密文

另一种方式是先addauth digest:usrname:password 先把授权信息加入上下文,这里password用的是明文,然后再setAcl /path auth:username:password:crdwa

所以如果在cli控制台测试,强烈建议用第二种方式,否则象上图中的方式用错了方式,pwd-b1在zk中被认为是密文,要解密出来几乎不可能,所以设置后,相当于这个节点就废了,因为你不知道密码,要操作该节点时,提供不了正确的认证信息)

红线4:还是刚才的理由,因为/a/b为world:anyone:cdrwa,没有限制,所以删除其下的子节点不受阻挡。

从上图可以看出,无法get父节点的内容,但是可以get子节点的内容,再次说明父、子节点的权限没直接关系,但是做delete时,上面的例子却遇到了麻烦:

想删除/a/b时,由于父节点/a的ACL列表里,只有ra权限,没有d权限,所以无法删除子节点。想删除/a时,发现下面还有子节点b,节点非空无法删除,所以这个示例就无解了(因为根据前面的操作,密码也还原不出来,也就无法修改ACL属性),而根节点/也无法删除,解决办法,只能到data目录里清空所有数据,再重启zk,但是这样就相当于所有数据全扔了,所以在设计ACL时,对于delete权限,要谨慎规划,在测试zk集群上做好测试,再转到生产环境操作。

 

最后给一些权限组合的测试结果:

要修改某个节点的ACL属性,必须具有read、admin二种权限

要删除某个节点下的子节点,必须具有对父节点的read权限,以及父节点的delete权限

摘自:http://www.cnblogs.com/yjmyzz/p/zookeeper-acl-demo.html

参考文章:
https://ihong5.wordpress.com/2014/07/10/apache-zookeeper-acl-access-control-list-getting-permission-sets/

https://ihong5.wordpress.com/2014/07/24/apache-zookeeper-setting-acl-in-zookeeper-client/

https://ihong5.wordpress.com/2014/06/24/znode-types-and-how-to-create-read-delete-and-write-in-zookeeper-via-zkclient/

http://zookeeper.apache.org/doc/r3.4.6/zookeeperProgrammers.html#sc_ZooKeeperAccessControl

原文作者:菩提树下的杨过
原文链接:http://yjmyzz.cnblogs.com 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 

原文地址:https://www.cnblogs.com/liuwenlin/p/10176076.html