源码分析RocketMQ ACL实现机制

本文详细阐述了RocketMQ的访问控制列表(AccessControlList,ACL)实现,包括如何通过配置开启和加载AccessValidator,以及PlainAccessValidator的类图、构造方法和parse和validate方法的工作原理。重点介绍了配置文件plain_acl.yml的解析和权限验证过程。
摘要由CSDN通过智能技术生成

本方法的实现共4个关键点。

代码@1:首先判断Broker是否开启了acl,通过配置参数aclEnable指定,默认为false。

代码@2:使用类似SPI机制,加载配置的AccessValidator,该方法返回一个列表,其实现逻辑时读取META-INF/service/org.apache.rocketmq.acl.AccessValidator文件中配置的访问验证器,默认配置内容如下:

在这里插入图片描述

代码@3:遍历配置的访问验证器(AccessValidator),并向Broker处理服务器注册钩子函数,RPCHook的doBeforeRequest方法会在服务端接收到请求,将其请求解码后,执行处理请求之前被调用;RPCHook的doAfterResponse方法会在处理完请求后,将结果返回之前被调用,其调用如图所示:

在这里插入图片描述

代码@4:在RPCHook#doBeforeRequest方法中调用AccessValidator#validate, 在真实处理命令之前,先执行ACL的验证逻辑,如果拥有该操作的执行权限,则放行,否则抛出AclException。

接下来,我们将重点放到Broker默认实现的访问验证器:PlainAccessValidator。

2、PlainAccessValidator


2.1 类图

在这里插入图片描述

  • AccessValidator

访问验证器接口,主要定义两个接口。

1)AccessResource parse(RemotingCommand request, String remoteAddr)

从请求头中解析本次请求对应的访问资源,即本次请求需要的访问权限。

2)void validate(AccessResource accessResource)

根据本次需要访问的权限,与请求用户拥有的权限进行对比验证,判断是拥有权限,如果没有访问该操作的权限,则抛出异常,否则放行。

  • PlainAccessValidator

RocketMQ默认提供的基于yml配置格式的访问验证器。

接下来我们重点看一下PlainAccessValidator的parse方法与validate方法的实现细节。在讲解该方法之前,我们首先认识一下RocketMQ封装访问资源的PlainAccessResource。

2.1.2 PlainAccessResource类图

在这里插入图片描述

我们对其属性一一做个介绍:

  • private String accessKey

访问Key,用户名。

  • private String secretKey

用户密码。

  • private String whiteRemoteAddress

远程IP地址白名单。

  • private boolean admin

是否是管理员角色。

  • private byte defaultTopicPerm = 1

默认topic访问权限,即如果没有配置topic的权限,则Topic默认的访问权限为1,表示为DENY。

  • private byte defaultGroupPerm = 1

默认的消费组访问权限,默认为DENY。

  • private Map<String, Byte> resourcePermMap

资源需要的访问权限映射表。

  • private RemoteAddressStrategy remoteAddressStrategy

远程IP地址验证策略。

  • private int requestCode

当前请求的requestCode。

  • private byte[] content

请求头与请求体的内容。

  • private String signature

签名字符串,这是通常的套路,在客户端时,首先将请求参数排序,然后使用secretKey生成签名字符串,服务端重复这个步骤,然后对比签名字符串,如果相同,则认为登录成功,否则失败。

  • private String secretToken

密钥token。

  • private String recognition

目前作用未知,代码中目前未被使用。

2.2 构造方法

public PlainAccessValidator() {

aclPlugEngine = new PlainPermissionLoader();

}

构造函数,直接创建PlainPermissionLoader对象,从命名上来看,应该是触发acl规则的加载,即解析plain_acl.yml,接下来会重点探讨,即acl启动流程之配置文件的解析。

2.3 parse方法

该方法的作用就是从请求命令中解析出本次访问所需要的访问权限,最终构建AccessResource对象,为后续的校验权限做准备。

