Zookeeper源码阅读 ACL基础

Posted gongcomeon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zookeeper源码阅读 ACL基础相关的知识,希望对你有一定的参考价值。

前言

之前看代码的时候也同步看了看一些关于zk源码的博客,有一两篇讲到了ZK里ACL的基础的结构,我自己这边也看了看相关的代码,在这里分享一下!

ACL和ID

ACL和ID都是有Jute生成的实体类,分别代表了ZK里ACL和不同ACL模式下的具体实体。

ACL:

public class ACL implements Record {
  private int perms;
  private org.apache.zookeeper.data.Id id;

可以看到,ACL包含了两个域,分别代表了权限值(perms)和ACL的验证实体(id)。

ID:

public class Id implements Record {
  private String scheme;
  private String id;
  public Id() {
  }
  public Id( //参数构造器
        String scheme,
        String id) {
    this.scheme=scheme;
    this.id=id;
  }

在ID类中,主要有两个域,scheme即是验证模式,而id则代表在scheme的验证模式下的具体内容。

perms

关于perms,ZK中的权限类型和linux系统中的文件权限很类似,大致上都会有读、写、操作的权限,而且都是通过位操作来控制,非常的优雅。但是有两点需要强调:

  1. zk的权限系统中没有所谓user,group,others的概念,也就是说没有一个用户或者说用户组是一个znode的拥有者,但是可以给一群用户或一个用户赋予某个znode admin的权限。
  2. 对某个znode设置的ACL只对它自己有效,对它的子节点无效的。
@InterfaceAudience.Public
public interface Perms {
    int READ = 1 << 0;//getdata/getchildren是否可以

    int WRITE = 1 << 1;//zookeeper中write是表示是否可以setdata

    int CREATE = 1 << 2;//创建子节点

    int DELETE = 1 << 3;//删除子节点

    int ADMIN = 1 << 4;//可以通过setacl设置权限

    int ALL = READ | WRITE | CREATE | DELETE | ADMIN;
}

总结:可以看到通过位操作为不同的权限赋予不同的值,类似linux中权限值的概念。

ID

@InterfaceAudience.Public
    public interface Ids {
        /**
         * This Id represents anyone.
         */
         //默认的为world模式设置的ID
        public final Id ANYONE_ID_UNSAFE = new Id("world", "anyone");

        /**
         * This Id is only usable to set ACLs. It will get substituted with the
         * Id's the client authenticated with.
         */
         //默认的为auth模式设置的ID,代表了任何已经被确认的用户,在setacl的时候使用
        public final Id AUTH_IDS = new Id("auth", "");

        /**
         * This is a completely open ACL .
         */
         //默认的open ACL LIst,表示所有用户(ANYONE_ID_UNSAFE)有所有权限
        public final ArrayList<ACL> OPEN_ACL_UNSAFE = new ArrayList<ACL>(
                Collections.singletonList(new ACL(Perms.ALL, ANYONE_ID_UNSAFE)));

        /**
         * This ACL gives the creators authentication id's all permissions.
         */
         //默认的creator ACL LIst,表示auth_ids这些账号有所有权限
        public final ArrayList<ACL> CREATOR_ALL_ACL = new ArrayList<ACL>(
                Collections.singletonList(new ACL(Perms.ALL, AUTH_IDS)));

        /**
         * This ACL gives the world the ability to read.
         */
         //默认的read ACL LIst,表示所有账号(ANYONE_ID_UNSAFE)有read权限
        public final ArrayList<ACL> READ_ACL_UNSAFE = new ArrayList<ACL>(
                Collections
                        .singletonList(new ACL(Perms.READ, ANYONE_ID_UNSAFE)));
    }

总结:可以看到,zk内部在ZooDefs提供了两个默认的ID实例,分别是world和auth两种schema的实现。同时在DigestAuthenticationProvider中实现了super schema的访问。

/** 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)
 */
//superDigest 的值是super:<base64encoded(SHA1(password))>,可以在zk启动时配置zookeeper.DigestAuthenticationProvider.superDigest的值
private final static String superDigest = System.getProperty(
    "zookeeper.DigestAuthenticationProvider.superDigest");
public KeeperException.Code 
    handleAuthentication(ServerCnxn cnxn, byte[] authData)
{
    String id = new String(authData);//把验证信息转换为string
    try {
        String digest = generateDigest(id);//加密
        if (digest.equals(superDigest)) {//判断是否是super
            cnxn.addAuthInfo(new Id("super", ""));//加入一个super的ID
        }
        cnxn.addAuthInfo(new Id(getScheme(), digest));//加入digest schema下的id为digest变量值的ID
        return KeeperException.Code.OK;
    } catch (NoSuchAlgorithmException e) {
        LOG.error("Missing algorithm",e);
    }
    return KeeperException.Code.AUTHFAILED;
}

schema

上面说到了zk内部的world,auth和super三种schema的实现,实际上,zk还有三种不提供固定id的schema。这三种schema的实现类都实现了AuthenticationProvider接口。

/**
 * This interface is implemented by authentication providers to add new kinds of
 * authentication schemes to ZooKeeper.
 */
public interface AuthenticationProvider {
    /**
     * The String used to represent this provider. This will correspond to the
     * scheme field of an Id.
     * 
     * @return the scheme of this provider.
     */
     //获取当前的schema
    String getScheme();

