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); }
繼續跟進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在底層原始碼做出了哪些動作,以及服務端將我們手動新增進來的許可權資訊都放在記憶體中
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;
- 當前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鏈中往下傳遞