物聯網架構成長之路(6)-EMQ權限控制

1. 前言mysql

  EMQTT屬於一個比較小衆的開源軟件,不少資料不全,很麻煩,不少功能都是靠猜想,還有就是看官方提供的那幾個插件,瞭解。git

2. 說明github

  上一小節的插件 emq_plugin_wunaozairedis

  文件 emq_plugin_wunaozai.erlsql

  這個文件就是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,具體效果怎樣,要後面才知道。

  目前我是先搭一下框架,性能優化在後面纔會進行考慮。

  下一小結主要對上面進行小結,並提供對應的插件代碼

相關文章
相關標籤/搜索