    /**
     * This method is called when a client passes authentication data for this
     * scheme. The authData is directly from the authentication packet. The
     * implementor may attach new ids to the authInfo field of cnxn or may use
     * cnxn to send packets back to the client.
     * 
     * @param cnxn
     *                the cnxn that received the authentication information.
     * @param authData
     *                the authentication data received.
     * @return TODO
     */
    //客户端发送的验证信息处理
    KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);

    /**
     * This method is called to see if the given id matches the given id
     * expression in the ACL. This allows schemes to use application specific
     * wild cards.
     * 
     * @param id
     *                the id to check.
     * @param aclExpr
     *                the expression to match ids against.
     * @return true if the id can be matched by the expression.
     */
    //校验id是否符合表达式
    boolean matches(String id, String aclExpr);

    /**
     * This method is used to check if the authentication done by this provider
     * should be used to identify the creator of a node. Some ids such as hosts
     * and ip addresses are rather transient and in general don't really
     * identify a client even though sometimes they do.
     * 
     * @return true if this provider identifies creators.
     */
    //判断这种schema的provider是否能定位creator
    boolean isAuthenticated();

    /**
     * Validates the syntax of an id.
     * 
     * @param id
     *                the id to validate.
     * @return true if id is well formed.
     *
     //检验语法
    boolean isValid(String id);
}

这里以DigestAuthenticationProvider做例子,SASLAuthenticationProvider(不清楚具体作用,在后面的ProviderRegistry也没有注册)和IPAuthenticationProvider是差不多的,可以类比着看看:

public class DigestAuthenticationProvider implements AuthenticationProvider {
    private static final Logger LOG =
        LoggerFactory.getLogger(DigestAuthenticationProvider.class);

    /** 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)
     */
    //去读zookeeper.DigestAuthenticationProvider.superDigest配置
    private final static String superDigest = System.getProperty(
        "zookeeper.DigestAuthenticationProvider.superDigest");

    //当前是digest schema
    public String getScheme() {
        return "digest";
    }

