1. 前言
EMQTT属于一个比较小众的开源软件,很多资料不全,很麻烦,很多功能都是靠猜测,还有就是看官方提供的那几个插件,了解。
2. 说明
上一小节的插件 emq_plugin_wunaozai
文件 emq_plugin_wunaozai.erl
这个文件就是Hook钩子设计了,里面默认已经有了。比如在 on_client_connected这个函数下增加一行 io:format()打印,那么,对应每个mqtt客户端连接到服务器都会打印这一行。一开始我还以为验证逻辑写在这里,然后通过判断,返回{stop,Client},最后发现不是的。能到这里,是表示已经连接上了。具体的权限验证是在emq_auth_demo_wunaozai.erl这个文件。
文件 emq_auth_demo_wunaozai.erl
这个文件check函数改成如下
check(#mqtt_client{client_id = ClientId, username = Username}, Password, _Opts) ->
io:format("Auth Demo: clientId=~p, username=~p, password=~p~n",
[ClientId, Username, Password]),
if
Username == <<"test">> ->
ok;
true ->
error
end.
表示mqtt客户端登录到服务器要使用用户名为test。否则无法登录。参考emq_auth_pgsql 和 emq_auth_mysql 并测试,发现这个check会有三种返回结果。
ok. error. ignore.
如果是ok就表示验证通过。但是要注意的是,多种组合权限验证的时候。例如,在我准备设计的验证流程是,先判断redis是否存在对应的帐号/密码,如果没有那么就到Postgresql读取判断是否有对应的帐号密码。假使是处于两个插件的话,单其中一个Redis插件返回ok,那么就不再判断pgsql插件验证了。如果插件返回error,同样也不会判断pgsql插件。只有返回ignore,才会再判断后面的插件。
文件 emq_acl_demo_wunaozai.erl
这个文件check_acl 函数修改如下
check_acl({Client, PubSub, Topic}, _Opts) ->
io:format("ACL Demo: ~p ~p ~p~n", [Client, PubSub, Topic]),
io:format("~n == ACL ==~n"),
if
Topic == <<"/World">> ->
io:format("allow"),
allow;
true ->
io:format("deny"),
deny
end.
表示只可以订阅/World 主题。
基本跟上面原理相同,主要修改check_acl并判断权限,有3中返回。
allow. deny. ignore.
3. Redis 连接测试
主要参考emq_auth_redis 这个插件,写插件之前先安装redis和用redis-cli玩一下emqttd知道的emq_plugin_redis插件。
为了简单,很多配置都省略的,只留一些基本的
增加 etc/emq_plugin_wunaozai.config
##redis config
wunaozai.auth.redis.server = 127.0.0.1:
wunaozai.auth.redis.pool =
wunaozai.auth.redis.database =
##wunaozai.auth.redis.password =
wunaozai.auth.redis.auth_cmd = HMGET mqtt_user:%u password
wunaozai.auth.redis.password_hash = plain
wunaozai.auth.redis.super_cmd = HGET mqtt_user:%u is_superuser
wunaozai.auth.redis.acl_cmd = HGETALL mqtt_acl:%u
增加 priv/emq_auth_redis.schema
%% wunaozai.auth.redis.server
{
mapping,
"wunaozai.auth.redis.server",
"emq_plugin_wunaozai.server",
[
{default, {"127.0.0.1", }},
{datatype, [integer, ip, string]}
]
}. %% wunaozai.auth.redis.pool
{
mapping,
"wunaozai.auth.redis.pool",
"emq_plugin_wunaozai.server",
[
{default, },
{datatype, integer}
]
}. %% wunaozai.auth.redis.database =
{
mapping,
"wunaozai.auth.redis.database",
"emq_plugin_wunaozai.server",
[
{default, },
{datatype, integer}
]
}. %% wunaozai.auth.redis.password =
{
mapping,
"wunaozai.auth.redis.password",
"emq_plugin_wunaozai.server",
[
{default, ""},
{datatype, string},
hidden
]
}. %% translation
{
translation,
"emq_plugin_wunaozai.server",
fun(Conf) ->
{RHost, RPort} =
case cuttlefish:conf_get("wunaozai.auth.redis.server", Conf) of
{Ip, Port} -> {Ip, Port};
S -> case string:tokens(S, ":") of
[Domain] -> {Domain, };
[Domain, Port] -> {Domain, list_to_integer(Port)}
end
end,
Pool = cuttlefish:conf_get("wunaozai.auth.redis.pool", Conf),
Passwd = cuttlefish:conf_get("wunaozai.auth.redis.password", Conf),
DB = cuttlefish:conf_get("wunaozai.auth.redis.database", Conf),
[{pool_size, Pool},
{auto_reconnect, },
{host, RHost},
{port, RPort},
{database, DB},
{password, Passwd}]
end
}. %% wunaozai.auth.redis.auth_cmd = HMGET mqtt_user:%u password
{
mapping,
"wunaozai.auth.redis.auth_cmd",
"emq_plugin_wunaozai.auth_cmd",
[
{datatype, string}
]
}. %% wunaozai.auth.redis.password_hash = plain
{
mapping,
"wunaozai.auth.redis.password_hash",
"emq_plugin_wunaozai.password_hash",
[
{datatype, string}
]
}. %% wunaozai.auth.redis.super_cmd = HGET mqtt_user:%u is_superuser
{
mapping,
"wunaozai.auth.redis.super_cmd",
"emq_plugin_wunaozai.super_cmd",
[
{datatype, string}
]
}. %% wunaozai.auth.redis.acl_cmd = HGETALL mqtt_acl:%u
{
mapping,
"wunaozai.auth.redis.acl_cmd",
"emq_plugin_wunaozai.acl_cmd",
[
{datatype, string}
]
}. %%translation
{
translation, "emq_plugin_wunaozai.password_hash",
fun(Conf) ->
HashValue = cuttlefish:conf_get("wunaozai.auth.redis.password_hash", Conf),
case string:tokens(HashValue, ",") of
[Hash] -> list_to_atom(Hash);
[Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)};
[Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)};
_ -> plain
end
end
}.
这个时候,dashboard端,可以看到如下信息:
如果遇到特殊情况,有时候,是热加载插件问题,记住 rm -rf _rel && make clean && make 即可
修改 rebar.confi 增加redis依赖
$ cat rebar.config
{deps, [
{eredis, ".*", {git, "https://github.com/wooga/eredis", "master"}},
{ecpool, ".*", {git, "https://github.com/emqtt/ecpool", "master"}}
]}.
{erl_opts, [debug_info,{parse_transform,lager_transform}]}.
修改 Makefile 增加redis依赖
增加 include/emq_plugin_wunaozai.hrl 头文件
-define(APP, emq_plugin_wunaozai).
复制emq_auth_redis/src/emq_auth_redis_config.erl 这个文件到我们的插件中,然后修改文件名和对应的一些内容。
-module ...
-include ...
keys() -> ...
为每个文件都加上-include (“emq_plugin_wunaozai.hrl”).
文件emq_plugin_wunaozai_sup.erl 要在后面增加redis连接池配置。
-module(emq_plugin_wunaozai_sup).
-behaviour(supervisor).
-include("emq_plugin_wunaozai.hrl"). %% API
-export([start_link/]). %% Supervisor callbacks
-export([init/]). start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) ->
{ok, Server} = application:get_env(?APP, server),
PoolSpec = ecpool:pool_spec(?APP, ?APP, emq_plugin_wunaozai_cli, Server),
{ok, { {one_for_one, , }, [PoolSpec]} }.
创建 emq_plugin_wunaozai_cli.erl 文件, 同样从emq_auth_redis_cli.erl进行复制然后作修改。
到这里,可以先编译一下看是否通过,由于Erlang语言不是很熟悉,基本每做一步修改,都进行编译,防止语法错误,否则很难检查问题。
文件emq_plugin_wunaozai_app.erl 进行修改
-module(emq_plugin_wunaozai_app). -behaviour(application). -include("emq_plugin_wunaozai.hrl"). %% Application callbacks
-export([start/, stop/]). start(_StartType, _StartArgs) ->
{ok, Sup} = emq_plugin_wunaozai_sup:start_link(),
if_cmd_enabled(auth_cmd, fun reg_authmod/),
if_cmd_enabled(acl_cmd, fun reg_aclmod/),
emq_plugin_wunaozai:load(application:get_all_env()),
{ok, Sup}. stop(_State) ->
ok = emqttd_access_control:unregister_mod(auth, emq_auth_demo_wunaozai),
ok = emqttd_access_control:unregister_mod(acl, emq_acl_demo_wunaozai),
emq_plugin_wunaozai:unload(). %% 根据具体配置文件 emq_plugin_wunaozai.conf 是否有auth_cmd 或者 acl_cmd 配置项目来动态加载所属模块
reg_authmod(AuthCmd) ->
SuperCmd = application:get_env(?APP, super_cmd, undefined),
{ok, PasswdHash} = application:get_env(?APP, password_hash),
emqttd_access_control:register_mod(auth, emq_auth_demo_wunaozai, {AuthCmd, SuperCmd, PasswdHash}). reg_aclmod(AclCmd) ->
emqttd_access_control:register_mod(acl, emq_acl_demo_wunaozai, AclCmd). if_cmd_enabled(Par, Fun) ->
case application:get_env(?APP, Par) of
{ok, Cmd} -> Fun(Cmd);
undefined -> ok
end.
4. 简单验证一下帐号
通过上面的简单配置,集成redis模块基本就好了,接下来就是比较重要的业务逻辑判断了。这一步主要是在emq_auth_demo_wunaozai.erl 文件写下帐号密码判断。同理主要还是参考emq_auth_redis.erl
以上对应三部分,第一部分是Redis缓存中存在指定的帐号密码,第二部分是进行简单的验证,第三部分是打印的日志,一开始用错误的帐号密码进行登录,后面使用正确的帐号密码进行登录,以上,验证通过,可以通过Redis缓存信息进行帐号密码验证。
客户端测试工具的话,可以用DashBoard上的WebSocket连接测试,也可以在这里下载 https://repo.eclipse.org/content/repositories/paho-releases/org/eclipse/paho/org.eclipse.paho.ui.app/ ,一个桌面端程序。
测试的时候,建议用这个桌面端程序,WS连接的那个,有时候订阅不成功也提示订阅成功,会很麻烦。
同时好像还有一个问题,就是在采用Redis进行验证是,EMQ默认会开启ACL缓存,就是说,一个MQTT设备的一次新Connect,第一次才会去读取ACL,进行判断,后面就不会再进行ACL判断了。在测试时,可以关闭cache, 在./etc/emq.conf 文件下 mqtt.cache_acl = true 改为 mqtt.cache_acl = false ,这样每次pub/sub 都会读取Redis进行ACL判断。这个功能有好有坏,根据业务取舍。https://github.com/emqtt/emqttd/pull/764
个人想法,如果是安全性要求不高的局域网控制,是可以开启cache_acl的,如果是安全性要求较高的,这个选项就不开启了。这样性能会有所下降,如果是采用传统的关系型数据库进行ACL判断,每次pub/sub信息都会读取数据库,物联网下,可能不太现实,这里我是准备用Redis作为ACL Cache,具体效果怎样,要后面才知道。
目前我是先搭一下框架,性能优化在后面才会进行考虑。
下一小结主要对上面进行小结,并提供对应的插件代码
物联网架构成长之路(6)-EMQ权限控制的更多相关文章
-
物联网架构成长之路(7)-EMQ权限验证小结
1. 前言 经过前面几小节,讲了一下插件开发,这一小节主要对一些代码和目录结构进行讲解,这些都是测试过程中一些个人经验,不一定是官方做法.而且也有可能会因为版本不一致导致差异. 2. 目录结构 这个目 ...
-
物联网架构成长之路(31)-EMQ基于HTTP权限验证
看过之前的文章就知道,我之前是通过搞插件,或者通过里面的MongoDB来进行EMQ的鉴权登录和权限验证.但是前段时间发现,还是通过HTTP WebHook 方式来调用鉴权接口比较适合实际使用.还是实现 ...
-
物联网架构成长之路(33)-EMQ数据存储到influxDB
一.前言 时隔一年半,技术变化特别快,学习也要跟上才行.以前写过EMQ数据转存问题,当时用了比较笨的方法,通过写插件的方式,把MQTT里面的数据发送到数据库进行存储.当时也是为了学习erlang和em ...
-
物联网架构成长之路(3)-EMQ消息服务器了解
1. 了解 物联网最基础的就是通信了.通信协议,物联网协议好像有那么几个,以前各个协议都有优劣,最近一段时间,好像各大厂商都采用MQTT协议,所以我也不例外,不搞特殊,采用MQTT协议,选定了协议,接 ...
-
物联网架构成长之路(4)-EMQ插件创建
1. 说明 以下用到的知识,是建立在我目前所知道的知识领域,以后如果随着知识的拓展,不一定会更新内容.由于不是EMQ公司的人,EMQ的文档又很少,很多知识点都是靠猜的.2. 一些资料 架构设计 htt ...
-
物联网架构成长之路(5)-EMQ插件配置
1. 前言 上一小结说了插件的创建,这一节主要怎么编写代码,以及具体流程之类的.2. 增加一句Hello World 修改 ./deps/emq_plugin_wunaozai/src/emq_plu ...
-
物联网架构成长之路(25)-Docker构建项目用到的镜像1
0. 前言 现在项目处于初级阶段,按照规划,先构建几个以后可能会用到的Image,并上传到阿里云的Docker仓库.以后博客中用到的Image,大部分都会用到这几个基础的Image,构建一个简单的物联 ...
-
物联网架构成长之路(11)-Redis缓存主从复制
1. 说明 在我的物联网平台框架框架中,会用到Redis这个中间件.作为EMQ权限认证的缓存.https://www.cnblogs.com/think-in-java/p/5123884.html ...
-
物联网架构成长之路(40)-Bladex开发框架入门
0. 前言 前一小节,讲了如何入门,这里就简单讲一下如何自定义查询和权限控制配置. 1. 配置多租户 如果要启用该表的多租户功能,需要在application.yml 这里配置. 2. 配置模糊匹配 ...
随机推荐
-
【bzoj2456】 mode
http://www.lydsy.com/JudgeOnline/problem.php?id=2456 (题目链接) 只看了一眼,直觉便告诉我这是水题.于是跟某码农打赌说10分钟做出来叫爸爸,结果输 ...
-
Effective Java 34 Emulate extensible enums with interfaces
Advantage Disadvantage Enum types Clarity Safety Ease of maintenance. None extensibility Typesafe en ...
-
MYSQL router 自动均衡负载
配制文件: /etc/mysqlrouter/mysqlrouter.ini [DEFAULT] logging_folder = /var/log/mysql-router plugin_folde ...
-
《深入剖析Tomcat》阅读(三)
这里要介绍下Tomcat的一个重要设计方法,Catalina设计方式. Servlet容器是一个复杂系统,但是,它有三个基本任务,对每个请求,servlet容器会为其完成以下三个操作: 1.创建一个R ...
-
寻找单向链表的倒数第k个节点
题目: 输入一个单向链表,输出这个单向链表的倒数第k个节点 template<class T> class ListNode { public: T Data; ListNode<T ...
-
Tiny6410之UART裸机驱动
UART简介: UART(Universal Asynchronous Receiver and Transmitter)通用异步收发器(异步串行通信口),是一种通用的数据通信协议,它包括了RS232 ...
-
laypage 物理分页与逻辑分页实例
前言 以下介绍摘自 layui官网laypage layPage 致力于提供极致的分页逻辑,既可轻松胜任异步分页,也可作为页面刷新式分页.自 layui 2.0 开始,无论是从核心代码还是API设计, ...
-
opencv 图像矫正
四个坐标系的转换:https://blog.csdn.net/humanking7/article/details/44756073 标定和矫正:https://blog.csdn.net/u0134 ...
-
Spark学习之第一个程序打包、提交任务到集群
1.免秘钥登录配置: ssh-keygen cd .ssh touch authorized_keys cat id_rsa.pub > authorized_keys chmod 600 au ...
- ViewGroup