深入理解 ZooKeeper的ACL实现(一)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 深入理解 ZooKeeper的ACL实现(一)

2020-02-08 补充流程图



如果对您有帮助,欢迎点赞支持, 如果有不对的地方,欢迎指出批评


什么是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);
    }


继续跟进ClientCnxnaddAuthInfo()方法,源码如下 它主要做了两件事:

  • 将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.javareadRequest(), 源码如下:


// 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在底层源码做出了哪些动作,以及服务端将我们手动添加进来的权限信息都放在内存中


相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
6月前
|
存储 Shell 数据安全/隐私保护
ZooKeeper【基础知识 04】控制权限ACL(原生的 Shell 命令)
【4月更文挑战第11天】ZooKeeper【基础知识 04】控制权限ACL(原生的 Shell 命令)
100 7
|
6月前
|
存储 Shell 数据安全/隐私保护
ZooKeeper【基础 04】控制权限ACL(原生的 Shell 命令)
ZooKeeper【基础 04】控制权限ACL(原生的 Shell 命令)
160 0
|
算法 Shell Apache
Apache ZooKeeper - ZK的ACL权限控制( Access Control List )
Apache ZooKeeper - ZK的ACL权限控制( Access Control List )
507 0
|
Shell Linux 数据安全/隐私保护
Zookeeper系列(三)——Zookeeper的ACL权限控制
Zookeeper系列(三)——Zookeeper的ACL权限控制
665 1
Zookeeper系列(三)——Zookeeper的ACL权限控制
深入理解 ZooKeeper的ACL实现(二)
深入理解 ZooKeeper的ACL实现(二)
218 0
|
数据安全/隐私保护 安全 Linux
|
测试技术 Apache 开发工具
ZooKeeper 笔记(5) ACL(Access Control List)访问控制列表
zk做为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK通过ACL机制来解决访问权限问题,详见官网文档:http://zookeeper.
1003 0
|
2月前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2