PlainAccessResource accessResource = new PlainAccessResource();

if (remoteAddr != null && remoteAddr.contains(“:”)) {

accessResource.setWhiteRemoteAddress(remoteAddr.split(“:”)[0]);

} else {

accessResource.setWhiteRemoteAddress(remoteAddr);

}

Step1:首先创建PlainAccessResource,从远程地址中提取出远程访问IP地址。

if (request.getExtFields() == null) {

throw new AclException(“request’s extFields value is null”);

}

accessResource.setRequestCode(request.getCode());

accessResource.setAccessKey(request.getExtFields().get(SessionCredentials.ACCESS_KEY));

accessResource.setSignature(request.getExtFields().get(SessionCredentials.SIGNATURE));

accessResource.setSecretToken(request.getExtFields().get(SessionCredentials.SECURITY_TOKEN));

Step2:如果请求头中的扩展字段为空,则抛出异常,如果不为空,则从请求头中读取requestCode、accessKey(请求用户名)、签名字符串(signature)、secretToken。

try {

switch (request.getCode()) {

case RequestCode.SEND_MESSAGE:

accessResource.addResourceAndPerm(request.getExtFields().get(“topic”), Permission.PUB);

break;

case RequestCode.SEND_MESSAGE_V2:

accessResource.addResourceAndPerm(request.getExtFields().get(“b”), Permission.PUB);

break;

case RequestCode.CONSUMER_SEND_MSG_BACK:

accessResource.addResourceAndPerm(request.getExtFields().get(“originTopic”), Permission.PUB);

accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get(“group”)), Permission.SUB);

break;

case RequestCode.PULL_MESSAGE:

accessResource.addResourceAndPerm(request.getExtFields().get(“topic”), Permission.SUB);

accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get(“consumerGroup”)), Permission.SUB);

break;

case RequestCode.QUERY_MESSAGE:

accessResource.addResourceAndPerm(request.getExtFields().get(“topic”), Permission.SUB);

break;

case RequestCode.HEART_BEAT:

HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);

for (ConsumerData data : heartbeatData.getConsumerDataSet()) {

accessResource.addResourceAndPerm(getRetryTopic(data.getGroupName()), Permission.SUB);

for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) {

accessResource.addResourceAndPerm(subscriptionData.getTopic(), Permission.SUB);

}

}

break;

case RequestCode.UNREGISTER_CLIENT:

final UnregisterClientRequestHeader unregisterClientRequestHeader =

(UnregisterClientRequestHeader) request

.decodeCommandCustomHeader(UnregisterClientRequestHeader.class);

accessResource.addResourceAndPerm(getRetryTopic(unregisterClientRequestHeader.getConsumerGroup()), Permission.SUB);

break;

case RequestCode.GET_CONSUMER_LIST_BY_GROUP:

final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader =

(GetConsumerListByGroupRequestHeader) request

.decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class);

accessResource.addResourceAndPerm(getRetryTopic(getConsumerListByGroupRequestHeader.getConsumerGroup()), Permission.SUB);

break;

case RequestCode.UPDATE_CONSUMER_OFFSET:

final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader =

(UpdateConsumerOffsetRequestHeader) request

.decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class);

accessResource.addResourceAndPerm(getRetryTopic(updateConsumerOffsetRequestHeader.getConsumerGroup()), Permission.SUB);

accessResource.addResourceAndPerm(updateConsumerOffsetRequestHeader.getTopic(), Permission.SUB);

break;

default:

break;

}

} catch (Throwable t) {

throw new AclException(t.getMessage(), t);

}

