zookeeper 密码_zookeeper:ACL权限控制如何避免未经授权的访问?

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。下面这张图片展示了接口的内部结构,用户通过该接口实现自定义的权限控制。

49820f62786338d628c9eb2b266d0f1e.png

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值