    //base64加密
    static final private String base64Encode(byte b[]) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < b.length;) {
            int pad = 0;
            int v = (b[i++] & 0xff) << 16;
            if (i < b.length) {
                v |= (b[i++] & 0xff) << 8;
            } else {
                pad++;
            }
            if (i < b.length) {
                v |= (b[i++] & 0xff);
            } else {
                pad++;
            }
            sb.append(encode(v >> 18));
            sb.append(encode(v >> 12));
            if (pad < 2) {
                sb.append(encode(v >> 6));
            } else {
                sb.append('=');
            }
            if (pad < 1) {
                sb.append(encode(v));
            } else {
                sb.append('=');
            }
        }
        return sb.toString();
    }

    static final private char encode(int i) {
        i &= 0x3f;
        if (i < 26) {
            return (char) ('A' + i);
        }
        if (i < 52) {
            return (char) ('a' + i - 26);
        }
        if (i < 62) {
            return (char) ('0' + i - 52);
        }
        return i == 62 ? '+' : '/';
    }

    //生成加密的id
    static public String generateDigest(String idPassword)
            throws NoSuchAlgorithmException {
        String parts[] = idPassword.split(":", 2);//split字符串
        byte digest[] = MessageDigest.getInstance("SHA1").digest(
                idPassword.getBytes());//SHA1加密
        return parts[0] + ":" + base64Encode(digest);//id+":"+base64加密
    }

    public KeeperException.Code
        handleAuthentication(ServerCnxn cnxn, byte[] authData)
    {
        String id = new String(authData);
        try {
            String digest = generateDigest(id);//拿到digest id
            if (digest.equals(superDigest)) {//判断是不是super用户
                cnxn.addAuthInfo(new Id("super", ""));
            }
            cnxn.addAuthInfo(new Id(getScheme(), digest));//把schema和digest值添加到连接信息里
            return KeeperException.Code.OK;
        } catch (NoSuchAlgorithmException e) {
            LOG.error("Missing algorithm",e);
        }
        return KeeperException.Code.AUTHFAILED;
    }

    public boolean isAuthenticated() {
        return true;//可以定位creator
    }

    public boolean isValid(String id) {
        String parts[] = id.split(":");
        return parts.length == 2;//校验合法性,通过判断是否是以“:”分割的字符串
    }

    public boolean matches(String id, String aclExpr) {
        return id.equals(aclExpr);//字符串相等时才匹配
    }

    /** Call with a single argument of user:pass to generate authdata.
     * Authdata output can be used when setting superDigest for example.
     * @param args single argument of user:pass
     * @throws NoSuchAlgorithmException
     */
    public static void main(String args[]) throws NoSuchAlgorithmException {
        for (int i = 0; i < args.length; i++) {
            System.out.println(args[i] + "->" + generateDigest(args[i]));
        }
    }
}

关于isAuthenticated的一点解释:zookeeper官方文档是这样说的:If the new ACL includes an "auth" entry, isAuthenticated is used to see if the authentication information for this scheme that is assocatied with the connection should be added to the ACL. Some schemes should not be included in auth. For example, the IP address of the client is not considered as an id that should be added to the ACL if auth is specified.

也就是说如果是request的authinfo是auth的schema,那么它对应的Id应该被加入当前ACL的list里,但是zk在这里做了限制,并不是所有的schema的Id都可以加入ACL的list里的,如果用IP的schema设置的就不行,但如果是digest模式的就可以。结合后面在PrepRequestProcessor的fixupACL一起看。

ProviderRegistry

三种provider都会在ProviderRegistry中注册,使用了策略器模式。

public class ProviderRegistry {
    private static final Logger LOG = LoggerFactory.getLogger(ProviderRegistry.class);

    private static boolean initialized = false;
    private static HashMap<String, AuthenticationProvider> authenticationProviders =
        new HashMap<String, AuthenticationProvider>();

    public static void initialize() {
        synchronized (ProviderRegistry.class) {
            if (initialized)
                return;
            //ip和digest两种schema的provider注册
            IPAuthenticationProvider ipp = new IPAuthenticationProvider();
            DigestAuthenticationProvider digp = new DigestAuthenticationProvider();
            //加入ProviderRegistry的域
            authenticationProviders.put(ipp.getScheme(), ipp);
            authenticationProviders.put(digp.getScheme(), digp);
            //去读properties文件,拿到所有的key
            Enumeration<Object> en = System.getProperties().keys();
            while (en.hasMoreElements()) {
                String k = (String) en.nextElement();
                //自定义的acl provider在开发后需要注册到zk中,后面详细说下
                if (k.startsWith("zookeeper.authProvider.")) {
                    String className = System.getProperty(k);//获取类名
                    try {
                        Class<?> c = ZooKeeperServer.class.getClassLoader()
                                .loadClass(className);//类加载
                        AuthenticationProvider ap = (AuthenticationProvider) c
                                .getDeclaredConstructor().newInstance();
                        authenticationProviders.put(ap.getScheme(), ap);//把自定义的权限控制器加入map中
                    } catch (Exception e) {
                        LOG.warn("Problems loading " + className,e);
                    }
                }
            }
            initialized = true;
        }
    }

    //根绝schema获取对应的authentication provider
    public static AuthenticationProvider getProvider(String scheme) {
        if(!initialized)
            initialize();
        return authenticationProviders.get(scheme);
    }

    //列出所有的provider
    public static String listProviders() {
        StringBuilder sb = new StringBuilder();
        for(String s: authenticationProviders.keySet()) {
        sb.append(s + " ");
}
        return sb.toString();
    }
}

