概述
Z K作为一个分布式协调框架、内部存储着一些分布式系统运行时状态的元数据。如何有效的保护这些数据的安全、如何做一个比较好的权限控制显得非常的重要。
ZK 为我们提供一套完善的 ACL(access control list,访问控制列表) 权限控制机制来保障数据的安全。
ACL 介绍
我们可以从三个方面来理解 ACL 机制
- Scheme 权限模式
- Id 授权对象
- Permission 权限
通常使用 scheme:id:permission
来标志一个有效的 ACL 信息、我们先来看看我们默认的数据节点里面的 ACL 数据
getACl / 复制代码
我们也可以看到他也是分为三部分的
world
对应的就是 schemeanyone
对应的就是 idcdrwa
对应的就是 permission
下面我们就分别介绍它们
权限
- create:c 数据节点的创建权限、允许授权对象在该数据节点下创建子节点。
- delete:d 子节点的删除权限、允许授权对象删除该数据节点的子节点
- read:r 数据节点的读取权限、允许授权对象对该数据节点读取数据内容和获取子节点列表信息
- write:w 数据节点的更新权限、允许授权对象对数据节点的数据内容进行更新
- admin:a 数据节点的管理权限、允许授权对象对该数据节点进行 ACL 相关的设置操作
权限模式
如果按分类来说、ZK 中其实只有两种权限模式,一种是基于IP/IP段的,一种是基于账号密码的。
但是可以细分为以下四种
- IP
- digest
- world
- super
IP
IP 模式可以针对数据节点设置 IP 地址或设置 IP 网段的方式进行配置。
[zk: localhost:2181(CONNECTED) 44] create /acl_ip data ip:127.0.0.1:cdrwa Created /acl_ip 复制代码
我们创建了数据节点 acl_ip
并且为这个节点设置了 ACL ,使用的是 IP 这种模式、授权对象就是 127.0.0.1
这个 ip,而权限则是五种权限全部都赋予了。我们在另外本机电脑的另一个 zkClient 中访问该数据节点
[zk: 127.0.0.1:2181(CONNECTED) 21] get /acl_ip data cZxid = 0x520 ctime = Sat May 16 13:04:40 CST 2020 mZxid = 0x520 mtime = Sat May 16 13:04:40 CST 2020 pZxid = 0x520 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 4 numChildren = 0 [zk: 127.0.0.1:2181(CONNECTED) 22] getAcl /acl_ip 'ip,'127.0.0.1 : cdrwa 复制代码
digest
就是我们常见的账号密码模式
username:password 复制代码
但是对于我们在命令行中、我们输入的并不是一个原始的密码、而是需要我们对 username:password
进行加密和编码之后的值。
[zk: localhost:2181(CONNECTED) 46] create /acl_digest data digest:foo:Jfg7TYUBs/6KEtdDWd5OB6bdD2Q=:wrcda Created /acl_digest [zk: localhost:2181(CONNECTED) 47] getAcl /acl_digest 'digest,'foo:Jfg7TYUBs/6KEtdDWd5OB6bdD2Q= : cdrwa 复制代码
原始的数据是: username 为 foo, password 为 true,但是在命令行中输入的 password 已然不是我们原本的 true 了
原因也很简单、安全嘛、那它的加密以及编码的逻辑是啥?
在源代码中 org.apache.zookeeper.server.auth.DigestAuthenticationProvider#generateDigest
public static String generateDigest(String idPassword) throws NoSuchAlgorithmException { String[] parts = idPassword.split(":", 2); byte[] digest = MessageDigest.getInstance("SHA1").digest(idPassword.getBytes()); return parts[0] + ":" + base64Encode(digest); } 复制代码
可以看到其先对 username:password
进行SHA1 的加密、然后再进行 base64 的编码,最后得出来的就是我们返回的就是我们在命令行中输入的 foo:Jfg7TYUBs/6KEtdDWd5OB6bdD2Q=
这个字符串。
String idPassword = "foo:true"; System.out.println(generateDigest(idPassword)); // 打印结果为 foo:Jfg7TYUBs/6KEtdDWd5OB6bdD2Q= 复制代码
我们现在在另一个 zkClient 中增加 digest 信息然后访问这个数据节点
[zk: localhost:2181(CONNECTED) 9] addauth digest foo:true [zk: localhost:2181(CONNECTED) 10] get /acl_digest data cZxid = 0x521 ctime = Sat May 16 13:41:55 CST 2020 mZxid = 0x521 mtime = Sat May 16 13:41:55 CST 2020 pZxid = 0x521 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 4 numChildren = 0 复制代码
world
world 是一种最开放的权限控制模式,事实上这种权限控制几乎没有任何的作用、数据节点的访问权限对所有用户开放,我们默认的就是这种权限模式。这种模式其实就是一种特殊的 digest 模式,只不过它的 id 只有一个 anyone
super
super 就是超级用户的意思、也是一种特殊的 digest。 在这个模式下、超级用户可以对任意的数据节点进行任意的操作。
授权对象
- IP 授权模式下、授权对象就是 ip
- digest 授权模式下、授权对象就是
username:base64(sha1(username:password))
- world 授权模式下、只有一个授权对象
anyone
- super 授权模式下、跟 digest 授权模式一样
super 授权模式介绍
如何配置一个超级管理员的授权对象呢?
假如我们配置的账号密码为 foo:foo
我们可以在启动 zkServer 的时候加入如下的系统属性
-Dzookeeper.DigestAuthenticationProvider.superDigest=foo:qllW6iET90npPATKMTxiFSiQ5Ns= 复制代码
可以在启动 zkServer 的时候在 idea 的配置中加上这个参数
然后启动 server 则可
如果我们使用的是已经是官方编译好的zk、则可以在 bin 目录下修改 zkServer.sh
脚本的内容
nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.DigestAuthenticationProvider.superDigest=foo:qllW6iET90npPATKMTxiFSiQ5Ns=" \ 复制代码
加上我们的系统变量、然后启动则可
foo:foo 为 super 授权对象 的 username 和 password
/acl_super 节点的 username 和 password 都是 super 这个字符串
// super:super [zk: localhost:2181(CONNECTED) 2] create /acl_super data digest:super:gG7s8t3oDEtIqF6DM9LlI/R+9Ss=:wrdca Created /acl_super [zk: localhost:2181(CONNECTED) 3] getAcl /acl_super 'digest,'super:gG7s8t3oDEtIqF6DM9LlI/R+9Ss= : cdrwa [zk: localhost:2181(CONNECTED) 4] get /acl_super Authentication is not valid : /acl_super [zk: localhost:2181(CONNECTED) 5] addauth digest foo:foo [zk: localhost:2181(CONNECTED) 6] get /acl_super data cZxid = 0x52b ctime = Sat May 16 15:18:18 CST 2020 mZxid = 0x52b mtime = Sat May 16 15:18:18 CST 2020 pZxid = 0x52b cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 4 numChildren = 0 复制代码
super 授权模式验证部分源码
private static final String superDigest = System.getProperty("zookeeper.DigestAuthenticationProvider.superDigest"); 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; } 复制代码
我们看到当我们的 digest 等于 superDigest 的时候、就会向 ServerCnxn 中增加多一个 Id 对象
private Set<Id> authInfo = Collections.newSetFromMap(new ConcurrentHashMap<Id, Boolean>()); 复制代码
而在我们访问节点的时候、触发 checkACL
org.apache.zookeeper.server.ZooKeeperServer#checkACL
public void checkACL(ServerCnxn cnxn, List<ACL> acl, int perm, List<Id> ids, String path, List<ACL> setAcls) throws KeeperException.NoAuthException { // acl 为空 if (acl == null || acl.size() == 0) { return; } // super 授权模式 for (Id authId : ids) { if (authId.getScheme().equals("super")) { return; } } for (ACL a : acl) { Id id = a.getId(); if ((a.getPerms() & perm) != 0) { // world 授权模式 if (id.getScheme().equals("world") && id.getId().equals("anyone")) { return; } .... .... } } // 抛出异常 throw new KeeperException.NoAuthException(); }