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函數改成如下
1 check(#mqtt_client{client_id = ClientId, username = Username}, Password, _Opts) -> 2 io:format("Auth Demo: clientId=~p, username=~p, password=~p~n", 3 [ClientId, Username, Password]), 4 if 5 Username == <<"test">> -> 6 ok; 7 true -> 8 error 9 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 函數修改如下
1 check_acl({Client, PubSub, Topic}, _Opts) -> 2 io:format("ACL Demo: ~p ~p ~p~n", [Client, PubSub, Topic]), 3 io:format("~n == ACL ==~n"), 4 if 5 Topic == <<"/World">> -> 6 io:format("allow"), 7 allow; 8 true -> 9 io:format("deny"), 10 deny 11 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
1 ##redis config 2 wunaozai.auth.redis.server = 127.0.0.1:6379 3 wunaozai.auth.redis.pool = 8 4 wunaozai.auth.redis.database = 0 5 ##wunaozai.auth.redis.password = 6 wunaozai.auth.redis.auth_cmd = HMGET mqtt_user:%u password 7 wunaozai.auth.redis.password_hash = plain 8 wunaozai.auth.redis.super_cmd = HGET mqtt_user:%u is_superuser 9 wunaozai.auth.redis.acl_cmd = HGETALL mqtt_acl:%u
增加 priv/emq_auth_redis.schema
1 %% wunaozai.auth.redis.server 2 { 3 mapping, 4 "wunaozai.auth.redis.server", 5 "emq_plugin_wunaozai.server", 6 [ 7 {default, {"127.0.0.1", 6379}}, 8 {datatype, [integer, ip, string]} 9 ] 10 }. 11 12 %% wunaozai.auth.redis.pool 13 { 14 mapping, 15 "wunaozai.auth.redis.pool", 16 "emq_plugin_wunaozai.server", 17 [ 18 {default, 8}, 19 {datatype, integer} 20 ] 21 }. 22 23 %% wunaozai.auth.redis.database = 0 24 { 25 mapping, 26 "wunaozai.auth.redis.database", 27 "emq_plugin_wunaozai.server", 28 [ 29 {default, 0}, 30 {datatype, integer} 31 ] 32 }. 33 34 %% wunaozai.auth.redis.password = 35 { 36 mapping, 37 "wunaozai.auth.redis.password", 38 "emq_plugin_wunaozai.server", 39 [ 40 {default, ""}, 41 {datatype, string}, 42 hidden 43 ] 44 }. 45 46 %% translation 47 { 48 translation, 49 "emq_plugin_wunaozai.server", 50 fun(Conf) -> 51 {RHost, RPort} = 52 case cuttlefish:conf_get("wunaozai.auth.redis.server", Conf) of 53 {Ip, Port} -> {Ip, Port}; 54 S -> case string:tokens(S, ":") of 55 [Domain] -> {Domain, 6379}; 56 [Domain, Port] -> {Domain, list_to_integer(Port)} 57 end 58 end, 59 Pool = cuttlefish:conf_get("wunaozai.auth.redis.pool", Conf), 60 Passwd = cuttlefish:conf_get("wunaozai.auth.redis.password", Conf), 61 DB = cuttlefish:conf_get("wunaozai.auth.redis.database", Conf), 62 [{pool_size, Pool}, 63 {auto_reconnect, 1}, 64 {host, RHost}, 65 {port, RPort}, 66 {database, DB}, 67 {password, Passwd}] 68 end 69 }. 70 71 72 %% wunaozai.auth.redis.auth_cmd = HMGET mqtt_user:%u password 73 { 74 mapping, 75 "wunaozai.auth.redis.auth_cmd", 76 "emq_plugin_wunaozai.auth_cmd", 77 [ 78 {datatype, string} 79 ] 80 }. 81 82 %% wunaozai.auth.redis.password_hash = plain 83 { 84 mapping, 85 "wunaozai.auth.redis.password_hash", 86 "emq_plugin_wunaozai.password_hash", 87 [ 88 {datatype, string} 89 ] 90 }. 91 92 %% wunaozai.auth.redis.super_cmd = HGET mqtt_user:%u is_superuser 93 { 94 mapping, 95 "wunaozai.auth.redis.super_cmd", 96 "emq_plugin_wunaozai.super_cmd", 97 [ 98 {datatype, string} 99 ] 100 }. 101 102 %% wunaozai.auth.redis.acl_cmd = HGETALL mqtt_acl:%u 103 { 104 mapping, 105 "wunaozai.auth.redis.acl_cmd", 106 "emq_plugin_wunaozai.acl_cmd", 107 [ 108 {datatype, string} 109 ] 110 }. 111 112 %%translation 113 { 114 translation, "emq_plugin_wunaozai.password_hash", 115 fun(Conf) -> 116 HashValue = cuttlefish:conf_get("wunaozai.auth.redis.password_hash", Conf), 117 case string:tokens(HashValue, ",") of 118 [Hash] -> list_to_atom(Hash); 119 [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; 120 [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; 121 _ -> plain 122 end 123 end 124 }.
這個時候,dashboard端,可以看到如下信息:

如果遇到特殊情況,有時候,是熱加載插件問題,記住 rm -rf _rel && make clean && make 即可
修改 rebar.confi 增加redis依賴
$ cat rebar.config
1 {deps, [ 2 {eredis, ".*", {git, "https://github.com/wooga/eredis", "master"}}, 3 {ecpool, ".*", {git, "https://github.com/emqtt/ecpool", "master"}} 4 ]}. 5 {erl_opts, [debug_info,{parse_transform,lager_transform}]}.
修改 Makefile 增加redis依賴
增加 include/emq_plugin_wunaozai.hrl 頭文件
1 -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連接池配置。
1 -module(emq_plugin_wunaozai_sup). 2 -behaviour(supervisor). 3 -include("emq_plugin_wunaozai.hrl"). 4 5 %% API 6 -export([start_link/0]). 7 8 %% Supervisor callbacks 9 -export([init/1]). 10 11 start_link() -> 12 supervisor:start_link({local, ?MODULE}, ?MODULE, []). 13 14 init([]) -> 15 {ok, Server} = application:get_env(?APP, server), 16 PoolSpec = ecpool:pool_spec(?APP, ?APP, emq_plugin_wunaozai_cli, Server), 17 {ok, { {one_for_one, 10, 100}, [PoolSpec]} }. 18
創建 emq_plugin_wunaozai_cli.erl 文件, 同樣從emq_auth_redis_cli.erl進行復制然后作修改。
到這里,可以先編譯一下看是否通過,由於Erlang語言不是很熟悉,基本每做一步修改,都進行編譯,防止語法錯誤,否則很難檢查問題。
文件emq_plugin_wunaozai_app.erl 進行修改
1 -module(emq_plugin_wunaozai_app). 2 3 -behaviour(application). 4 5 -include("emq_plugin_wunaozai.hrl"). 6 7 %% Application callbacks 8 -export([start/2, stop/1]). 9 10 start(_StartType, _StartArgs) -> 11 {ok, Sup} = emq_plugin_wunaozai_sup:start_link(), 12 if_cmd_enabled(auth_cmd, fun reg_authmod/1), 13 if_cmd_enabled(acl_cmd, fun reg_aclmod/1), 14 emq_plugin_wunaozai:load(application:get_all_env()), 15 {ok, Sup}. 16 17 stop(_State) -> 18 ok = emqttd_access_control:unregister_mod(auth, emq_auth_demo_wunaozai), 19 ok = emqttd_access_control:unregister_mod(acl, emq_acl_demo_wunaozai), 20 emq_plugin_wunaozai:unload(). 21 22 %% 根據具體配置文件 emq_plugin_wunaozai.conf 是否有auth_cmd 或者 acl_cmd 配置項目來動態加載所屬模塊 23 reg_authmod(AuthCmd) -> 24 SuperCmd = application:get_env(?APP, super_cmd, undefined), 25 {ok, PasswdHash} = application:get_env(?APP, password_hash), 26 emqttd_access_control:register_mod(auth, emq_auth_demo_wunaozai, {AuthCmd, SuperCmd, PasswdHash}). 27 28 reg_aclmod(AclCmd) -> 29 emqttd_access_control:register_mod(acl, emq_acl_demo_wunaozai, AclCmd). 30 31 if_cmd_enabled(Par, Fun) -> 32 case application:get_env(?APP, Par) of 33 {ok, Cmd} -> Fun(Cmd); 34 undefined -> ok 35 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,具體效果怎樣,要后面才知道。
目前我是先搭一下框架,性能優化在后面才會進行考慮。
下一小結主要對上面進行小結,並提供對應的插件代碼
