ZooKeeper的ACL實現源碼閱讀

  • 2019 年 10 月 3 日
  • 筆記

如果對您有幫助,歡迎點贊支援, 如果有不對的地方,歡迎指出批評

什麼是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

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


setACL源碼追蹤入口

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

setACL服務端的處理邏輯

請求來到服務端,在遇到第一次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的類圖

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鏈中往下傳遞

如果對您有幫助,歡迎點贊支援, 如果有不對的地方,歡迎指出批評