RocketMQ源码分析之权限ACL

Broker服务端

 

初始化时会初始化权限相关的类,是否校验权限需要通过aclEnable控制,在指定目录下查找所有的访问校验规则类

BrokerController#initialize
public static final String ACL_VALIDATOR_ID = "META-INF/service/org.apache.rocketmq.acl.AccessValidator";
private void initialAcl() {
    if (!this.brokerConfig.isAclEnable()) {
        log.info("The broker dose not enable acl");
        return;
    }

    List accessValidators = ServiceProvider.load(ServiceProvider.ACL_VALIDATOR_ID, AccessValidator.class);
    if (accessValidators == null || accessValidators.isEmpty()) {
        log.info("The broker dose not load the AccessValidator");
        return;
    }

    for (AccessValidator accessValidator: accessValidators) {
        final AccessValidator validator = accessValidator;
        this.registerServerRPCHook(new RPCHook() {

            @Override
            public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
                //Do not catch the exception
                validator.validate(validator.parse(request, remoteAddr));
            }

            @Override
            public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) {
            }
        });
    }
}

包装成钩子类注册通讯服务端

public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer
public void registerRPCHook(RPCHook rpcHook) {
    if (rpcHook != null && !rpcHooks.contains(rpcHook)) {
        rpcHooks.add(rpcHook);
    }
}

在加载SPI时会初始化访问校验类PlainAccessValidator


public PlainAccessValidator() {
    aclPlugEngine = new PlainPermissionLoader();
}

实例化权限许可类

private String fileHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
private String fileName = System.getProperty("rocketmq.acl.plain.file", DEFAULT_PLAIN_ACL_FILE);
public PlainPermissionLoader() {
    load();
    watch();
}

从指定目录加载服务端配置的权限文件


public void load() {
    Map plainAccessResourceMap = new HashMap<>();
    List globalWhiteRemoteAddressStrategy = new ArrayList<>();

    JSONObject plainAclConfData = AclUtils.getYamlDataObject(fileHome + File.separator + fileName,
        JSONObject.class);

    if (plainAclConfData == null || plainAclConfData.isEmpty()) {
        throw new AclException(String.format("%s file  is not data", fileHome + File.separator + fileName));
    }
    log.info("Broker plain acl conf data is : ", plainAclConfData.toString());
    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)));
        }
    }

    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;
}

解析具体的Yaml文件

public static  T getYamlDataObject(String path, Class clazz) {
    Yaml ymal = new Yaml();
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(new File(path));
        return ymal.loadAs(fis, clazz);
    } catch (Exception e) {
        throw new AclException(String.format("The  file for Plain mode was not found , paths %s", path), e);
    } finally {
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                throw new AclException("close transport fileInputStream Exception", e);
            }
        }
    }
}

解析全局白名单地址,不同的规则对应不同的策略类

public RemoteAddressStrategy getRemoteAddressStrategy(String remoteAddr) {
    if (StringUtils.isBlank(remoteAddr)) {
        return BLANK_NET_ADDRESS_STRATEGY;
    }
    if ("*".equals(remoteAddr)) {
        return NULL_NET_ADDRESS_STRATEGY;
    }
    if (remoteAddr.endsWith("}")) {
        String[] strArray = StringUtils.split(remoteAddr, ".");
        String four = strArray[3];
        if (!four.startsWith("{")) {
            throw new AclException(String.format("MultipleRemoteAddressStrategy netaddress examine scope Exception netaddress", remoteAddr));
        }
        return new MultipleRemoteAddressStrategy(AclUtils.getAddreeStrArray(remoteAddr, four));
    } else if (AclUtils.isColon(remoteAddr)) {
        return new MultipleRemoteAddressStrategy(StringUtils.split(remoteAddr, ","));
    } else if (AclUtils.isAsterisk(remoteAddr) || AclUtils.isMinus(remoteAddr)) {
        return new RangeRemoteAddressStrategy(remoteAddr);
    }
    return new OneRemoteAddressStrategy(remoteAddr);

}

