佐须之男 发布的文章

wifidog源码分析 - 初始化阶段

Wifidog是一个linux下开源的认证网关软件,它主要用于配合认证服务器实现无线路由器的认证放行功能。
wifidog是一个后台的服务程序,可以通过wdctrl命令对wifidog主程序进行控制。
本文解释wifidog在启动阶段所做的初始化主要工作(代码片段1.1)

  • 初始化配置(先将配置结构体初始化为默认值,在读取配置文件修改配置结构体)
  • 初始化已连接客户端列表(如果是通过wdctrl重启wifidog,将会读取之前wifidog的已连接客户端列表 代码片段1.2
    代码片段1.3)
  • 如无特殊情况,分离进程,建立守护进程 (代码片段1.1)
  • 添加多个http请求回调函数(包括404错误回调函数) (见之后章节)
  • 摧毁删除现有的iptables路由表规则 (见之后章节)
  • 建立新的iptables路由表规则 (见之后章节)
  • 启动多个功能线程 (见之后章节)
  • 循环等待客户端连接 (见之后章节)

代码片段1.1:

int main(int argc, char **argv) {

    s_config *config = config_get_config();  //就是返回全局变量config结构体的地址
    config_init();    //初始化全局变量config结构体为默认值

    parse_commandline(argc, argv);    //根据传入参数执行操作(如果参数有-x则会设置restart_orig_pid为已运行的wifidog的pid)

    /* Initialize the config */
    config_read(config->configfile);    //根据配置文件设置全局变量config结构体
    config_validate();    //判断GatewayInterface和AuthServer是否为空,空则无效退出程序。

    /* Initializes the linked list of connected clients */
    client_list_init();    //将已连接客户端链表置空。

    /* Init the signals to catch chld/quit/etc */
    init_signals();    //初始化一些信号

    if (restart_orig_pid) {    //用于restart,如果有已运行的wifidog,先会kill它
        /*
         * We were restarted and our parent is waiting for us to talk to it over the socket
         */
        get_clients_from_parent();    //从已运行的wifidog中获取客户端列表,详见 代码片段1.2

        /*
         * At this point the parent will start destroying itself and the firewall. Let it finish it's job before we continue
         */

        while (kill(restart_orig_pid, 0) != -1) {    //kill已运行的wifidog
            debug(LOG_INFO, "Waiting for parent PID %d to die before continuing loading", restart_orig_pid);
            sleep(1);
        }

        debug(LOG_INFO, "Parent PID %d seems to be dead. Continuing loading.");
    }

    if (config->daemon) {    //创建为守护进程,config->daemon默认值为-1

        debug(LOG_INFO, "Forking into background");

        switch(safe_fork()) {
            case 0: /* child */
                setsid();    //创建新会话,脱离此终端,实现守护进程
                append_x_restartargv();
                main_loop();    //进入主循环(核心代码在此)。
                break;

            default: /* parent */
                exit(0);
                break;
        }
    }
    else {
        append_x_restartargv();
        main_loop();
    }

    return(0); /* never reached */
}

代码片段1.2(获取已启动的wifidog的客户端列表):

