1、命令入口
经常使用Kafka的同学可能已经注意到了,赋予某个用户消费某个topic权限的命令是:
bin/kafka-acls --authorizer kafka.security.auth.SimpleAclAuthorizer \
--authorizer-properties zookeeper.connect=localhost:2181 --add \
--allow-principal User:* --operation read --topic test --group mygroup
在这个命令中,清晰得指出了鉴权使用的类是kafka.security.auth.SimpleAclAuthorizer
;
当然你也可以不指定,查看kafka-acl
脚本,只有一句话:
exec $(dirname $0)/kafka-run-class.sh kafka.admin.AclCommand "$@"
,
打开AclCommand
类的代码,解析命令行参数的代码包含这么一段:
class AclCommandOptions(args: Array[String]) {
val parser = new OptionParser(false)
val authorizerOpt = parser.accepts("authorizer", "Fully qualified class name of the authorizer, defaults to kafka.security.auth.SimpleAclAuthorizer.")
.withRequiredArg
.describedAs("authorizer")
.ofType(classOf[String])
.defaultsTo(classOf[SimpleAclAuthorizer].getName)
不指定鉴权类时,默认就是kafka.security.auth.SimpleAclAuthorizer
。
2、 逐句解读kafka.security.auth.SimpleAclAuthorizer
类
在解读代码前,先解释下Acl的概念。
顾名思义,ACL是访问控制列表(access control list)。但是,大家经常会误认为权限控制是ACL实现的。其实,ACL只是提供了存储机制,具体的权限控制逻辑由权限控制模块实现。另外,ACL权限分为功能权限和数据权限。
联系到Kafka topic操作,创建、修改、删除topic的权限属于功能权限,用户能否在某个topic生产和消费的权限属于数据权限。
创建、修改、删除Topic的权限不是由kafka.security.auth.SimpleAclAuthorizer
类控制的,而是由ZK控制。
org.I0Itec.zkclient.ZkClient类
/**
* Add authentication information to the connection. This will be used to identify the user and check access to
* nodes protected by ACLs
*
* @param scheme
* @param auth
*/
public void addAuthInfo(final String scheme, final byte[] auth) {
retryUntilConnected(new Callable
功能权限不是本文分析的重点,不展开叙述。
SimpleAclAuthorizer类只有368行(包含注释),完成鉴权的核心方法只有不到30行。(Kafka的代码还是很简洁的!)
override def authorize(session: Session, operation: Operation, resource: Resource): Boolean = {
val principal = session.principal
val host = session.clientAddress.getHostAddress
val acls = getAcls(resource) ++ getAcls(new Resource(resource.resourceType, Resource.WildCardResource))
// Check if there is any Deny acl match that would disallow this operation.
val denyMatch = aclMatch(operation, resource, principal, host, Deny, acls)
// Check if there are any Allow ACLs which would allow this operation.
// Allowing read, write, delete, or alter implies allowing describe.
// See #{org.apache.kafka.common.acl.AclOperation} for more details about ACL inheritance.
val allowOps = operation match {
case Describe => Set[Operation](Describe, Read, Write, Delete, Alter)
case DescribeConfigs => Set[Operation](DescribeConfigs, AlterConfigs)
case _ => Set[Operation](operation)
}
val allowMatch = allowOps.exists(operation => aclMatch(operation, resource, principal, host, Allow, acls))
//we allow an operation if a user is a super user or if no acls are found and user has configured to allow all users
//when no acls are found or if no deny acls are found and at least one allow acls matches.
val authorized = isSuperUser(operation, resource, principal, host) ||
isEmptyAclAndAuthorized(operation, resource, principal, host, acls) ||
(!denyMatch && allowMatch)
logAuditMessage(principal, authorized, operation, resource, host)
authorized
}
方法只有三个参数:session保存了使用鉴权方法的客户端信息;operation就是具体鉴权的操作。比如:consumer、producer等;Resource就是本次鉴权的资源。比如:topic,group。
方法体内,首先得到本次鉴权的principal,pricinpal是KafkaPrincipal类的实例,类定义如下:
public class KafkaPrincipal implements Principal {
public static final String SEPARATOR = ":";
public static final String USER_TYPE = "User";
public final static KafkaPrincipal ANONYMOUS = new KafkaPrincipal(KafkaPrincipal.USER_TYPE, "ANONYMOUS");
private String principalType;
private String name;
public KafkaPrincipal(String principalType, String name) {
if (principalType == null || name == null) {
throw new IllegalArgumentException("principalType and name can not be null");
}
this.principalType = principalType;
this.name = name;
}
public static KafkaPrincipal fromString(String str) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException("expected a string in format principalType:principalName but got " + str);
}
String[] split = str.split(SEPARATOR, 2);
if (split == null || split.length != 2) {
throw new IllegalArgumentException("expected a string in format principalType:principalName but got " + str);
}
return new KafkaPrincipal(split[0], split[1]);
}
代码比较简单,有用的信息主要是这两句:
if (split == null || split.length != 2) {
throw new IllegalArgumentException("expected a string in format principalType:principalName but got " + str);
}
为何有用,暂且留个伏笔。
接着看下面的代码,是至关重要的一句:
val acls = getAcls(resource) ++ getAcls(new Resource(resource.resourceType, Resource.WildCardResource))
这句代码会去ZK查询本资源的Acl列表,比如本次鉴权的资源对象是:new Resource(Topic, "myTopic")
,有两个注意点:
1、ResourceType是个枚举类,定义了Kafka里的各种资源,比如topic,group等。
2、不是实时从ZK中读取,而是为了提高效率,从缓存中读取。那么缓存什么时候更新呢?
可能读者已经注意到了这个变量Resource.WildCardResource
,这个变量是个字符串,就是*
号。为何要搞个名称是*
号的资源呢?
kafka可能包含成百上千的Topic,消费时候可能包含更多的group,我们不大可能对这些资源一一赋予权限,所以就需要通配机制。比如,文章开头那条命令稍作修改,将具体的资源名称改为*
号:
bin/kafka-acls --authorizer kafka.security.auth.SimpleAclAuthorizer \
--authorizer-properties zookeeper.connect=localhost:2181 --add \
--allow-principal User:* --operation read --topic '*' --group '*'
表示对所有的topic和group赋予权限。这条语句为何能起作用,就是因为我们查询Acl时候构造了名称为*
号的资源!
各资源的Acl都是以json格式存储在ZK路径上,
/kafka-acl/Topic/topic-1 => {"version": 1, "acls": [ { "host":"host1", "permissionType": "Allow","operation": "Read","principal": "User:alice"}]}
/kafka-acl/Cluster/kafka-cluster => {"version": 1, "acls": [ { "host":"host1", "permissionType": "Allow","operation": "Read","principal": "User:alice"}]}
/kafka-acl/Group/group-1 => {"version": 1, "acls": [ { "host":"host1", "permissionType": "Allow","operation": "Read","principal": "User:alice"}]}
我们看下Acl类的代码:
object Acl {
val WildCardPrincipal: KafkaPrincipal = new KafkaPrincipal(KafkaPrincipal.USER_TYPE, "*")
val WildCardHost: String = "*"
val AllowAllAcl = new Acl(WildCardPrincipal, Allow, WildCardHost, All)
val PrincipalKey = "principal"
val PermissionTypeKey = "permissionType"
val OperationKey = "operation"
val HostsKey = "host"
val VersionKey = "version"
val CurrentVersion = 1
val AclsKey = "acls"
..........
def fromJson(aclJson: String): Set[Acl] = {
if (aclJson == null || aclJson.isEmpty)
return collection.immutable.Set.empty[Acl]
var acls: collection.mutable.HashSet[Acl] = new collection.mutable.HashSet[Acl]()
Json.parseFull(aclJson) match {
case Some(m) =>
val aclMap = m.asInstanceOf[Map[String, Any]]
//the acl json version.
require(aclMap(VersionKey) == CurrentVersion)
val aclSet: List[Map[String, Any]] = aclMap(AclsKey).asInstanceOf[List[Map[String, Any]]]
aclSet.foreach(item => {
val principal: KafkaPrincipal = KafkaPrincipal.fromString(item(PrincipalKey).asInstanceOf[String])
val permissionType: PermissionType = PermissionType.fromString(item(PermissionTypeKey).asInstanceOf[String])
val operation: Operation = Operation.fromString(item(OperationKey).asInstanceOf[String])
val host: String = item(HostsKey).asInstanceOf[String]
acls += new Acl(principal, permissionType, host, operation)
})
case None =>
}
acls.toSet
}
通过fromJson方法将ZK中存储的Acl json反序列化为Acl对象,KafkaPrincipal使用fromString方法反序列化,那么要求我们保存在ZK中的用户pricinpal字符串必须是类型:名称
格式。(呼应上面伏笔)
PermissionType和Operation类都是通过fromString方法反序列化,这要求我们保存在ZK中的字符串必须在它们的枚举定义范围内。
接下来是真正的鉴权逻辑:
1、首先判断是否有不允许此次操作的Acl
2、接着判断是否有允许此操作的Acl(Describe操作比较特殊,允许read, write, delete, or alter都意味着允许Describe)
3、然后判断是否是超级用户
4、再判断是否没有Acl时候默认是有权限,这是Kafka的一个配置项allow.everyone.if.no.acl.found
最终判断本次鉴权是否通过的逻辑,代码的注释写得很清楚:
1、如果是超级用户,通过
2、如果配置项allow.everyone.if.no.acl.found
设置为true,通过
3、如果没有Acl Deny,且至少有一个Acl Allow,通过
至此,这个方法就解读完了。是不是很简单?
上面遗留了一个问题,Acl的缓存什么时候更新呢?在初始化方法configure
中可以看到,
override def configure(javaConfigs: util.Map[String, _]) {
val configs = javaConfigs.asScala
val props = new java.util.Properties()
configs.foreach { case (key, value) => props.put(key, value.toString) }
superUsers = configs.get(SimpleAclAuthorizer.SuperUsersProp).collect {
case str: String if str.nonEmpty => str.split(";").map(s => KafkaPrincipal.fromString(s.trim)).toSet
}.getOrElse(Set.empty[KafkaPrincipal])
shouldAllowEveryoneIfNoAclIsFound = configs.get(SimpleAclAuthorizer.AllowEveryoneIfNoAclIsFoundProp).exists(_.toString.toBoolean)
// Use `KafkaConfig` in order to get the default ZK config values if not present in `javaConfigs`. Note that this
// means that `KafkaConfig.zkConnect` must always be set by the user (even if `SimpleAclAuthorizer.ZkUrlProp` is also
// set).
val kafkaConfig = KafkaConfig.fromProps(props, doLog = false)
val zkUrl = configs.get(SimpleAclAuthorizer.ZkUrlProp).map(_.toString).getOrElse(kafkaConfig.zkConnect)
val zkConnectionTimeoutMs = configs.get(SimpleAclAuthorizer.ZkConnectionTimeOutProp).map(_.toString.toInt).getOrElse(kafkaConfig.zkConnectionTimeoutMs)
val zkSessionTimeOutMs = configs.get(SimpleAclAuthorizer.ZkSessionTimeOutProp).map(_.toString.toInt).getOrElse(kafkaConfig.zkSessionTimeoutMs)
zkUtils = ZkUtils(zkUrl,
sessionTimeout = zkSessionTimeOutMs,
connectionTimeout = zkConnectionTimeoutMs,
kafkaConfig.zkEnableSecureAcls)
zkUtils.makeSurePersistentPathExists(SimpleAclAuthorizer.AclZkPath)
loadCache()
zkUtils.makeSurePersistentPathExists(SimpleAclAuthorizer.AclChangedZkPath)
aclChangeListener = new ZkNodeChangeNotificationListener(zkUtils, SimpleAclAuthorizer.AclChangedZkPath, SimpleAclAuthorizer.AclChangedPrefix, AclChangedNotificationHandler)
aclChangeListener.init()
}
1、初始化的时候会loadCache
2、初始化时候注册了ZK的aclChange
监听(/kafka-acl-changes/acl_changes_0000000000)(ZK的监听你还知道哪些?)
当某个资源的acl发生变化时,就是在ZK的/kafka-acl-changes路径下生成一个递增的流水号Node,Node的Data存储acl发生变化的资源信息。资源信息字符串参考Resource
的定义:
case class Resource(resourceType: ResourceType, name: String) {
override def toString: String = {
resourceType.name + Resource.Separator + name
}
}
3、 二次开发kafka.security.auth.SimpleAclAuthorizer
类
Kafka-0.11.0.1版本只支持对用户的鉴权,那么如果我们想支持对用户角色的鉴权,该如何做呢?
如果你读懂了上面的代码解读,很容易想到解决方案。
1、创建用户角色时候,在ZK相应的资源节点写入Acl数据(你只需构造一个表示角色的KafkaPrincipal)
2、鉴权时候,查找到用户绑定的角色,读取该角色的Acl进行鉴权(你只需构造一个表示角色的KafkaPrincipal)
3、当角色的权限发生变化时,记得在/kafka-acl-changes下生成流水记录,保证角色权限的变更实时更新。