解析访问账户信息具体配置


public PlainAccessResource buildPlainAccessResource(PlainAccessConfig plainAccessConfig) throws AclException {
    if (plainAccessConfig.getAccessKey() == null
        || plainAccessConfig.getSecretKey() == null
        || plainAccessConfig.getAccessKey().length() <= 6
        || plainAccessConfig.getSecretKey().length() <= 6) {
        throw new AclException(String.format(
            "The accessKey=%s and secretKey=%s cannot be null and length should longer than 6",
                plainAccessConfig.getAccessKey(), plainAccessConfig.getSecretKey()));
    }
    PlainAccessResource plainAccessResource = new PlainAccessResource();
    plainAccessResource.setAccessKey(plainAccessConfig.getAccessKey());
    plainAccessResource.setSecretKey(plainAccessConfig.getSecretKey());
    plainAccessResource.setWhiteRemoteAddress(plainAccessConfig.getWhiteRemoteAddress());

    plainAccessResource.setAdmin(plainAccessConfig.isAdmin());

    plainAccessResource.setDefaultGroupPerm(Permission.parsePermFromString(plainAccessConfig.getDefaultGroupPerm()));
    plainAccessResource.setDefaultTopicPerm(Permission.parsePermFromString(plainAccessConfig.getDefaultTopicPerm()));

    Permission.parseResourcePerms(plainAccessResource, false, plainAccessConfig.getGroupPerms());
    Permission.parseResourcePerms(plainAccessResource, true, plainAccessConfig.getTopicPerms());

    plainAccessResource.setRemoteAddressStrategy(remoteAddressStrategyFactory.
            getRemoteAddressStrategy(plainAccessResource.getWhiteRemoteAddress()));

    return plainAccessResource;
}

解析默认的topic以及重试topic权限


public static final byte DENY = 1;
public static final byte ANY = 1 << 1;
public static final byte PUB = 1 << 2;
public static final byte SUB = 1 << 3;
public static byte parsePermFromString(String permString) {
    if (permString == null) {
        return Permission.DENY;
    }
    switch (permString.trim()) {
        case "PUB":
            return Permission.PUB;
        case "SUB":
            return Permission.SUB;
        case "PUB|SUB":
            return Permission.PUB | Permission.SUB;
        case "SUB|PUB":
            return Permission.PUB | Permission.SUB;
        case "DENY":
            return Permission.DENY;
        default:
            return Permission.DENY;
    }
}

解析配置的具体topic及重试topic权限

public static void parseResourcePerms(PlainAccessResource plainAccessResource, Boolean isTopic,
    List resources) {
    if (resources == null || resources.isEmpty()) {
        return;
    }
    for (String resource : resources) {
        String[] items = StringUtils.split(resource, "=");
        if (items.length == 2) {
            plainAccessResource.addResourceAndPerm(isTopic ? items[0].trim() : PlainAccessResource.getRetryTopic(items[0].trim()), parsePermFromString(items[1].trim()));
        } else {
            throw new AclException(String.format("Parse resource permission failed for %s:%s", isTopic ? "topic" : "group", resource));
        }
    }
}

启动文件更新观察线程,检查文件更新更新内存中最新的权限数据


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);
    }
}

客户端(生产消费者)

 

提供端消费端启动的时候需要设置RPCHook即AclClientRPCHook,注册到通讯客户端

DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5", getAclRPCHook(), new AllocateMessageQueueAveragely());
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_6", getAclRPCHook());
new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY, ACL_SECRET_KEY));

实例化会话凭证类

public SessionCredentials(String accessKey, String secretKey) {
    this.accessKey = accessKey;
    this.secretKey = secretKey;
}

在与服务端通讯时会先触发钩子