void get_clients_from_parent(void) {
    int sock;
    struct sockaddr_un    sa_un;
    s_config * config = NULL;
    char linebuffer[MAX_BUF];
    int len = 0;
    char *running1 = NULL;
    char *running2 = NULL;
    char *token1 = NULL;
    char *token2 = NULL;
    char onechar;
    char *command = NULL;
    char *key = NULL;
    char *value = NULL;
    t_client * client = NULL;
    t_client * lastclient = NULL;

    config = config_get_config();

    debug(LOG_INFO, "Connecting to parent to download clients");

    /* 连接socket */
    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    memset(&sa_un, 0, sizeof(sa_un));
    sa_un.sun_family = AF_UNIX;
    strncpy(sa_un.sun_path, config->internal_sock, (sizeof(sa_un.sun_path) - 1));    //config->internal_sock的值为"/tmp/wifidog.sock"

    /* 连接已启动的wifidog */
    if (connect(sock, (struct sockaddr *)&sa_un, strlen(sa_un.sun_path) + sizeof(sa_un.sun_family))) {
        debug(LOG_ERR, "Failed to connect to parent (%s) - client list not downloaded", strerror(errno));
        return;
    }

    debug(LOG_INFO, "Connected to parent.  Downloading clients");

    LOCK_CLIENT_LIST();

    command = NULL;
    memset(linebuffer, 0, sizeof(linebuffer));
    len = 0;
    client = NULL;
    /* 接收数据,逐个字符接收 */
    /* 数据包格式为 CLIENT|ip=%s|mac=%s|token=%s|fw_connection_state=%u|fd=%d|counters_incoming=%llu|counters_outgoing=%llu|counters_last_updated=%lu\n */
    while (read(sock, &onechar, 1) == 1) {
        if (onechar == '\n') {
            /* 如果接收到末尾('\n'),则转为'\0' */
            onechar = '\0';
        }
        linebuffer[len++] = onechar;

        if (!onechar) {
            /* 以下将数据转化为t_client结构体添加到客户端列表 */
            debug(LOG_DEBUG, "Received from parent: [%s]", linebuffer);
            running1 = linebuffer;
            while ((token1 = strsep(&running1, "|")) != NULL) {
                if (!command) {
                    /* The first token is the command */
                    command = token1;
                }
                else {
                /* Token1 has something like "foo=bar" */
                    running2 = token1;
                    key = value = NULL;
                    while ((token2 = strsep(&running2, "=")) != NULL) {
                        if (!key) {
                            key = token2;
                        }
                        else if (!value) {
                            value = token2;
                        }
                    }
                }

                if (strcmp(command, "CLIENT") == 0) {
                    /* This line has info about a client in the client list */
                    if (!client) {
                        /* Create a new client struct */
                        client = safe_malloc(sizeof(t_client));
                        memset(client, 0, sizeof(t_client));
                    }
                }

                if (key && value) {
                    if (strcmp(command, "CLIENT") == 0) {
                        /* Assign the key into the appropriate slot in the connection structure */
                        if (strcmp(key, "ip") == 0) {
                            client->ip = safe_strdup(value);
                        }
                        else if (strcmp(key, "mac") == 0) {
                            client->mac = safe_strdup(value);
                        }
                        else if (strcmp(key, "token") == 0) {
                            client->token = safe_strdup(value);
                        }
                        else if (strcmp(key, "fw_connection_state") == 0) {
                            client->fw_connection_state = atoi(value);
                        }
                        else if (strcmp(key, "fd") == 0) {
                            client->fd = atoi(value);
                        }
                        else if (strcmp(key, "counters_incoming") == 0) {
                            client->counters.incoming_history = atoll(value);
                            client->counters.incoming = client->counters.incoming_history;
                        }
                        else if (strcmp(key, "counters_outgoing") == 0) {
                            client->counters.outgoing_history = atoll(value);
                            client->counters.outgoing = client->counters.outgoing_history;
                        }
                        else if (strcmp(key, "counters_last_updated") == 0) {
                            client->counters.last_updated = atol(value);
                        }
                        else {
                            debug(LOG_NOTICE, "I don't know how to inherit key [%s] value [%s] from parent", key, value);
                        }
                    }
                }
            }

            /* End of parsing this command */
            if (client) {
                /* Add this client to the client list */
                if (!firstclient) {
                    firstclient = client;
                    lastclient = firstclient;
                }
                else {
                    lastclient->next = client;
                    lastclient = client;
                }
            }

            /* Clean up */
            command = NULL;
            memset(linebuffer, 0, sizeof(linebuffer));
            len = 0;
            client = NULL;
        }
    }

    UNLOCK_CLIENT_LIST();
    debug(LOG_INFO, "Client list downloaded successfully from parent");

    close(sock);
}

代码片段1.3(已启动的wifidog发送客户端列表到新启动的wifidog):
//thread_wdctl_handler(void *arg)函数是wifidog启动后自动创建的控制线程,主要用于与wdctrl进行socket通信,根据wdctrl命令执行不同的操作。这里我们着重讲解的是wdctrl发送restart后wifidog的执行逻辑。

