碎碎念
花了一些时间,做了一款可以用手机来开门的门锁,从原理到最终的开发实现绝大部分工作都是我一人完成,一部分工作得到了老师的帮助,以及在赶项目的时候得到了@Leafer学长的支持与帮助,感谢。
其实这个想法从去年的六月份就已经提出来了,只是拖到了今年二月份才有机会实现它。这篇文章算是对这整个制作过程的一个记录,也算是一个总结吧。附上一张这个项目参加本地TEDx活动展示的照片。
注意,由于这个项目要拿出去比个赛,所以 对于这篇文章的所有内容,包括但不仅限于文字、图片、思路、代码(开源库除外)我均保留所有权利,并对侵权行为追究法律责任。
这么说来好像并没有什么人有空来侵权╮ (. ❛ ᴗ ❛.) ╭
但是这篇文章主体仍遵循知识共享-4.0协议,你可以在注明这篇文章的链接并且非商用的情况下自由地转载这篇文章。
扯远了_(:з」∠)_下面开始切入正题。
原理
这个项目最初的想法来源于我看到市面上一些指纹解锁门锁非常昂贵,动辄上千元,由此想到可以把现在都集成在门锁上的指纹识别部分移到手机上。这样,利用手机的强大能力,可以大大减少门锁成本,或许还能提高安全性以及可拓展性。
所以原理其实也很简单。一共三个设备构成完整流程:手机,门锁,以及一台公网上的服务器。手机与门锁均联网。
懒得画流程图了,我尽量说得简单点。详细技术细节看下一节吧。
首先在手机上选择要打开的门锁,随后输入该门锁的密码。服务器验证通过后用一些参数生成一串密钥,MD5之后扔给手机。
手机收到该密钥后以此为内容生成一个二维码,在屏幕上显示出来。门锁利用摄像头扫描这个二维码,获得密钥后与扔给服务器进行比对。如果和此前MD5后的密钥一致,服务器就通知门锁开门。
这种方案有一些优点。比如可拓展性强,后期可以拓展多种解锁方式,如密码,图案,或者最初设想的指纹,甚至面部识别或者两步验证。同时通过一些手段也可以保证安全性。重点是第一台原型的成本也只有500元左右,大大减少了成本。
在实现这一方案的过程中,我看到了别人做出的有一些相似的方案,是固定二维码贴在门锁旁边,手机扫码解锁。我想这样会带来与共享单车一样的问题:二维码一旦被篡改就无法解锁,甚至会带来危险。由于这一点,最后我坚持了动态生成二维码,由门锁扫描的方案。
实现过程
当然用单片机。由于要联网,还要扫描二维码,我选择了树莓派作为门锁的核心设备。
↑超乱的灵魂走线...
然后在本来最纠结的锁芯方面有幸得到了老师的帮助,没有用电磁锁,而用了一个带电机的传统锁。唯一缺点就是用钥匙开锁时转动阻力实在太大,而且电机驱动稍稍慢了点。凑合着用吧。
在手机如何与门锁通信本来也非常纠结。最初的方案其实是用声波进行通信。手机把密钥转换成声波播放,门锁进行识别。
最后发现这一方案有一些限制。我使用了http://rest.sinaapp.com这一开源库,但它的文档实在有些简单了。好不容易折腾出来,却无奈发现在一些环境下效果实在不好。最终放弃,想了半天决定使用二维码。
关于在手机上用什么技术实现,按原本指纹识别的方案是需要APP的。然而不会写。所以转而使用已经足够强大的HTML5,美其名曰“轻量优雅”。解锁方案就先改成密码解锁和图案解锁(类安卓),反正只是个DEMO嘛。
在安全方面,我对解锁的核心,即解锁密钥做了诸多限制。首先每一把门锁在某一刻只能有一串密钥,且每一串密钥的存活周期为一分钟,过期即失效。
同时我使用了账户登录制度,密钥归档于各个账户下。当账号登录时,前端网页就会以非常密集的频率重复发送简短的请求(我把它称为伪·心跳包)。一旦一个账号一段时间没有发送该请求,后端就会让该账户下的所有密钥失效。
以及,我在服务器上开启了HTTPS和HSTS,同时使用了更安全的ECC证书,配置了更安全的加密算法。
而且,服务器端会拒绝所有非浏览器请求(虽然可以模拟,但毕竟多防一点是一点嘛)。
我还在前段页面偷偷摸摸埋了几个地雷触发点。现在先收集点数据,下次有空了加上区分人机行为的功能好了(然而现在还没有2333)。
还有,门锁端与服务器的通信采用了与前端不同的域名(虽然是同一台服务器)以及自定义的UA,还是那句话,多防一点是一点。通过这些制度,我把我能想到的所有破解方式全部ban掉了。当然,如果你能发现什么漏洞,欢迎告诉我,感谢!
当然肯定不会忘了SQL注入和XSS漏洞,至少尽力了。
然后就没有什么大问题了。于是上手。
开发分成三部分。一部分是服务器上的后端和手机上的前端,一部分是门锁上的程序,还有是硬件布局。
先说前端和后端吧。其实有了思路也很简单了。后端用了PHP 7,开发简单而且据说性能不错。伪·心跳包和过期密钥处理上用了Python,一个死循环处理数据库而已。前端用AJAX实现前后端分离,而且保证了全部流程只刷新一次页面(登录需要刷新。虽然可以不刷新,但是懒)。本来想用这个项目练练vue的,结果发现来不及了。先写死了代码,以后再说吧。
前端用了一个非常棒的开源项目MDUI(http://mdui.org),模拟了Material Design效果,配上个Frameless的浏览器简直就 像 是个APP。
对于如何在解锁完成后在手机上有所反馈的问题,直接用轮询服务器的方式暴力解决。正好有伪·心跳包,就搭了顺风车,用伪·心跳包查询是否解锁。
前端后端就这么不负责任地扯完了。然后是门锁上的程序。
更简单,一个死循环而已。读取一个超声波传感器的值,如果发现有手机在门锁前面(超声波传感器读取距离变小),就联系服务器,如果有密钥待验证,就用与超声波传感器在同一平面内的摄像头拍一张照片。如果没有读取到二维码就进入下一次循环,反之读取二维码与服务器验证。如果正确就叫电机开门,延时一会儿再锁门,然后进入下个循环。
特写来一发。
其实本来是想用循环检查服务器上有没有密钥,有的话再拍照的,然后发现这对服务器来说有点累2333所以加上了超声波传感器。
最后来说说硬件布局。
一定会有人发现头图里面有一块Arduino板,然后奇怪地发现上面扯了这么多却只字未提Arduino。事实上我真的没有用到Arduino的计算能力,纯粹是因为电机要额外供电而我手头没有可以用跳线供电的线,所以机智地用Arduino把5V输入转换成了跳线接口。
你看只连了电源输出↓
还有一些跳线也看起来很奇怪,它们连到了线路板的螺丝孔里...那是为了把这些模块固定到面包板上。
啊...我都佩服我自己的机智(*/ω\*)。
剩下的结构其实也很简单了,只是线一多就乱了。主要是树莓派控制HC-SR04超声波传感器,电机以及摄像头,Arduino板给电机供电。摄像头为了保持小巧及低成本,用了排线的版本。
然而还有坑。
是直流电机的问题。作为一个如假包换的直流电机,这个电机通过切换正负极来改变旋转方向。所以要用H桥。
(图源网络)
很明显,切换不同对角线上开关的闭合就能改变中间电机两端的正负极。
但是无论是H桥芯片还是H桥模块我都没有_(:з」∠)_
但是正好手头有一个双路继电器模块。
于是拿它做了一个大号简易版H桥。原理差不多。
唯一的毛病就是如果一不小心两边同时闭合就会Boom。所以改了连接和代码。首先把接地一端都接在继电器常闭端,同时在代码逻辑上总是先把两边同时切到接地端再进行其他操作。这样就尽量避免了短路问题。
越过了这些坑,我最后对整个布局改进了一下。其实就是加了个红色LED作为视觉反馈,顺便也方便了开发时勘误。
到这一步,整个项目就基本上完成了。这是树莓派端的完整Python脚本:
# -*- coding:utf-8 -*- import time import RPi.GPIO as GPIO import urllib.request from PIL import Image import os, signal, subprocess #初始化 GPIO.setmode(GPIO.BCM) #超声波 GPIO.setup(24, GPIO.OUT) GPIO.setup(23, GPIO.IN) #LED GPIO.setup(25, GPIO.OUT) #继电器 GPIO.setup(8, GPIO.OUT) GPIO.setup(21, GPIO.OUT) #超声波读取距离函数 def checkdist(): GPIO.output(25, True) time.sleep(0.03) GPIO.output(25, False) GPIO.output(24, GPIO.HIGH) time.sleep(0.000015) GPIO.output(24,GPIO.LOW) while not GPIO.input(23): pass t1 = time.time() while GPIO.input(23): pass t2 = time.time() return (t2-t1)*340/2 #开、关门函数 def downPower(): GPIO.output(8, False) GPIO.output(21, False) def openDoor(): GPIO.output(21, True) time.sleep(3) GPIO.output(21, False) def closeDoor(): GPIO.output(8, True) time.sleep(3) GPIO.output(8, False) #二维码读取函数 def lesen(): #拍摄一幅图像 os.system("raspistill -w 640 -h 480 -o /home/pi/cameraqrc/image.jpg -t 100 ") print ("raspistill finished") #处理图像以方便读取 im = Image.open("/home/pi/cameraqrc/image.jpg").rotate(180).convert("L").save("/home/pi/cameraqrc/image-1.jpg") GPIO.output(25, True) time.sleep(0.1) GPIO.output(25, False) #读取二维码 zbarcam=subprocess.Popen("zbarimg --raw /home/pi/cameraqrc/image-1.jpg", stdout=subprocess.PIPE, shell=True, preexec_fn=os.setsid) qrcodetext=zbarcam.stdout.readline() return qrcodetext #主函数 def havePhone(): urlc = "https://xxx.flyhigher.top/checkkey/?lock_id=test001" #才不告诉你们真实地址嘞 content = urllib.request.urlopen(urlc).read().decode("utf-8") print(content) if content == "true": result=lesen().strip().decode("utf-8") print(result) if result != '': headers = { 'User-Agent' : 'Lock/Beta1'} urld="https://xxx.flyhigher.top/ifkey/" #才不告诉你们真实地址嘞 pdata={'lock_id':'test001','passw':result} pdata=urllib.parse.urlencode(pdata) binary_data = pdata.encode('ascii') print('Checking!') req = urllib.request.Request(urld, binary_data, headers) fd = urllib.request.urlopen(req).read().decode("utf-8") print(fd) if fd == "test001": print("开门") downPower() openDoor() time.sleep(7) print("关门") downPower() closeDoor() else: print("Wrong!") else: print('Continue!') #方便调试时关机 elif content == "die": print('Yes!') os.system("sudo shutdown") #主循环 while True: dis = checkdist()*100 print(dis) if dis <= 30: havePhone() time.sleep(0.5)
其他代码这么多我就不放了。你来打我呀略略略╮ (. ❛ ᴗ ❛.) ╭
有空就放运行时的动图吧...
更新,ToDo List
- 用Vue重构前端
- 改用更好的密钥生成算法
- 重新测试以发现可能存在的漏洞
- 加入两步验证功能
- 加入开门记录功能
- 加入习惯分析功能
- 加入短信/邮件告警功能
- 加密数据库
- 更多安全特性
总结
感觉还是很棒的,接下来我会对它加入一些拓展,比如新的解锁方式、远程开门、优化流程、优化硬件布局以及增强安全性,拓展它的实用性。
以及,参加TEDx的时候有很多人建议我申请专利,估计很快就会落实(液!
这个项目也让我有了很多收获。第一次接触了Vue.js,但是遗憾最后没有应用;第一次尝试使用没有mysql扩展的PHP7开发后端,用熟了mysqli;自认为对硬件开发和树莓派初步入了个门;对Web安全有了更深刻的了解,未来可能会加上更高级的安全特性。当然,这个项目也得到了一些人的帮助,感谢他们!
然而估计很快就会开硬件方面的新坑...
发表回复