文章出處

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 設置權限,跟剛才的效果一樣,但是密碼這里輸入的是明文,控制臺模式下手動輸入更方便。

 

好了,揭開加密規則:

    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行

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>
View Code

 

然后寫一段代碼測試一下:

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權限

 

參考文章:
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

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()