static void *
thread_wdctl_handler(void *arg)
{
    int    fd,
        done,
        i;
    char    request[MAX_BUF];
    ssize_t    read_bytes,
        len;

    debug(LOG_DEBUG, "Entering thread_wdctl_handler....");

    fd = (int)arg;

    debug(LOG_DEBUG, "Read bytes and stuff from %d", fd);

    /* 初始化变量 */
    read_bytes = 0;
    done = 0;
    memset(request, 0, sizeof(request));

    /* 读取命令 */
    while (!done && read_bytes < (sizeof(request) - 1)) {
        len = read(fd, request + read_bytes,
                sizeof(request) - read_bytes);    //读取wdctrl发送的命令

        /* 判断命令正确性 */
        for (i = read_bytes; i < (read_bytes + len); i++) {
            if (request[i] == '\r' || request[i] == '\n') {
                request[i] = '\0';
                done = 1;
            }
        }

        /* Increment position */
        read_bytes += len;
    }

        //判断命令
    if (strncmp(request, "status", 6) == 0) {
        wdctl_status(fd);
    } else if (strncmp(request, "stop", 4) == 0) {
        wdctl_stop(fd);
    } else if (strncmp(request, "reset", 5) == 0) {
        wdctl_reset(fd, (request + 6));
    } else if (strncmp(request, "restart", 7) == 0) {
        wdctl_restart(fd);    //执行wdctl_restart(int afd)函数
    }

    if (!done) {
        debug(LOG_ERR, "Invalid wdctl request.");
                //关闭套接字
        shutdown(fd, 2);
        close(fd);
        pthread_exit(NULL);
    }

    debug(LOG_DEBUG, "Request received: [%s]", request);

        //关闭套接字
    shutdown(fd, 2);
    close(fd);
    debug(LOG_DEBUG, "Exiting thread_wdctl_handler....");

    return NULL;
}


//wdctl_restart(int afd)函数详解
static void
wdctl_restart(int afd)
{
    int    sock,
        fd;
    char    *sock_name;
    struct     sockaddr_un    sa_un;
    s_config * conf = NULL;
    t_client * client = NULL;
    char * tempstring = NULL;
    pid_t pid;
    ssize_t written;
    socklen_t len;

    conf = config_get_config();

    debug(LOG_NOTICE, "Will restart myself");

    /*
     * 准备内部连接socket
     */
    memset(&sa_un, 0, sizeof(sa_un));
    sock_name = conf->internal_sock;    //conf->internal_sock值为"/tmp/wifidog.sock"
    debug(LOG_DEBUG, "Socket name: %s", sock_name);

    if (strlen(sock_name) > (sizeof(sa_un.sun_path) - 1)) {

        debug(LOG_ERR, "INTERNAL socket name too long");
        return;
    }

    debug(LOG_DEBUG, "Creating socket");
    sock = socket(PF_UNIX, SOCK_STREAM, 0);    //建立内部socket套接字

    debug(LOG_DEBUG, "Got internal socket %d", sock);

    /* 如果sock_name文件存在,则删除*/
    unlink(sock_name);

    debug(LOG_DEBUG, "Filling sockaddr_un");
    strcpy(sa_un.sun_path, sock_name); 
    sa_un.sun_family = AF_UNIX;

    debug(LOG_DEBUG, "Binding socket (%s) (%d)", sa_un.sun_path, strlen(sock_name));


    if (bind(sock, (struct sockaddr *)&sa_un, strlen(sock_name) + sizeof(sa_un.sun_family))) {
        debug(LOG_ERR, "Could not bind internal socket: %s", strerror(errno));
        return;
    }

    if (listen(sock, 5)) {
        debug(LOG_ERR, "Could not listen on internal socket: %s", strerror(errno));
        return;
    }

    /*
     * socket建立完成,创建子进程
     */
    debug(LOG_DEBUG, "Forking in preparation for exec()...");
    pid = safe_fork();
    if (pid > 0) {
        /* 父进程 */

        /* 等待子进程连接此socket :*/
        debug(LOG_DEBUG, "Waiting for child to connect on internal socket");
        len = sizeof(sa_un);
        if ((fd = accept(sock, (struct sockaddr *)&sa_un, &len)) == -1){    //接受连接
            debug(LOG_ERR, "Accept failed on internal socket: %s", strerror(errno));
            close(sock);
            return;
        }

        close(sock);

        debug(LOG_DEBUG, "Received connection from child.  Sending them all existing clients");

        /*子进程已经完成连接,发送客户端列表 */
        LOCK_CLIENT_LIST();
        client = client_get_first_client();    //获取第一个客户端
        while (client) {
            /* Send this client */
            safe_asprintf(&tempstring, "CLIENT|ip=%s|mac=%s|token=%s|fw_connection_state=%u|fd=%d|counters_incoming=%llu|counters_outgoing=%llu|counters_last_updated=%lu\n", client->ip, client->mac, client->token, client->fw_connection_state, client->fd, client->counters.incoming, client->counters.outgoing, client->counters.last_updated);
            debug(LOG_DEBUG, "Sending to child client data: %s", tempstring);
            len = 0;
            while (len != strlen(tempstring)) {
                written = write(fd, (tempstring + len), strlen(tempstring) - len);    //发送给子进程
                if (written == -1) {
                    debug(LOG_ERR, "Failed to write client data to child: %s", strerror(errno));
                    free(tempstring);
                    break;
                }
                else {
                    len += written;
                }
            }
            free(tempstring);
            client = client->next;
        }
        UNLOCK_CLIENT_LIST();

        close(fd);

        debug(LOG_INFO, "Sent all existing clients to child.  Committing suicide!");

        shutdown(afd, 2);
        close(afd);


        wdctl_stop(afd);
    }
    else {
        /* 子进程,先关闭资源 */
        close(wdctl_socket_server);
        close(icmp_fd);
        close(sock);
        shutdown(afd, 2);
        close(afd);
        debug(LOG_NOTICE, "Re-executing myself (%s)", restartargv[0]);

        setsid();
        execvp(restartargv[0], restartargv);    //执行外部命令,这里重新启动wifidog

        debug(LOG_ERR, "I failed to re-execute myself: %s", strerror(errno));
        debug(LOG_ERR, "Exiting without cleanup");
        exit(1);
    }
}