Step3:根据请求命令,设置本次请求需要拥有的权限,上述代码比较简单,就是从请求中得出本次操作的Topic、消息组名称,为了方便区分topic与消费组,消费组使用消费者对应的重试主题,当成资源的Key,从这里也可以看出,当前版本需要进行ACL权限验证的请求命令如下:

  • SEND_MESSAGE

  • SEND_MESSAGE_V2

  • CONSUMER_SEND_MSG_BACK

  • PULL_MESSAGE

  • QUERY_MESSAGE

  • HEART_BEAT

  • UNREGISTER_CLIENT

  • GET_CONSUMER_LIST_BY_GROUP

  • UPDATE_CONSUMER_OFFSET

// Content

SortedMap<String, String> map = new TreeMap<String, String>();

for (Map.Entry<String, String> entry : request.getExtFields().entrySet()) {

if (!SessionCredentials.SIGNATURE.equals(entry.getKey())) {

map.put(entry.getKey(), entry.getValue());

}

}

accessResource.setContent(AclUtils.combineRequestContent(request, map));

return accessResource;

Step4:对扩展字段进行排序,便于生成签名字符串,然后将扩展字段与请求体(body)写入content字段。完成从请求头中解析出本次请求需要验证的权限。

2.4 validate 方法

public void validate(AccessResource accessResource) {

aclPlugEngine.validate((PlainAccessResource) accessResource);

}

验证权限,即根据本次请求需要的权限与当前用户所拥有的权限进行对比,如果符合,则正常执行;否则抛出AclException。

为了揭开配置文件的解析与验证,我们将目光投入到PlainPermissionLoader。

3、PlainPermissionLoader


该类的主要职责:加载权限,即解析acl主要配置文件plain_acl.yml。

3.1 类图

在这里插入图片描述

下面对其核心属性与核心方法一一介绍:

  • DEFAULT_PLAIN_ACL_FILE

默认acl配置文件名称,默认值为conf/plain_acl.yml。

  • String fileName

acl配置文件名称,默认为DEFAULT_PLAIN_ACL_FILE ,可以通过系统参数-Drocketmq.acl.plain.file=fileName指定。

  • Map<String, PlainAccessResource> plainAccessResourceMap

解析出来的权限配置映射表,以用户名为键。

  • RemoteAddressStrategyFactory remoteAddressStrategyFactory

远程IP解析策略工厂,用于解析白名单IP地址。

  • boolean isWatchStart

是否开启了文件监听,即自动监听plain_acl.yml文件,一旦该文件改变,可在不重启服务器的情况下自动生效。

  • public PlainPermissionLoader()

构造方法。

  • public void load()

加载配置文件。

  • public void validate(PlainAccessResource plainAccessResource)

验证是否有权限访问待访问资源。

3.2 PlainPermissionLoader构造方法

public PlainPermissionLoader() {

load();

watch();

}

在构造方法中调用load与watch方法。

3.3 load

Map<String, PlainAccessResource> plainAccessResourceMap = new HashMap<>();

List globalWhiteRemoteAddressStrategy = new ArrayList<>();

String path = fileHome + File.separator + fileName;

JSONObject plainAclConfData = AclUtils.getYamlDataObject(path,JSONObject.class);

Step1:初始化plainAccessResourceMap(用户配置的访问资源,即权限容器)、globalWhiteRemoteAddressStrategy:全局IP白名单访问策略。配置文件,默认为${ROCKETMQ_HOME}/conf/plain_acl.yml。

JSONArray globalWhiteRemoteAddressesList = plainAclConfData.getJSONArray(“globalWhiteRemoteAddresses”);

if (globalWhiteRemoteAddressesList != null && !globalWhiteRemoteAddressesList.isEmpty()) {

for (int i = 0; i < globalWhiteRemoteAddressesList.size(); i++) {

globalWhiteRemoteAddressStrategy.add(remoteAddressStrategyFactory.

getRemoteAddressStrategy(globalWhiteRemoteAddressesList.getString(i)));

}

}

Step2:globalWhiteRemoteAddresses:全局白名单,类型为数组。根据配置的规则,使用remoteAddressStrategyFactory获取一个访问策略,下文会重点介绍其配置规则。

