1. 程式人生 > >ZooKeeper的ACL實現原始碼閱讀

ZooKeeper的ACL實現原始碼閱讀

什麼是ACL(Access Control List)

zookeeper在分散式系統中承擔中介軟體的作用,它管理的每一個節點上可能都儲存這重要的資訊,因為應用可以讀取到任意節點,這就可能造成安全問題,ACL的作用就是幫助zookeeper實現許可權控制, 比如對節點的增刪改查

點選查上篇部落格中客戶端使用acl的詳解

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);
    }

繼續跟進ClientCnxnaddAuthInfo()方法,原始碼如下 它主要做了兩件事:

  • 將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.javareadRequest(), 原始碼如下:

// 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在底層原始碼做出了哪些動作,以及服務端將我們手動新增進來的許可權資訊都放在記憶體中


getACL原始碼追蹤入口

同樣會和addAuth操作一樣,主執行緒從控制檯解析出使用者的請求封裝進request然後封裝進pakcet傳送給服務端

getACL服務端的處理邏輯

請求來到服務端,在遇到第一次checkAcl之間,請求會順利的來到第一個處理器PrepRequestProcessor, 所以我們的入口點就是這裡

    protected void pRequest(Request request) throws RequestProcessorException {
        // LOG.info("Prep>>> cxid = " + request.cxid + " type = " +
        // request.type + " id = 0x" + Long.toHexString(request.sessionId));
        request.hdr = null;
        request.txn = null;
    // todo 下面的不同型別的資訊, 對應這不同的處理器方式
    try {
        switch (request.type) {
            case OpCode.create:
                // todo 建立每條記錄對應的bean , 現在還是空的, 在面的pRequest2Txn 完成賦值
            CreateRequest createRequest = new CreateRequest();
            // todo 跟進這個方法, 再從這個方法出來,往下執行,可以看到呼叫了下一個處理器
            pRequest2Txn(request.type, zks.getNextZxid(), request, createRequest, true);
            break;
        case OpCode.delete:
            DeleteRequest deleteRequest = new DeleteRequest();               
            pRequest2Txn(request.type, zks.getNextZxid(), request, deleteRequest, true);
            break;
        case OpCode.setData:
            SetDataRequest setDataRequest = new SetDataRequest();                
            pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
            break;
        case OpCode.setACL:
            // todo 客戶端傳送的setAcl命令, 會流經這個選項
            SetACLRequest setAclRequest = new SetACLRequest();
            /**  SetACLRequest的屬性
             *   private String path;
             *   private java.util.List<org.apache.zookeeper.data.ACL> acl;
             *   private int version;
             */
            // todo 繼續跟進去
            pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
            break;
        case OpCode.check:

使用者在控制檯輸入類似 setAcl /node4 digest:zhangsan:jA/7JI9gsuLp0ZQn5J5dcnDQkHA= 請求將被解析執行到上面的case OpCode.setACL: 它new了一個空的物件SetACLRequest,這個物件一會在pRequest2Txn()函式中進行初始化

繼續跟進pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
原始碼如下: 它的解析我寫在這段程式碼的下面

    protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize)
        throws KeeperException, IOException, RequestProcessorException
    {
        // todo 使用request的相關屬性,創建出 事務Header
        request.hdr = new TxnHeader(request.sessionId, request.cxid, zxid,
                                    Time.currentWallTime(), type);

        switch (type) {
            case OpCode.create:
                // todo 校驗session的情況
                zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
                CreateRequest createRequest = (CreateRequest)record;  
                .
                .
                .
            case OpCode.setACL:
                // todo 檢查session的合法性
                zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
                // todo record; 上一步中new 出來的SetACLRequest空物件,
                // todo 這樣設計的好處就是, 可以進行橫向的擴充套件, 讓當前這個方法 PRequest2Tm()中可以被Record的不同實現類複用
                SetACLRequest setAclRequest = (SetACLRequest)record;
               // todo 將結果反序列化進 setAclRequest
                if(deserialize)
                    ByteBufferInputStream.byteBuffer2Record(request.request, setAclRequest);

                // todo 獲取path 並校驗
                path = setAclRequest.getPath();
                validatePath(path, request.sessionId);

                // todo 去除重複的acl
                listACL = removeDuplicates(setAclRequest.getAcl());
                if (!fixupACL(request.authInfo, listACL)) {
                    // todo request.authInfo的預設值就是本地ip, 如果沒有這個值的話,在server本地,client都連線不上
                    throw new KeeperException.InvalidACLException(path);
                }
                //todo  獲取當前節點的record
                nodeRecord = getRecordForPath(path);
                // todo 共用的checkACL 方法
                // todo  在setAcl時,使用checkACL進行許可權的驗證
                // todo  nodeRecord.acl 當前節點的acl
                // todo 跟進這個方法
                checkACL(zks, nodeRecord.acl, ZooDefs.Perms.ADMIN,
                        request.authInfo);
                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;
            // todo     createSession/////////////////////////////////////////////////////////////////
            case OpCode.createSession:
            .
            .
            .
  • 先說一下有個亮點, 就是這個函式中倒數第二個引數位置寫著需要的引數是record型別的,但是實際上我們傳遞進來的型別是SetACLRequest上面的這個空物件SetACLRequest這樣的設計使得的擴充套件性變得超級強

這是record的類圖

言歸正傳,來到這個函式算是進入了第二個高潮, 他主要做了這幾件事

  • 檢查session是否合法
  • 將資料反序列化進 SetACLRequest
  • 校驗path是否合法
  • 去除重複的acl
  • CheckAcl鑑權

我們重點看最後兩個地方

去除重複的acl

fixupACL(request.authInfo, listACL)

這個函式很有趣,舉個例子,通過控制檯,我們連線上一個服務端,然後通過如下命令往服務端的authInfo集合中新增三條資料

addauth digest lisi1:1 
addauth digest lisi2:2
addauth digest lisi3:3

然後給lisi授予針對node1的許可權

setAcl /node auth:lisi1:123123:adr

在此檢視,會發現lisi2 lisi3同樣有了對node1的許可權

CheckAcl鑑權

checkACL(zks, nodeRecord.acl, ZooDefs.Perms.ADMIN,request.authInfo); 原始碼如下:

這個函式的主要邏輯就是,從頭到尾的執行,只要滿足了合法的許可權就退出,否則執行到最後都沒有合法的許可權,就丟擲沒有授權的異常從而中斷請求,如果正常返回了,說明許可權經過了驗證,既然經過了驗證request就可以繼續在process鏈上執行,進一步進行處理

  static void checkACL(ZooKeeperServer zks, List<ACL> acl, int perm,
            List<Id> ids) throws KeeperException.NoAuthException {
        // todo 這是個寫在配置檔案中的 配置屬性 zookeeper.skipACL , 可以關閉acl驗證
        if (skipACL) {
            return;
        }
        // todo 當前的節點沒有任何驗證的規則的話,直接通過
        if (acl == null || acl.size() == 0) {
            return;
        }
        // todo 如果ids中存放著spuer 超級使用者,也直接通過
        for (Id authId : ids) {
            if (authId.getScheme().equals("super")) {
                return;
            }
        }
        // todo 迴圈當前節點上存在的acl點
        for (ACL a : acl) {
            Id id = a.getId();
            // todo 使用& 位運算  , 去ZooDefs類看看位移的情況
            // todo  如果設定的許可權為 a.getPerms() =dra = d+r+a = 8+1+16 = 25
            // todo   perm = 16
            /**
             *  進行&操作
             *  25 & 16
             *  11001
             *  10000
             *   結果
             *  10000
             *  結果不是0 ,進入if { }
             */
            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;
                        }
                    }
                }
            }
        }
        //todo  到最後也沒返回回去, 就丟擲異常
        throw new KeeperException.NoAuthException();
    }