小结
客户端列表只有在restart命令中才会执行,实际上流程就是

父wifidog准备socket
父wifidog启动子wifidog
子wifidog连接父wifidog
客户端列表传递
子wifidog终止父wifidog

本文章由 http://www.wifidog.pro/2015/03/17/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-6.html整理编辑,转载请注明出处

wifidog认证wifidog路由接口文档分析wifidog 原理

概述

wifidog是搭建无线热点认证系统的解决方案之一,他比nocat更适合互联网营销思路。目前支持openwrt系统,他实现了路由器和认证服务器的数据交互,在路由器方是用C语言代码,通过wifidog程序和linux iptables防火墙实现接入用户的认证跳转和控制,在认证服务器方是通过php实现用户的认证流程和管理。
优点:有开源代码,可以很方便的搭建认证系统。
缺点:通过iptables方式实现,性能比较差,整体拉低了路由器的数据包处理速度,协议比较繁琐,对认证服务器的造成性能损耗比较大,在安全方面都是明文传输,有一定的安全隐患。

认证流程图:
wifidog-flow-2009.png

网关心跳协议

Wifidog将ping协议作为心跳机制向认证服务器发送当前状态信息。实现认证服务器和每个节点的状态双向健康监测的机制。

请求信息:
http://auth_sever/ping/? gw_id=%s sys_load=%lu sys_memfree=%u sys_load=%.2f wifidog_uptime=%lu

回复格式:
Pong

例子:
GET/ping/? gw_id=001217DA42D2&sys_uptime=742725&sys_memfree=2604&sys_load=0.03&wifidog_uptime

用户状态心跳协议

请求格式:
http://auth_server/auth/? stage= ip= mac= token= incoming= outgoing=

注意:
ip,mac,token为用户的基本信息,incoming/outgoing为用户的连接计数信息。 stage=counter|login|logout,分别表示:已认证,新认证用户,超时需要删除的用户。

回复格式:
Auth:状态码(注意中间冒号和状态码之间有个空格)

状态码:
0-AUTH_DENIED-Userfirewallusersaredeletedandtheuserremoved. 1-AUTH_ALLOWED-Userwasvalid,addfirewallrulesifnotpresent

例子:
GET/auth/?stage=counters&ip=7.0.0.107&mac=00:40:05:5F:44:43&token=4f473ae3ddc5c1c2165f7a0973c57a98&incoming=6031353&outgoing=827770HTTP/1.0 User-Agent:cnrouterwifidog Host:auth.cnrouter.com

跳转协议

对于新连接用户,路由器将其产生的任意url请求通过302重定向到认证平台。
请求格式:
http://auth_server/login/? gw_id= gw_address= gw_port= mac= url=

例子:
GET/login/? gw_id=808100949391&gw_address=192.168.81.1&gw_port=80&mac=aa:bb:cc:dd:cc:ee&url=http://www.sina.com.cn/HTTP/1.0 User-Agent:cnrouterwifidog Host:auth.cnrouter.com

注册协议

请求格式:
http://gw_ip/wifidog/auth? token=

