Zookeeper的ACL机制来实现客户端对数据节点的访问控制
一个ACL权限设置通常可以分为三部分:权限模式(Scheme)、授权对象(ID)、权限信息(Permission),最终组成一条例如“scheme:id:permission”格式的ACL请求信息
1Scheme
zookeeper的权限验证方式大体分为两种类型,一种是范围验证,一种是口令验证
- 范围验证
这种方式是指设置单个ip或者ip网段,然后赋权,比如:ip:192.168.1.1或192.168.1.1/10 - 口令验证
可以理解为用户名密码方式,这种方式在zookeeper中叫做Digest认证。当然密码不会使用明文,而是部分使用 SHA-1和BASE64算法进行加密 - Super
具有super权限的客户端可以对zookeeper上的任意数据节点进行任意操作,类似于超级管理员的概念
使用示例:
//创建节点
create /digest_node1
//设置digest权限验证
setAcl /digest_node1 digest:用户名:base64格式的密码:rwadc
//查询节点Acl权限
getAcl /digest_node1
//授权操作
addauth digest user:password
- world
这种模式对应于系统中所有用户,设置了world权限模式系统中的所有用户操作都可以不进行权限验证
2授权对象(ID)
这个说的是针对上述的权限模式而言的,采用IP方式,授权对象就是一个IP地址或者IP地址段;采用digest或者super方式,则对应一个用户;如果是world方式,则是授权系统中所有的用户
3权限信息(Permission)
- 数据节点(create)创建权限,授予权限的对象可以在数据节点下创建子节点;
- 数据节点(wirte)更新权限,授予权限的对象可以更新该数据节点;
- 数据节点(read)读取权限,授予权限的对象可以读取该节点的内容以及子节点的信息;
- 数据节点(delete)删除权限,授予权限的对象可以删除该数据节点的子节点;
- 数据节点(admin)管理者权限,授予权限的对象可以对该数据节点体进行 ACL 权限设置。
每个节点都有维护自身的 ACL 权限数据,即使是该节点的子节点也是有自己的 ACL 权限而不是直接继承其父节点的权限
自定义权限验证
zookeeper提供了一种权限扩展机制来让用户实现自己的权限控制方式,官方文档中对这种机制的定义是 “Pluggable ZooKeeper Authenication”,意思是可插拔的授权机制。
首先,要想实现自定义的权限控制机制,最核心的一点是实现 ZooKeeper 提供的权限控制器接口 AuthenticationProvider。下面这张图片展示了接口的内部结构,用户通过该接口实现自定义的权限控制。
实现了自定义权限后,如何才能让 ZooKeeper 服务端使用自定义的权限验证方式呢?接下来就需要将自定义的权限控制注册到 ZooKeeper 服务器中,而注册的方式通常有两种。
第一种是通过设置系统属性来注册自定义的权限控制器:
-Dzookeeper.authProvider.x=CustomAuthenticationProvider
另一种是在配置文件 zoo.cfg 中进行配置:
authProvider.x=CustomAuthenticationProvider
ACL内部实现原理
- 客户端处理过程
我们来看addAuthInfo(...)
/**
* Add the specified scheme:auth information to this connection.
*
* This method is NOT thread safe
*
* @param scheme
* @param auth
*/
public void addAuthInfo(String scheme, byte auth[]) {
cnxn.addAuthInfo(scheme, auth);
}
public void addAuthInfo(String scheme, byte auth[]) {
if (!state.isAlive()) {
return;
}
authInfo.add(new AuthData(scheme, auth));
queuePacket(new RequestHeader(-4, OpCode.auth), null,
new AuthPacket(0, scheme, auth), null, null, null, null,
null, null);
}
首先客户端通过 ClientCnxn 类中的 addAuthInfo 方法向服务端发送 ACL 权限信息变更请求,该方法首先将 scheme 和 auth 封装成 AuthPacket 类,并通过 RequestHeader 方法表示该请求是权限操作请求,最后将这些数据统一封装到 packet 中,并添加到 outgoingQueue 队列中发送给服务端。
ACL 权限控制机制的客户端实现相对简单,只是封装请求类型为权限请求,方便服务器识别处理,而发送到服务器的信息包括我们之前提到的权限校验信息。
- 服务端实现过程
我们从readRequest()开始看
private void readRequest() throws IOException {
zkServer.processPacket(this, incomingBuffer);
}
当节点授权请求发送到服务端后,在服务器的处理中首先调用 readRequest()方法作为服务器处理的入口,其内部只是调用 processPacket 方法。
public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
// We have the request, now process and setup for next
InputStream bais = new ByteBufferInputStream(incomingBuffer);
BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
RequestHeader h = new RequestHeader();
h.deserialize(bia, "header");
// Through the magic of byte buffers, txn will not be
// pointing
// to the start of the txn
incomingBuffer = incomingBuffer.slice();
if (h.getType() == OpCode.auth) {
LOG.info("got auth packet " + cnxn.getRemoteSocketAddress());
/*processPacket 方法的内部,首先反序列化客户端的请求信息并封装到 AuthPacket 对象中*/
AuthPacket authPacket = new AuthPacket();
ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket);
String scheme = authPacket.getScheme();
/*getServerProvider 方法根据不同的 scheme 判断具体的实现类,这里我们使用 Digest 模式为例,因此该实现类是 DigestAuthenticationProvider */
AuthenticationProvider ap = ProviderRegistry.getProvider(scheme);
Code authReturn = KeeperException.Code.AUTHFAILED;
if(ap != null) {
try {
/*调用其 handleAuthentication() 方法进行权限验证*/
authReturn = ap.handleAuthentication(cnxn, authPacket.getAuth());
} catch(RuntimeException e) {
LOG.warn("Caught runtime exception from AuthenticationProvider: " + scheme + " due to " + e);
authReturn = KeeperException.Code.AUTHFAILED;
}
}
/*如果返 KeeperException.Code.OK 则表示该请求已经通过了权限验证*/
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?
}
else {
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
h.getType(), incomingBuffer, cnxn.getAuthInfo());
si.setOwner(ServerCnxn.me);
submitRequest(si);
}
}
cnxn.incrOutstandingRequests(h);
}
重点关注handleAuthentication()
public KeeperException.Code
handleAuthentication(ServerCnxn cnxn, byte[] authData)
{
String id = new String(authData);
try {
String digest = generateDigest(id);
if (digest.equals(superDigest)) {
cnxn.addAuthInfo(new Id("super", ""));
}
cnxn.addAuthInfo(new Id(getScheme(), digest));
return KeeperException.Code.OK;
} catch (NoSuchAlgorithmException e) {
LOG.error("Missing algorithm",e);
}
return KeeperException.Code.AUTHFAILED;
}
这里我们重点讲解一下 addAuthInfo 函数,其作用是将解析到的权限信息存储到 ZooKeeper 服务器的内存中,该信息在整个会话存活期间一直会保存在服务器上,如果会话关闭,该信息则会被删,这个特性很像我们之前学过的数据节点中的临时节点。
再来看PrepRequestProcessor类中的run方法
public void run() {
try {
while (true) {
Request request = submittedRequests.take();
long traceMask = ZooTrace.CLIENT_REQUEST_TRACE_MASK;
if (request.type == OpCode.ping) {
traceMask = ZooTrace.CLIENT_PING_TRACE_MASK;
}
if (LOG.isTraceEnabled()) {
ZooTrace.logRequest(LOG, traceMask, 'P', request, "");
}
if (Request.requestOfDeath == request) {
break;
}
pRequest(request);
}
} catch (RequestProcessorException e) {
if (e.getCause() instanceof XidRolloverException) {
LOG.info(e.getCause().getMessage());
}
handleException(this.getName(), e);
} catch (Exception e) {
handleException(this.getName(), e);
}
LOG.info("PrepRequestProcessor exited loop!");
}
重要的方法pRequest(request);这个方法将被称为ProcessRequestThread,这是一个单例,这样就会有一个线程调用此代码。
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;
try {
switch (request.type) {
case OpCode.create:
CreateRequest createRequest = new CreateRequest();
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:
SetACLRequest setAclRequest = new SetACLRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
break;
case OpCode.check:
CheckVersionRequest checkRequest = new CheckVersionRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, checkRequest, true);
break;
case OpCode.multi:
MultiTransactionRecord multiRequest = new MultiTransactionRecord();
try {
ByteBufferInputStream.byteBuffer2Record(request.request, multiRequest);
} catch(IOException e) {
request.hdr = new TxnHeader(request.sessionId, request.cxid, zks.getNextZxid(),
zks.getTime(), OpCode.multi);
throw e;
}
...省略
}
这里边主要的方法是pRequest2Txn(),在这个方法里重点关注checkACL(zks, parentRecord.acl, ZooDefs.Perms.CREATE, request.authInfo);
/*通过 PrepRequestProcessor 中的 checkAcl 函数检查对应的请求权限*/
static void checkACL(ZooKeeperServer zks, List acl, int perm,
List ids) throws KeeperException.NoAuthException {
if (skipACL) {
return;
}
if (acl == null || acl.size() == 0) {
/*如果该节点没有任何权限设置则直接返回*/
return;
}
for (Id authId : ids) {
/*如果该节点有super权限设置则直接返回*/
if (authId.getScheme().equals("super")) {
return;
}
}
/*如果该节点有权限设置则循环遍历节点信息进行检查*/
for (ACL a : acl) {
Id id = a.getId();
if ((a.getPerms() & perm) != 0) {
if (id.getScheme().equals("world")
&& id.getId().equals("anyone")) {
/*如果具有WORLD的权限则直接返回表明权限认证成功*/
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;
}
}
}
}
}
/*抛出 NoAuthException 异常中断操作表明权限认证失败*/
throw new KeeperException.NoAuthException();
}
到目前为止我们对 ACL 权限在 ZooKeeper 服务器客户端和服务端的底层实现过程进行了深度的分析。总体来说,
- 客户端在 ACL 权限请求发送过程的步骤比较简单:
- 首先是封装该请求的类型
- 之后将权限信息封装到 request 中并发送给服务端。
- 而服务器的实现比较复杂
在授权接口中,值得注意的是会话的授权信息存储在 ZooKeeper 服务端的内存中,如果客户端会话关闭,授权信息会被删除。下次连接服务器后,需要重新调用授权接口进行授权。
- 首先分析请求类型是否是权限相关操作
- 之后根据不同的权限模式(scheme)调用不同的实现类验证权限最后存储权限信息。