JSONArray accounts = plainAclConfData.getJSONArray(“accounts”);

if (accounts != null && !accounts.isEmpty()) {

List plainAccessConfigList = accounts.toJavaList(PlainAccessConfig.class);

for (PlainAccessConfig plainAccessConfig : plainAccessConfigList) {

PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig);

plainAccessResourceMap.put(plainAccessResource.getAccessKey(),plainAccessResource);

}

}

this.globalWhiteRemoteAddressStrategy = globalWhiteRemoteAddressStrategy;

this.plainAccessResourceMap = plainAccessResourceMap;

Step3:解析plain_acl.yml文件中的另外一个根元素accounts,用户定义的权限信息。从PlainAccessConfig的定义来看,accounts标签下支持如下标签:

  • accessKey

  • secretKey

  • whiteRemoteAddress

  • admin

  • defaultTopicPerm

  • defaultGroupPerm

  • topicPerms

  • groupPerms

上述标签的说明,请参考::《RocketMQ ACL使用指南》 。具体的解析过程比较容易,就不再细说。

load方法主要完成acl配置文件的解析,将用户定义的权限加载到内存中。

3.4 watch

private void watch() {

try {

String watchFilePath = fileHome + fileName;

FileWatchService fileWatchService = new FileWatchService(new String[] {watchFilePath}, new FileWatchService.Listener() {

@Override

public void onChanged(String path) {

log.info(“The plain acl yml changed, reload the context”);

load();

}

});

fileWatchService.start();

log.info(“Succeed to start AclWatcherService”);

this.isWatchStart = true;

} catch (Exception e) {

log.error(“Failed to start AclWatcherService”, e);

}

}

监听器,默认以500ms的频率判断文件的内容是否变化。在文件内容发生变化后调用load()方法,重新加载配置文件。那FileWatchService是如何判断两个文件的内容发生了变化呢?

FileWatchService#hash

private String hash(String filePath) throws IOException, NoSuchAlgorithmException {

Path path = Paths.get(filePath);

md.update(Files.readAllBytes(path));

byte[] hash = md.digest();

return UtilAll.bytes2string(hash);

}

获取文件md5签名来做对比,这里为什么不在启动时先记录上一次文件的修改时间,然后先判断其修改时间是否变化,再判断其内容是否真正发生变化。

3.5 validate

// Check the global white remote addr

for (RemoteAddressStrategy remoteAddressStrategy : globalWhiteRemoteAddressStrategy) {

if (remoteAddressStrategy.match(plainAccessResource)) {

return;

}

}

Step1:首先使用全局白名单对资源进行验证,只要一个规则匹配,则返回,表示认证成功。

if (plainAccessResource.getAccessKey() == null) {

throw new AclException(String.format(“No accessKey is configured”));

}

if (!plainAccessResourceMap.containsKey(plainAccessResource.getAccessKey())) {

throw new AclException(String.format(“No acl config for %s”, plainAccessResource.getAccessKey()));

}

Step2:如果请求信息中,没有设置用户名,则抛出未配置AccessKey异常;如果Broker中并为配置该用户的配置信息,则抛出AclException。

// Check the white addr for accesskey

PlainAccessResource ownedAccess = plainAccessResourceMap.get(plainAccessResource.getAccessKey());

if (ownedAccess.getRemoteAddressStrategy().match(plainAccessResource)) {

return;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

分布式技术专题+面试解析+相关的手写和学习的笔记pdf

还有更多Java笔记分享如下:

image

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-hFg4fY3y-1713751321071)]

[外链图片转存中…(img-0LDwdUVt-1713751321072)]

[外链图片转存中…(img-bHPDYHM0-1713751321072)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

分布式技术专题+面试解析+相关的手写和学习的笔记pdf

还有更多Java笔记分享如下:

[外链图片转存中…(img-eRSgNxMz-1713751321072)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值