• WiFiDog 与 AuthServer


    背景

    在一些公共场所(比如公交车、地跌、机场等)连接当地的 WiFi 时会弹出一个验证表单,输入验证信息(比如短信验证码)后就能够通过该 WiFi 联网。

    本文将介绍通过 OpenWrt WiFiDog 来实现这个认证过程,认证服务器(WiFiDog Captive Portal/AuthServer)由 Java 编写。

    WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,它主要提供如下功能:

    1. 位置相关的内容递送(比如不同的接入点可以投递不同的广告)
    2. 用户认证和授权(认证方式可以通过短信,或者是基础第三方开放平台,比如 QQ、微信等,授权可通过流控实现)
    3. 集中式网络监控(在认证服务器上可以获得各个 WiFi 热点上用户的流量使用情况)

    WiFiDog 在架构上分为两个部分:

    • Gateway:即安装在路由器上的 WiFiDog 软件
    • Captive Portal:即认证服务器(AuthServer),是 Web HTTP 服务,独立部署在公网上

    为了启用认证功能,我们需要对 WiFiDog 进行一点配置,修改配置文件 /etc/wifidog.conf,找到 AuthServer,并取消其中的几行注释:

    AuthServer {               
        Hostname 192.168.1.109 
    # SSLAvailable             
    # SSLPort                  
        HTTPPort 8910          
    # Path                     
    # LoginScriptPathFragment  
    # PortalScriptPathFragment 
    # MsgScriptPathFragment    
    # PingScriptPathFragment   
    # AuthScriptPathFragment   
    }
    

    为了简便,其他的配置项我们都用默认的。修改后重启 WiFiDog:/etc/init.d/wifidog restart

    认证协议

    WiFiDog v1 协议是目前(2016)使用最广泛的,协议流程如下(红色部分即认证服务需要实现的接口):

    WiFiDog认证流程

    1. GW(Gateway) 会定时调用 AS(AuthServer) 的 ping 接口来检查 AS 是否在线
      1.1. AS 在响应 body 里写入 “Pong”
    2. 用户连上 GW 后,通过浏览器发出对某站点的请求
      2.1. GW 收到请求后发现用户没有认证过,则重定向用户到 AS 的 /login
    3. 浏览器请求 AS 的 /login
      3.1. AS 返回验证表单 HTML
    4. 用户填写验证表单后提交 AS
      4.1. AS 验证通过后生成 token 并重定向用户到 GW
    5. 浏览器带 token 请求 GW 的验证接口
      5.1. GW 获取用户 token 后再请求 AS /auth 接口
      5.2. AS 验证 token 有效性,成功则返回 “Auth: 1”(注意空格),验证通过后 GW 会定时请求 AS 的 /auth 接口来检查 token 有效性,期间会带上用户的流量数据
      5.3. GW 重定向用户到 AS /portal
    6. 浏览器请求 GW 的 /portal 接口
      6.1 AS 重定向用户到 2 中指定的某站点(也可以按需进行广告投放)

    报文详解

    基于 WiFiDog 1.2.1 整理,192.168.1.1 是路由 IP,192.168.1.109 同时是客户端和 AuthServer 的 IP。

    ping

    request [
      URI=/wifidog/ping/
      method=GET
      remoteAddr=192.168.1.1
      queryStr=gw_id=100D7F6F25F5&sys_uptime=7265&sys_memfree=95256&sys_load=0.03&wifidog_uptime=61
      headers=[
        user-agent=WiFiDog 1.2.1
        host=192.168.1.109
      ]
    ]

    GW 将系路由标识、负载情况传给 AuthSer,可以基于这些数据做监控。

    login

    request [
      URI=/wifidog/login/
      method=GET
      remoteAddr=192.168.1.109
      queryStr=gw_address=192.168.1.1&gw_port=2060&gw_id=100D7F6F25F5&ip=192.168.1.109&mac=64:00:6a:5d:0a:23&url=http%3A%2F%2Ffangstar.net%2F
      headers=[
        host=192.168.1.109:8910
        connection=keep-alive
        upgrade-insecure-requests=1
        user-agent=Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36
        accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
        accept-encoding=gzip, deflate, sdch
        accept-language=zh-CN,zh;q=0.8,da;q=0.6,en;q=0.4,id;q=0.2,ja;q=0.2,sq;q=0.2,zh-TW;q=0.2,en-US;q=0.2
        cookie=JSESSIONID=3AE20A3B0636E2F7DB7247AD055491FE
      ]
    ]

    查询参数中的 url 是用户要访问的目标站点。

    auth

    request [
      URI=/wifidog/auth/
      method=GET
      remoteAddr=192.168.1.1
      queryStr=stage=login&ip=192.168.1.109&mac=64:00:6a:5d:0a:23&token=22&incoming=0&outgoing=0&gw_id=100D7F6F25F5
      headers=[
        user-agent=WiFiDog 1.2.1
        host=192.168.1.109
      ]
    ]
    • stage:第一次认证是 login,后续定时轮询是 counters
    • incoming/outgoing:用户流量

    portal

    request [
      URI=/wifidog/portal/
      method=GET
      remoteAddr=192.168.1.109
      queryStr=gw_id=100D7F6F25F5
      headers=[
        host=192.168.1.109:8910
        connection=keep-alive
        cache-control=max-age=0
        upgrade-insecure-requests=1
        user-agent=Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36
        accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
        referer=http://192.168.1.109:8910/wifidog/login/?gw_address=192.168.1.1&gw_port=2060&gw_id=100D7F6F25F5&ip=192.168.1.109&mac=64:00:6a:5d:0a:23&url=http%3A%2F%2Ffangstar.net%2F
        accept-encoding=gzip, deflate, sdch
        accept-language=zh-CN,zh;q=0.8,da;q=0.6,en;q=0.4,id;q=0.2,ja;q=0.2,sq;q=0.2,zh-TW;q=0.2,en-US;q=0.2
        cookie=JSESSIONID=3AE20A3B0636E2F7DB7247AD055491FE
      ]
    ]

    Java 实现

    项目基于 Spring Boot 进行实现,用 maven 构建,主要代码如下:

    /*
     * Copyright (c) 2016, fangstar.com
     *
     * All rights reserved.
     */
    package net.fangstar.wifi.portal.controller;
    
    import java.io.PrintWriter;
    import java.util.Enumeration;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import net.fangstar.wifi.portal.Server;
    import org.apache.commons.lang.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    /**
     * WiFiPortal 控制器.
     *
     * @author <a href="http://88250.b3log.org">Liang Ding</a>
     * @version 1.0.0.0, Aug 1, 2016
     * @since 1.0.0
     */
    @Controller
    @RequestMapping("/wifidog")
    public class WiFiPortalController {
    
        /**
         * Logger.
         */
        private static final Logger LOGGER = LoggerFactory.getLogger(WiFiPortalController.class);
    
        /**
         * WiFiDog 网关地址.
         */
        private static final String GATEWAY_ADDR = Server.CONF.getString("gateway.addr");
    
        /**
         * Test token.
         */
        private static final String TOKEN = "22";
    
        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public void showLogin(final HttpServletRequest request, final HttpServletResponse response) {
            logReq(request);
    
            try (final PrintWriter writer = response.getWriter()) {
                final HttpSession session = request.getSession();
                String visitURL = (String) session.getAttribute("url");
                if (StringUtils.isBlank(visitURL)) {
                    visitURL = request.getParameter("url");
                }
                session.setAttribute("url", visitURL);
    
                writer.write("<html>    
    "
                        + "    <head>
    "
                        + "        <meta charset="UTF-8">
    "
                        + "        <title>登录 - Portal</title>
    "
                        + "    </head>
    "
                        + "    <body>
    "
                        + "        <form action="http://192.168.1.109:8910/wifidog/login" method="POST">
    "
                        + "            <input type="text" id="username" name="username" "
                        + "                   placeholder="Username">
    "
                        + "            <input type="password" id="password" name="password" "
                        + "                   placeholder="Password">
    "
                        + "            <button type="submit">登录</button>
    "
                        + "        </form>
    "
                        + "    </body>
    "
                        + "</html>");
    
                writer.flush();
            } catch (final Exception e) {
                LOGGER.error("Write response failed", e);
            }
        }
    
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public void login(final HttpServletRequest request, final HttpServletResponse response) {
            logReq(request);
    
            try {
                response.sendRedirect(GATEWAY_ADDR + "/wifidog/auth?token=" + TOKEN);
            } catch (final Exception e) {
                LOGGER.error("Write response failed", e);
            }
        }
    
        @RequestMapping(value = "/auth", method = {RequestMethod.POST, RequestMethod.GET})
        public void auth(final HttpServletRequest request, final HttpServletResponse response) {
            logReq(request);
    
            try (final PrintWriter writer = response.getWriter()) {
                final String token = request.getParameter("token");
                if (TOKEN.equals(token)) {
                    writer.write("Auth: 1");
                } else {
                    writer.write("Auth: 0");
                }
    
                writer.flush();
            } catch (final Exception e) {
                LOGGER.error("Write response failed", e);
            }
        }
    
        @RequestMapping(value = "portal", method = {RequestMethod.POST, RequestMethod.GET})
        public void portal(final HttpServletRequest request, final HttpServletResponse response) {
            logReq(request);
    
            try {
                final String visitURL = (String) request.getSession().getAttribute("url");
    
                response.sendRedirect(visitURL);
            } catch (final Exception e) {
                LOGGER.error("Write response failed", e);
            }
        }
    
        @RequestMapping(value = "ping", method = {RequestMethod.POST, RequestMethod.GET})
        public void ping(final HttpServletRequest request, final HttpServletResponse response) {
            logReq(request);
    
            try (final PrintWriter writer = response.getWriter()) {
                writer.write("Pong");
                writer.flush();
            } catch (final Exception e) {
                LOGGER.error("Write response failed", e);
            }
        }
    
        private void logReq(final HttpServletRequest request) {
            final StringBuilder reqBuilder = new StringBuilder("
    request [
      URI=")
                    .append(request.getRequestURI())
                    .append("
      method=").append(request.getMethod())
                    .append("
      remoteAddr=").append(request.getRemoteAddr());
    
            final String queryStr = request.getQueryString();
            if (StringUtils.isNotBlank(queryStr)) {
                reqBuilder.append("
      queryStr=").append(queryStr);
            }
    
            final StringBuilder headerBuilder = new StringBuilder();
            final Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                final String headerName = headerNames.nextElement();
                final String headerValue = request.getHeader(headerName);
    
                headerBuilder.append("    ").append(headerName).append("=").append(headerValue).append("
    ");
            }
            headerBuilder.append("  ]");
            reqBuilder.append("
      headers=[
    ").append(headerBuilder.toString()).append("
    ]");
    
            LOGGER.debug(reqBuilder.toString());
        }
    }

    完整项目代码已经在 GitHub 上开源,欢迎大家关注 wifidog-java-portal


    PS:欢迎加入开源技术 Q 群 13139268,让学习和分享成为一种习惯!

  • 相关阅读:
    qt鼠标事件总结(坐标,跟踪,点击判断)
    从零开始学C++之RTTI、dynamic_cast、typeid、类与类之间的关系uml
    前端篇: 前端演进史
    找一款防文件或文件夹误删除,移动,修改的软件
    yyyy
    盘点我用过的那些网盘(那些年,我们一起玩的网盘)
    Win8/8.1/10获得完整管理员权限的方法
    [置顶] IT老男人读《因为痛,所以叫青春》
    dddd
    用JUNCTION映射文件夹内容 解决多系统跑同一个虚拟机而共享文件夹路径不同的问题
  • 原文地址:https://www.cnblogs.com/lanzhi/p/6467787.html
Copyright © 2020-2023  润新知