补充:前面说的自定义权限控制器在zk中主要分为两步:

  1. 自定义类实现AuthenticationProvider接口
  2. 注册自定义的权限控制器,有两种方法:
    1. 在启动zk时在启动参数中配置-Dzookeeper.authProvider.序号(1,2...)=类路径(com.a.b.cprovider);
    2. 在zoo.cfg中配置authProvider.序号(1,2...)=类路径(com.a.b.cprovider);

一旦自己写了自定义的权限控制器,在ProviderRegistry中会去扫描所有的权限控制器,并负责注册他们。

总结一下zk的ACL系统schema,供user使用的主要有4种:

ip:形式为"ip:expression" ,expression可以为单个Ip也可以为表达多个ip的表达式;

digest:"username:password",最常见的;

world:对所有客户端开放;

super:超级管理员权限,可以在zookeeper启动时设置,不设置也有默认值。

其实根据代码还有两种,auth模式表示已经认证的用户,sasl这种看代码也没看到哪里使用了,不是特别清楚。

ACL的使用

这里所说的ACL的使用不只是说代码里那里验证用到了acl,而是从创建、修改到使用都会有相关的代码分析。

其中,在Zookeeper类中create()方法(create节点)中首先有对ACL的简单校验和设置,这里仅仅贴出相关代码。

if (acl != null && acl.size() == 0) { //设置的acl不可以为空
    throw new KeeperException.InvalidACLException();
}
request.setAcl(acl);//在请求中添加acl,记得之前提到过request中包含path,data和acl列表。

同时,Zookeeper类中又同步和异步两种setacl的方法,也和zookeeper的很多接口一样,都提供同步和异步两种方案。这里看下异步的代码,其实同步的和异步的逻辑差不多。

/**
 * The asynchronous version of create.
 *
 * @see #create(String, byte[], List, CreateMode)
 */

public void setACL(final String path, List<ACL> acl, int version,
            StatCallback cb, Object ctx)
{
    final String clientPath = path;
    PathUtils.validatePath(clientPath, createMode.isSequential());//首先去校验一下path字符串的合法性

    final String serverPath = prependChroot(clientPath);//这是prepend命名空间的一步,zookeeper可以在配置的时候就规定命名空间,然后把clientpath append到命名空间后

    RequestHeader h = new RequestHeader();
    h.setType(ZooDefs.OpCode.setACL);//请求头中设置setacl的opcode
    SetACLRequest request = new SetACLRequest();
    request.setPath(serverPath);//设置路径
    request.setAcl(acl);//设置路径
    request.setVersion(version);//设置版本
    SetACLResponse response = new SetACLResponse();
    cnxn.queuePacket(h, new ReplyHeader(), request, response, cb,
                     clientPath, serverPath, ctx, null);//把请求放入queue中
}

PrepRequestProcessor类是负责请求处理链的功能类,后面会详细讲到,这里先简单说下和ACL基本验证有关的方法fixupACL和校验的方法checkACL。

/**
 * This method checks out the acl making sure it isn't null or empty,
 * it has valid schemes and ids, and expanding any relative ids that
 * depend on the requestor's authentication information.
 *
 * @param authInfo list of ACL IDs associated with the client connection
 * @param acl list of ACLs being assigned to the node (create or setACL operation)
 * @return
 */
//
private boolean fixupACL(List<Id> authInfo, List<ACL> acl) {
    if (skipACL) { //zk可以配置是否跳过ACL检查
        return true;
    }
    if (acl == null || acl.size() == 0) {//acl列表不可为null或空
        return false;
    }

    Iterator<ACL> it = acl.iterator();
    LinkedList<ACL> toAdd = null;
    while (it.hasNext()) {
        ACL a = it.next();
        Id id = a.getId();
        if (id.getScheme().equals("world") && id.getId().equals("anyone")) {
            // wide open
            //如果是world模式,对所有客户端开放
        } else if (id.getScheme().equals("auth")) {//id的schema是auth
            // This is the "auth" id, so we have to expand it to the
            // authenticated ids of the requestor
            it.remove();//auth模式下吧这个从acl列表中删掉
            if (toAdd == null) {
                toAdd = new LinkedList<ACL>();
            }
            boolean authIdValid = false;
            for (Id cid : authInfo) {
                AuthenticationProvider ap =
                    ProviderRegistry.getProvider(cid.getScheme());//根据schema去获取验证控制器
                if (ap == null) {
                    LOG.error("Missing AuthenticationProvider for "
                            + cid.getScheme());//验证控制器不存在就打log
                } else if (ap.isAuthenticated()) {//ip的会返回false, digest的会返回true,表示是否符合验证
                    authIdValid = true;
                    toAdd.add(new ACL(a.getPerms(), cid));//把ACL加入list中
                }
            }
            if (!authIdValid) {//在auth模式下是否valid
                return false;
            }
        } else {//在其他模式下,ip, digest
            AuthenticationProvider ap = ProviderRegistry.getProvider(id
                    .getScheme());//去ProviderRegistry获取对应的AuthenticationProvider
            if (ap == null) {//AuthenticationProvider校验
                return false;
            }
            if (!ap.isValid(id.getId())) {//调用AuthenticationProvider的检验方法去查询格式是否合法
                return false;
            }
        }
    }
    if (toAdd != null) {//toAdd不为空表示auth模式下有增加的id
        for (ACL a : toAdd) {
            acl.add(a);
        }
    }
    return acl.size() > 0;
}