例子:
GET wifidog/auth?token=12312412124 User-Agent:iphone Host:路由器ip 注册请求成功,以307的方式跳转平台的portal/?gw_id=

本文章由 http://www.wifidog.pro/2015/03/17/wifidog%E8%AE%A4%E8%AF%81%E8%B7%AF%E7%94%B1%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3.html整理编辑,转载请注明出处

wifidog认证服务器令牌结构

Token,一般模式
目前连接令牌都是弱实体,直接保存在连接表格里。一些利害关系人喜欢向连接添加特性(限时,稳定性令牌等等)来支持不同的无线社区模式。为了不搬起石头砸自己的脚,我们需要一个数据模式来解决连接处理和重新使用的问题,不只是它退化的情况。
以下是进行此操作的第一部分草稿。

数据模式

  • Token_templates
  • Token_template_id
  • Token_template_network(注:Server-wide令牌不予以支持,但代码会查找你同步的网络的令牌)
  • Token_template_creation_date
  • Token_max_incoming_bytes 如:允许覆盖带宽
  • Token_max_outgoing_bytes 如:允许覆盖带宽
  • Token_max_total_data 如:允许覆盖带宽
  • Token_max_connection_duration: 如:允许限制单独连接的长度
  • Token_max_usage_duration: 如:允许以小时计算出售访问(只有在使用时开始计算)
  • Token_max_wall_clock_duration: 如:允许出售日执照,周执照或月执照(当令牌第一次使用时开始计算)
  • Token_max_age: 如:允许在期满之前设置最长时限(当令牌被发布时开始计算)
  • Token_is _reusable:当期满时创建的令牌还可以重新使用吗?(能常情况下是可以的)

Tokens_template_valid_nodes(遗憾的是,酒店向他们的客户出售24小时访问权,我们考虑到他们的网络也许包括不只一个节点。如果令牌不准进入表格,它就会在网络的任何位置被认为是有效的)

  • Token_template_id
  • Token_valid_at_node

Token_lots:

  • Token_lot_id
  • Tonken_lot_comment:关于lot的自由形态评论
  • Token_lot_creation_date

Lot是被同时发布的令牌组,例如预付卡。
Token_status

  • Token_status
  • Tokens
  • Token_id
  • Token_template_id
  • Token_status:
  • Token_lot_id:参考token_lots
  • Token_creation_date(与连接起始时间不同)
  • Token_issuer:系统里的用户。为所创建令牌负责的用户(不必与使用者一致)
  • Token_owner:可以使用此令牌的用户(如果为空那就可以是任何人)

当建立连接时,token_templates表格中的数值连同网络策略或节点策略一同被使用,目的是用来计算连接表格里的max_data_transfer和expiration_date。这个计算比较贵,但是一旦完成,所有服务器所需要做的validate max_data_transfer和expiration_data几乎都是免费的。

连接(已存在表格中新的或重新定义的字段)

  • Connection_status(被删除了,现在应在token_status中查找)
  • Token_id 现在参考token table
  • Max_total_bytes(token_max_data_transfer—SUM(为此令牌的所有连接传送数据))
  • Max_incoming_bytes 同上
  • Max_outgoing_bytes 同上
  • Expiration_date(MIN(NOW+token_max_connection_duration,NOW+token_max_total_duration-SUM(为此令牌的所有连接传送数据),token_expiration_date))
  • Logout_reason 解释是什么导至连接关闭

如何运作,构思

告诉我我是否错了,但这就是我具体如何看待令牌结构的。
有了这个新的令牌结构,令牌本身在连接进程和管理上发挥的作用远比现在要大。
目前,认证服务器将用户作为是连接的中心点。用户是一个实体,拥有用户名和密码,这代表了一个物理上的人。在无用户模式下这个概念是无效的。例如一个splash_only网络只有一个用户,SPLASH_ONLY_USER并且每个用户之间并没有多少差别,也不能从滥用控制机制获益等等。此外,为此,当目前用户是splash-only用户时,代码需要添加特例脚本。
Token2.0中,令牌将会扮演现在用户的角色:代表物理人来连接到网络,然而用户将仅仅是一个认证方案。
以下是服务器端在登录进程中是如何运作的

  1. 可选的 显示登录页面
  2. 可选的 用户输入他的认证信息(用户/执照,访问代码,NIP,当日密码)
  3. 基于成功认证
      为令牌目的获取认证信息(例如splash-only节点的user_id,代替SPLAHS_ONLY_USER id的应该是MAC地址)
      用此用户/MAC来删除连接
      目前用户已拥有可再用的有效的令牌吗?
        是的,使用此令牌
        没有,此用户可以获得新令牌吗?
          是的,创新一个新令牌并使用
          不能,此用户无法连接,告诉他原因和应该如何操作(滥用违规控制,需要购买新令牌等等)
      为此令牌创建新连接
      根据网络策略为此连接计算数据
      将令牌返回网关

