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使用jks认证 zookeeper开启授权认证_zookeeper使用jks认证

实现了自定义权限后,如何才能让 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)调用不同的实现类验证权限最后存储权限信息。