幾個重要的引數

  • acl
    • 當前node已經存在的 需要的許可權資訊scheme:id;
  • perm
    • 當前使用者的操作需要的許可權
  • ids
    • 我們在上面通過addauth新增進authInfo列表中的資訊
  • skip跳過許可權驗證
  static boolean skipACL;
    static {
        skipACL = System.getProperty("zookeeper.skipACL", "no").equals("yes");
        if (skipACL) {
            LOG.info("zookeeper.skipACL==\"yes\", ACL checks will be skipped");
        }
    }

這裡面在驗證許可權時存在位運算,prem在ZooDFS.java中維護

// todo 位移的操作
@InterfaceAudience.Public
public interface Perms {
    // 左移
    int READ = 1 << 0;   //1      2的0次方

    int WRITE = 1 << 1;    //2    2的1次方

    int CREATE = 1 << 2;   // 4

    int DELETE = 1 << 3;  // 8

    int ADMIN = 1 << 4;  // 16

    int ALL = READ | WRITE | CREATE | DELETE | ADMIN;  //31

    /**
     *      00001
     *      00010
     *      00100
     *      01000
     *      10000
     *
     *      結果11111 = 31
     *
     */

}

總結:

通過跟蹤上面的原始碼,我們知道了zookeeper的許可權acl是如何實現的,以及客戶端和服務端之間是如何相互配合的

  • 客戶端同樣是經過主執行緒跟進不同的命令型別,將請求打包packet傳送到服務端
  • 服務端將addauth新增認證資訊儲存在記憶體中
  • node會被持久化,因為它需要的認證同樣被持久化
  • 在進行處理request之前,會進行checkAcl的操作,它是在第一個處理器中完成的,只有經過許可權認證,request才能繼續在processor鏈中往下傳遞