需要以下所有支持:

  • 欢迎页面个性化的简单方法(登录界面:用户/执照,访问代码,路径选择等等)
  • 增加网络/节点管理界面来管理它允许的连接/令牌类型。也许我们想要splash-only节点,用户名/密码和/或访问代码节点的混合网络,对吗?
  • 改变会话对象(并引用它),这样才能引用现在被称为令牌的对象。
  • 编写大量的代码来支持所有一切!(执行是自然而然的事)

本文章由 http://www.wifidog.pro/2015/03/16/wifidog%E8%AE%A4%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BB%A4%E7%89%8C%E7%BB%93%E6%9E%84.html 整理编辑,转载请注明出处

wifidog认证服务器用户角色和权限架构(2)

API
对于大多数开发者来说只有两个问题:权限(定义权限)和安全(进行使用)
这些函数在processAdminUI进行权限检测时经常被使用,因为他们将避免异常的允许用户进行登录或重新尝试操作:

Security::requirePermission(Permission $permission, $target_object, $user=null);  
Security::requireAnyPermissions(Array $permissionsArray, $target_object, $user=null);
Security::requireAllPermissions(Array $permissionsArray, $user=null);

Mirror function的存在可以简单检测用户是否有此权限。一般应用在displayAdminUI(如果指定选项不可得,但用户仍然可以编辑对象的其它部分)。

Security::hasPermission(Permission $permission, $target_object, $user=null);
Security::hasAnyPermissions(Array $permissionsArray, $target_object, $user=null);
Security::hasAllPermissions(Array $permissionsArray, $user=null);

内部执行和临界情况(网关的相互作用)

Security::getObjectsWithPermission(Permission $permission, $user=null);  //TO find an object on which the user has the permissions.  Especially usefull to build lists in menus and select boxes.
Security::hasRole($role, $targetObject, $user);  //user is optional, if unspecified, the current user is used.  User can also be null, meaning that there is no user currently logged-in

主要用于进行报告(还没有执行)

Security::getUsersWithPermission($permission, $targetObject); //Return list of users with this permission
Security::getUsersWithRole($role, $objectId); //Return an array of users with the given role.  If objects_id is null, all users with the specific role are returned, along with an array of objects for which they have this role.  Maybe this function won't actually be implemented, as it's there mostly for reporting and sending notification of hotspots going down.
Security::getPermissions($user);  //returns array of PERMISSION constants for this user.

数据模型
stakeholder_type table:

  • stakeholder_type_id

permission table:

  • permission_id REFERENCES permission_type stakeholder_type_id
  • REFERENCES stakeholder_types

roles table:

  • role_id text NOT NULL,
  • role_description_content_id text,
  • is_system_role bool NOT NULL DEFAULT false,
  • stakeholder_type_id text NOT NULL REFERENCES stakeholder_types,
  • role_creation_date

role_has_permissions:

  • role_id REFERENCES roles
  • permission_id REFERENCES permissions

每个利害关系人类型都会从利害关系人表格那得到一个表格:
利害关系人

  • user_id text NOT NULL
  • role_id text NOT NULL
  • object_id text NOT NULL

这些表格要依据利害关系人类型来命名。例如:对于节点来说,表格名为node_stakeholders:
解决的问题
允许进行的操作实例

  • 允许所有者拥有更多的粒度权限来编辑内容(一些人可以编辑登录,一些人不能,等等)
  • 限制用户访问单独节点(user_can_access_all_nodes,网格权限)
  • Fon_like:用户只能在他的节点在线时才能登录
  • 使用互联网的时间限制

本文章由 http://www.wifidog.pro/2015/03/16/wifidog%E8%AE%A4%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%94%A8%E6%88%B7%E8%A7%92%E8%89%B2%E5%92%8C%E6%9D%83%E9%99%90%E6%9E%B6%E6%9E%84-2.html整理编辑,转载请注明出处