ZooKeeper的ACL實現源碼閱讀
- 2019 年 10 月 3 日
- 筆記
如果對您有幫助,歡迎點贊支援, 如果有不對的地方,歡迎指出批評
什麼是ACL(Access Control List)
zookeeper在分散式系統中承擔中間件的作用,它管理的每一個節點上可能都存儲這重要的資訊,因為應用可以讀取到任意節點,這就可能造成安全問題,ACL的作用就是幫助zookeeper實現許可權控制, 比如對節點的增刪改查
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); }
繼續跟進ClientCnxn
的addAuthInfo()
方法,源碼如下 它主要做了兩件事:
- 將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.java
的readRequest()
, 源碼如下:
// 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在底層源碼做出了哪些動作,以及服務端將我們手動添加進來的許可權資訊都放在記憶體中
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的類圖
言歸正傳,來到這個函數算是進入了第二個高潮, 他主要做了這幾件事
- 檢查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;
- 當前node已經存在的 需要的許可權資訊
- 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鏈中往下傳遞
如果對您有幫助,歡迎點贊支援, 如果有不對的地方,歡迎指出批評