背景

在一些公共场所(比如公交车、地跌、机场等)连接当地的 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)使用最广泛的,协议流程如下(红色部分即认证服务需要实现的接口):

  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>    \n"
                    + "    <head>\n"
                    + "        <meta charset=\"UTF-8\">\n"
                    + "        <title>登录 - Portal</title>\n"
                    + "    </head>\n"
                    + "    <body>\n"
                    + "        <form action=\"http://192.168.1.109:8910/wifidog/login\" method=\"POST\">\n"
                    + "            <input type=\"text\" id=\"username\" name=\"username\" "
                    + "                   placeholder=\"Username\">\n"
                    + "            <input type=\"password\" id=\"password\" name=\"password\" "
                    + "                   placeholder=\"Password\">\n"
                    + "            <button type=\"submit\">登录</button>\n"
                    + "        </form>\n"
                    + "    </body>\n"
                    + "</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("\nrequest [\n  URI=")
                .append(request.getRequestURI())
                .append("\n  method=").append(request.getMethod())
                .append("\n  remoteAddr=").append(request.getRemoteAddr());

        final String queryStr = request.getQueryString();
        if (StringUtils.isNotBlank(queryStr)) {
            reqBuilder.append("\n  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("\n");
        }
        headerBuilder.append("  ]");
        reqBuilder.append("\n  headers=[\n").append(headerBuilder.toString()).append("\n]");

        LOGGER.debug(reqBuilder.toString());
    }
}

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


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

WiFiDog 与 AuthServer的更多相关文章

  1. wifidog auth-server安装配置

  2. wifidog 配置中文说明

    #网关IDGatewayID default#外部网卡ExternalInterface eth0#无线网卡GatewayInterface eth0#无线IPGatewayAddress 192.1 ...

  3. wifidog源码分析 - 初始化阶段

    Wifidog是一个linux下开源的认证网关软件,它主要用于配合认证服务器实现无线路由器的认证放行功能. wifidog是一个后台的服务程序,可以通过wdctrl命令对wifidog主程序进行控制. ...

  4. wifidog 源码初分析(3)-转

    上一篇分析了 接入设备 在接入路由器,并发起首次 HTTP/80 请求到路由器上时,wifidog 是如何将此 HTTP 请求重定向至 auth-server 的流程. 之后 接入设备 的浏览器接收到 ...

  5. wifidog 源码初分析(2)-转

    上一篇分析了接入设备的首次浏览器访问请求如何通过 防火墙过滤规则 重定向到 wifidog 的 HTTP 服务中,本篇主要分析了 wifidog 在接收到 接入设备的 HTTP 访问请求后,如何将此 ...

  6. wifidog 源码初分析(1)-转

    wifidog 的核心还是依赖于 iptables 防火墙过滤规则来实现的,所以建议对 iptables 有了了解后再去阅读 wifidog 的源码. 在路由器上启动 wifidog 之后,wifid ...

  7. OpenWRT使用wifidog实现强制认证的WIFI热点

    首先安装wifidog到OpenWRT的路由器: opkg update opkg install wifidog wifidog依赖下面这些模块: iptables-mod-extra iptabl ...

  8. OpenWrt中wifidog的配置及各节点页面参数

    修改/etc/wifidog.conf, 只需要修改文件的前半部分, 其他都保持默认 GatewayID default GatewayInterface br-lan GatewayAddress ...

  9. wifidog 认证

    首先简介一下什么是Portal认证.Portal认证.通常也会叫Web认证.未认证用户上网时,设备强制用户登录到特定站点,用户能够免费訪问当中的服务.当用户须要使用互联网中的其他信息时,必须在门户站点 ...

随机推荐

  1. Oracle PL/SQL学习之基础篇(1)

    1.PL/SQL,全称Procedure Language/SQL,过程化sql语言 PL/SQL的程序结构 declare --声明部分(包括变量.光标.例外声明) begin --语句序列(DML ...

  2. C 语言调试信息输出宏定义

    C 语言经常在实际的调试过程中,使用最基本的调试方法printf,我们可以使用__FILENAME__.__FUNCTION__.__LINE__,增加自己的输出宏定义: #define DVR_PR ...

  3. Oracle数据库学习(一):虚拟机下Oracle Linux的安装与配置

    这篇博文主要以图片的形式讲述Oracle Linux在虚拟机下的安装与配置 一.前期虚拟机安装ISO文件的配置 1.创建新的虚拟机 2.选择“自定义(高级)”选项,下一步,默认“虚拟机硬件兼容性”或选 ...

  4. JS正则表达式端口号,IP地址

    端口号:65535 正则:/^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6 ...

  5. [转] js画图开发库--mxgraph--[graphlayout-图形布局.html]

    [From] http://chwshuang.iteye.com/blog/1797740 布局变化,下方还有动画效果选项: <!Doctype html> <html xmlns ...

  6. Mac 10.12安装飞鸽传书IPMessager

    说明:这个版本的飞鸽传书不能和Linux的互通,但是可以和Windows的互通,我猜测是协议问题:如果想要互通只能是Mac和Linux同时安装iptux. 下载: (链接: https://pan.b ...

  7. Firefox、Chrome、IE9、IE8、IE7、IE6等浏览器HTTP_USER_AGENT汇总

    Firefox.Chrome.IE9.IE8.IE7.IE6 浏览器HTTP_USER_AGENT汇总 结论:  浏览器 \ OS XP(IE6) XP(IE7) XP(IE8) Win7 x64(I ...

  8. (转)MySQL出现同步延迟有哪些原因?如何解决?

    http://oldboy.blog.51cto.com/2561410/1682147----MySQL出现同步延迟有哪些原因?如何解决? 原文:http://www.zjian.me/mysql/ ...

  9. 【Ubuntu】安装配置apahce

    安装apachesudo apt-get install apache2 安装目录 /etc/apache2配置文件 /etc/apache2/apache2.conf默认网站根目录 /var/www ...

  10. 之前为dd写的一个小的demo(robotium)

    测试类的编写: package com.m1905.dd.mobile; import com.robotium.solo.By; import com.robotium.solo.Solo; imp ...