protected void doBeforeRpcHooks(String addr, RemotingCommand request) {
    if (rpcHooks.size() > 0) {
        for (RPCHook rpcHook: rpcHooks) {
            rpcHook.doBeforeRequest(addr, request);
        }
    }
}

执行请求前的加密处理


public class AclClientRPCHook implements RPCHook
public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
    byte[] total = AclUtils.combineRequestContent(request,
        parseRequestContent(request, sessionCredentials.getAccessKey(), sessionCredentials.getSecurityToken()));
    String signature = AclUtils.calSignature(total, sessionCredentials.getSecretKey());
    request.addExtField(SIGNATURE, signature);
    request.addExtField(ACCESS_KEY, sessionCredentials.getAccessKey());
    
    // The SecurityToken value is unneccessary,user can choose this one.
    if (sessionCredentials.getSecurityToken() != null) {
        request.addExtField(SECURITY_TOKEN, sessionCredentials.getSecurityToken());
    }
}

把请求头类中有值的属性进行打包放进map集合中

protected SortedMap parseRequestContent(RemotingCommand request, String ak, String securityToken) {
    CommandCustomHeader header = request.readCustomHeader();
    // Sort property
    SortedMap map = new TreeMap();
    map.put(ACCESS_KEY, ak);
    if (securityToken != null) {
        map.put(SECURITY_TOKEN, securityToken);
    }
    try {
        // Add header properties
        if (null != header) {
            Field[] fields = fieldCache.get(header.getClass());
            if (null == fields) {
                fields = header.getClass().getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                }
                Field[] tmp = fieldCache.putIfAbsent(header.getClass(), fields);
                if (null != tmp) {
                    fields = tmp;
                }
            }

            for (Field field : fields) {
                Object value = field.get(header);
                if (null != value && !field.isSynthetic()) {
                    map.put(field.getName(), value.toString());
                }
            }
        }
        return map;
    } catch (Exception e) {
        throw new RuntimeException("incompatible exception.", e);
    }
}

然后和消息体进行合并后组成全部的字节数组

public static byte[] combineRequestContent(RemotingCommand request, SortedMap fieldsMap) {
    try {
        StringBuilder sb = new StringBuilder("");
        for (Map.Entry entry : fieldsMap.entrySet()) {
            if (!SessionCredentials.SIGNATURE.equals(entry.getKey())) {
                sb.append(entry.getValue());
            }
        }

        return AclUtils.combineBytes(sb.toString().getBytes(CHARSET), request.getBody());
    } catch (Exception e) {
        throw new RuntimeException("incompatible exception.", e);
    }
}

用密钥进行加密,默认加密方式为HmacSHA1,生成签名放进请求头的扩展属性

public static String calSignature(byte[] data, String secretKey) {
    String signature = AclSigner.calSignature(data, secretKey);
    return signature;
}

服务端校验

 

处理请求前校验合法性,解析出请求头类的密钥等属性,判断请求码解析出对应的topic和重试topic所应该需要的许可,最后把请求头中的扩展属性和请求体进行合并生成具体的byte数组


public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
    //Do not catch the exception
    validator.validate(validator.parse(request, remoteAddr));
}

public AccessResource parse(RemotingCommand request, String remoteAddr) {
    PlainAccessResource accessResource = new PlainAccessResource();
    if (remoteAddr != null && remoteAddr.contains(":")) {
        accessResource.setWhiteRemoteAddress(remoteAddr.split(":")[0]);
    } else {
        accessResource.setWhiteRemoteAddress(remoteAddr);
    }

    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));

    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);
    }
    // Content
    SortedMap map = new TreeMap();
    for (Map.Entry entry : request.getExtFields().entrySet()) {
        if (!SessionCredentials.SIGNATURE.equals(entry.getKey())) {
            map.put(entry.getKey(), entry.getValue());
        }
    }
    accessResource.setContent(AclUtils.combineRequestContent(request, map));
    return accessResource;
}