总结:fixupACL其实主要分为三步:

  1. 如果是world, anyone,通过;
  2. 如果是auth模式,那么就跟前面讲isAuthenticated的一样,把符合的schema的id加入acl列表;
  3. 如果是其他的模式下,那么就要去检验id的格式合法性。

再看下checkACL:

/**
*
* @param zks zk server对象
* @param acl 对应节点或者父节点拥有的权限
* @param perm 目前操作需要的权限
* @param ids 目前请求提供的权限
* 参数解释 from https://www.jianshu.com/p/1dee7ad908fe
*/
static void checkACL(ZooKeeperServer zks, List<ACL> acl, int perm,
        List<Id> ids) throws KeeperException.NoAuthException {
    if (skipACL) {//是否跳过acl
        return;
    }
    if (acl == null || acl.size() == 0) {//ACL不能为空
        return;
    }
    for (Id authId : ids) {
        if (authId.getScheme().equals("super")) {//如果是request的authinfo里有super用户,那么直接返回
            return;
        }
    }
    for (ACL a : acl) {//遍历ACL列表
        Id id = a.getId(); 
        if ((a.getPerms() & perm) != 0) {如果当前节点可以做perm的操作
            if (id.getScheme().equals("world")
                    && id.getId().equals("anyone")) {//如果节点提供了world的访问权限,那么直接返回
                return;
            }
            AuthenticationProvider ap = ProviderRegistry.getProvider(id
                    .getScheme());//根据id获取对应的AuthenticationProvider
            if (ap != null) {
                for (Id authId : ids) {//遍历request拥有的id                        
                    if (authId.getScheme().equals(id.getScheme())
                            && ap.matches(authId.getId(), id.getId())) {//节点的id和request的id作对比,如果match就返回
                        return;
                    }
                }
            }
        }
    }
    throw new KeeperException.NoAuthException();
}

总结:checkACL是用来做ACL校验的重要方法,如果验证失败会直接抛出NoAuthException的异常。还有fixupACL和checkACL的主要区别在于前者是判断新的id是否合理以及加入ACL列表中;而后者是当前请求和节点权限合法性的检查,即当前请求是否能对节点进行某种操作。

思考

工厂模式和策略期模式

两者感觉很近似,看了些资料,大致的相同点在于都利用了封装,把具体的实现或者说操作隐藏在了第三方类的内部,客户端不需要关注内部的细节;

而大致的不同在于说工厂模式更关于于不同条件下实例的生成,而策略器模式则更关注的是在封装类内部可以自定义不同的操作,是否返回实例并不是关注的重点。

可以看看 策略模式和工厂模式的区别简单工厂,工厂,抽象工厂和策略的区别

命名空间

我以前都不知道zk可以设置命名空间,也是之前看代码的时候看到了去查资料看到的,感觉有时候用起来还是挺有意义的。很简单,可以看下 zk命名空间

参考

https://blog.csdn.net/summer_thirtyOne/article/details/51901575

https://blog.csdn.net/lovingprince/article/details/6935465

https://blog.csdn.net/lovingprince/article/details/6935465 最后关于super,world区别和前面用auth设置的例子可以看下

以上是关于Zookeeper源码阅读 ACL基础的主要内容,如果未能解决你的问题,请参考以下文章

zookeeper源码之权限控制

Zookeeper源码阅读 Server端Watcher

zookeeper 源码 选举和同步数据

ZooKeeper 源码阅读版本选择

如何进行 Java 代码阅读分析?

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建