校验合法性,首先判断全局白名单地址,判断访问key是否包含在服务端配置中,判断具体账户名即访问key所对应的白名单地址,校验签名信息

public void validate(AccessResource accessResource) {
    aclPlugEngine.validate((PlainAccessResource) accessResource);
}

public void validate(PlainAccessResource plainAccessResource) {

    // Check the global white remote addr
    for (RemoteAddressStrategy remoteAddressStrategy : globalWhiteRemoteAddressStrategy) {
        if (remoteAddressStrategy.match(plainAccessResource)) {
            return;
        }
    }

    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()));
    }

    // Check the white addr for accesskey
    PlainAccessResource ownedAccess = plainAccessResourceMap.get(plainAccessResource.getAccessKey());
    if (ownedAccess.getRemoteAddressStrategy().match(plainAccessResource)) {
        return;
    }

    // Check the signature
    String signature = AclUtils.calSignature(plainAccessResource.getContent(), ownedAccess.getSecretKey());
    if (!signature.equals(plainAccessResource.getSignature())) {
        throw new AclException(String.format("Check signature failed for accessKey=%s", plainAccessResource.getAccessKey()));
    }
    // Check perm of each resource

    checkPerm(plainAccessResource, ownedAccess);
}

最后校验topic的许可,有些请求码是需要admin权限的,判断具体的topic是否在服务端存在具体的配置,没有的话就使用默认的全局配置校验许可


void checkPerm(PlainAccessResource needCheckedAccess, PlainAccessResource ownedAccess) {
    if (Permission.needAdminPerm(needCheckedAccess.getRequestCode()) && !ownedAccess.isAdmin()) {
        throw new AclException(String.format("Need admin permission for request code=%d, but accessKey=%s is not", needCheckedAccess.getRequestCode(), ownedAccess.getAccessKey()));
    }
    Map needCheckedPermMap = needCheckedAccess.getResourcePermMap();
    Map ownedPermMap = ownedAccess.getResourcePermMap();

    if (needCheckedPermMap == null) {
        // If the needCheckedPermMap is null,then return
        return;
    }

    for (Map.Entry needCheckedEntry : needCheckedPermMap.entrySet()) {
        String resource = needCheckedEntry.getKey();
        Byte neededPerm = needCheckedEntry.getValue();
        boolean isGroup = PlainAccessResource.isRetryTopic(resource);

        if (!ownedPermMap.containsKey(resource)) {
            // Check the default perm
            byte ownedPerm = isGroup ? ownedAccess.getDefaultGroupPerm() :
                ownedAccess.getDefaultTopicPerm();
            if (!Permission.checkPermission(neededPerm, ownedPerm)) {
                throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup)));
            }
            continue;
        }
        if (!Permission.checkPermission(neededPerm, ownedPermMap.get(resource))) {
            throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup)));
        }
    }
}

admin权限码


// UPDATE_AND_CREATE_TOPIC
ADMIN_CODE.add(RequestCode.UPDATE_AND_CREATE_TOPIC);
// UPDATE_BROKER_CONFIG
ADMIN_CODE.add(RequestCode.UPDATE_BROKER_CONFIG);
// DELETE_TOPIC_IN_BROKER
ADMIN_CODE.add(RequestCode.DELETE_TOPIC_IN_BROKER);
// UPDATE_AND_CREATE_SUBSCRIPTIONGROUP
ADMIN_CODE.add(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP);
// DELETE_SUBSCRIPTIONGROUP
ADMIN_CODE.add(RequestCode.DELETE_SUBSCRIPTIONGROUP);

校验许可


public static boolean checkPermission(byte neededPerm, byte ownedPerm) {
    if ((ownedPerm & DENY) > 0) {
        return false;
    }
    if ((neededPerm & ANY) > 0) {
        return ((ownedPerm & PUB) > 0) || ((ownedPerm & SUB) > 0);
    }
    return (neededPerm & ownedPerm) > 0;
}

 

你可能感兴趣的:(RocketMQ)