V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
quietjosen
V2EX  ›  程序员

关于 Klib 分享的技术选型,各种纠结之后,我选了如下方案

  •  
  •   quietjosen ·
    atjason · 2017-06-20 08:21:18 +08:00 · 2006 次点击
    这是一个创建于 2474 天前的主题,其中的信息可能已经有所发展或是发生改变。

    技术方案选型是件很有意思的事,各个环节都有各种选择,可以组合出各种可能。在这些可能性中,挑选出最佳方案,是我很喜欢做的事

    最近刚刚完成 Klib 的标注分享,趁着热乎劲,小结一下:过程中纠结了哪些方案,以后最后选择了什么。

    0) 先来看看最终效果

    这就是 Klib 分享标注的操作流程:点击分享,立即得到可以全球访问的网页。操作不能更简单,背后的技术逻辑却很复杂:

    实际的开发是混在一起的、思路也是交叉的,不过,为了介绍方便,我大致按照数据流来推演。

    1) Klib 与接口服务器

    这部分的功能比较直接:Klib 将标注内容发送给接口服务器,服务器处理完后返回结果。

    需要介绍的,倒是功能之外的东西:

    • 如何防止接口被攻击
    • 如何做身份识别

    这部分内容其实是很复杂的,我最终采用了和 Klib 安全性相称的方案。

    1.0) 防止接口被攻击

    1.0.0) 接口服务器使用 https

    这是最基础、但非常有效的方式,全程使用 https 加密,已经可以大大提高安全性。

    1.0.1) 防止接口被非法使用

    如果接口是公开的、所有人都可以任意访问,就可以随意地向服务器丢垃圾数据,迅速将服务器挤爆。

    比如好的做法是 使用非对称加密,即使用一对私钥、公钥,使用 私钥加密 的数据,只能使用 公钥解密;反之,使用 公钥加密 的数据,只能使用 私钥解密。整体流程大致如下:

    • 接口服务器开放 公钥 A
    • 每个 Klib 客户端生成新的 私钥 B公钥 B
    • Klib 客户端使用 公钥 A 加密 公钥 B,并将其发送给接口服务器
    • 接口服务器使用 私钥 A 解密后,存储该客户端对应的 公钥 B
    • 之后,Klib 客户端发送数据时,使用 私钥 B 加密,接口服务器收到后使用 公钥 B 解密,并用 公钥 B 加密后返回数据

    听起来有点像绕口令?

    开发上也有点麻烦,毕竟服务器还要保存每个 Klib 客户端对应的公钥。如果有多个服务器,则需要在不同服务器间同步公钥,更加麻烦。对于我这个小产品 + 实验功能来说,暂时不需要这么高的安全级别。

    于是,我采用了更简单、但够用的 AES 对称加密。即 Klib 客户端和接口服务器使用相同的 AES 加密方法、同一个密码,加密请求和响应的数据;如果不能提供正确的加密,就无法使用服务器接口。

    这一方案主要的风险是:黑客可以反编译 Klib 得到密码。除了 Klib 本身会编译并签名,我还在代码里加密存储密码。基本上除了跟我有八辈子解不开的愁,99.9999% 的人是不会花精力来破解这个密码的。

    1.0.2) 使用时间戳 + MD5

    即使加密过的数据,最终也只是表现为一个 http 请求,而这个请求是可能被本地拦截,进而用于模拟正常用户请求。

    对应的防护是,在 http 请求中加入时间戳,并对 http 头的内容部分计算 MD5 (或 CRC 等),服务器端进行验证,就可保证 http 头不被滥用。

    其实,这是 OAuth 的范畴。好在,我在开发 图床神器 iPic 时,先后从客户端的角度实现了七牛、又拍、阿里云、Imgur、Flickr、Amazon S3 的 OAuth,这次实现一个简单的服务器端部分,也不算麻烦。

    1.1) 如何做身份识别

    上面说的是在面对黑客时的防护,听着有点晕是吧?下面来说说正常情况下的身份识别。

    比如:如果用户尝试停止一个分享,如何判断该用户是否有权限?

    如果有账户系统,这点比较容易解决。而 Klib 尚未引用账户系统,怎么办呢?比较高级的是使用区块链(咳咳),我目前的做法是:用户使用 Klib 分享一本书的标注时,服务器会返回一个随机数。下次用户在停止分享时,只要能提供这个随机数,即判定为有效请求。在上述各种防护的前提下,可以有效地防止被恶意停止分享。

    2) 接口服务器

    接口服务器是整个系统中最复杂的部分,它的职责比较多:

    • 验证请求,并接收数据
    • 存储数据
    • 根据数据生成静态网页
    • 将静态网页输送给静态服务器
    • 更新、删除分享时,更新数据存储和静态服务器

    验证请求和前面的介绍是对应的,这里略过不表。

    2.0) 使用 Python + Flask 实现功能部分

    所谓接口服务器,首先就是要开放接口(开门接客)具体的,就是 http 请求的路由表。比如,当 Klib 客户端向 https://api.klib.me/share 发送数据时,要有相应的代码来接收处理这个请求。

    在之前的文章 我入门 Python 后总结的基础教程 中,我已经介绍了使用 Flask 框架,这里不再重复。

    2.1) 使用 Nginx + Gunicorn 搭建服务器

    同上,请参考 我入门 Python 后总结的基础教程

    另外,使用 Supervisorctl 保证服务可靠运行。

    2.2) 使用 MySQL + SQLAlchemy 存储数据

    从数据存储的角度看,书的标注都是很规整的,无非是书名、作者、笔记内容等等。于是我选择了最常用的关系型数据库:MySQL

    如果直接使用 SQL 语句操作数据库,既繁琐又不安全,这里我使用可称为 ORM (Object Relational Mapping) 界事实标准的 SQLAlchemy 构建 Model、操作数据库。

    我本来想说「这没什么好介绍的」,但实际上,MySQL 的坑很多。比如,如果要支持 Emoji 表情,就要全程使用 utf8mb4 编码。还有很多其他的坑,此处略去一万字…

    2.3) 使用 Jinja 模板生成静态网页

    关于标注部分,Klib 发送的是 Markdown 格式,如:

    # 简单思考
    
    ## 卷首语
    
    - 商业的本质就是“持续提供用户真正想要的东西”,除此无他。
    
    - 召集具备回应用户需求的热情与能力的员工,并为他们营造出无拘无束可最大限度地发挥其才能的环境,除此无他。
    
    ## 第一章 经商不是“打仗”
    
    - 重要的是不断磨炼对“大众真实需求”的感知能力和使之实体化的技术。
    
    - 音乐和体育不同,不用与任何人战斗。
    

    需要使用 markdown 模式将其转换成 html 格式,如:

    <h1>简单思考</h1>
    <h2>卷首语</h2>
    <ul>
       <li>
          <p>商业的本质就是“持续提供用户真正想要的东西”,除此无他。</p>
       </li>
       <li>
          <p>召集具备回应用户需求的热情与能力的员工,并为他们营造出无拘无束可最大限度地发挥其才能的环境,除此无他。</p>
       </li>
    </ul>
    <h2>第一章 经商不是“打仗”</h2>
    <ul>
       <li>
          <p>重要的是不断磨炼对“大众真实需求”的感知能力和使之实体化的技术。</p>
       </li>
       <li>
          <p>音乐和体育不同,不用与任何人战斗。</p>
       </li>
    </ul>
    

    这里赞叹一下:Python 轮子就是多。只需轻轻地导入 markdown 模块,即可优雅地将 Markdown 转换为 html 格式,舒爽。

    import markdown
    html_str = markdown.markdown(markdown_str)
    

    对于最终生成的静态网站,像 css/js 等部分都是一样的,只是页面标题、正文等内容性的东西不同。于是,使用 Jiaja 模式表示这些通用部分,并 {{ title }} 这样的标注符表示各个分享所不同的内容部分;再用 render_template 方法替换模板中的内容,即可生成对应的静态文件。

    感叹:这样简洁直接的操作、无需各种复杂的配置,就能得到最后想要的东西,真真是编程中最可爱的环节

    3) 静态服务器与 CDN

    有了静态服务器,就像是有了宝贝,不能只是自己藏着,得拿出来让大家瞧瞧,这就是静态 (Web) 服务器要干的事。

    当然,静态服务器和接口服务器,在物理上可以是同一台服务器,这里只是从角色上进行区分。

    在展示静态网页方面,技术选型上主要有 2 方面的需求:

    • 网页内容能实时更新
    • 用户访问速度快

    其中,内容的更新对应用户分享时的 3 种操作:

    • 创建分享
      • 对应创建静态 html 文件
    • 更新分享的内容
      • 对应更新 html 文件内容
    • 停止分享
      • 对应删除 html 文件

    好,带着「实时」创建、更新、删除 html 文件这 3 个需求,我们来看看如何提高访问速度

    3.0) 😔 仅使用单一服务器

    首先,如何什么都不做,意味全球的用户(Klib 必须是国际性产品,得考虑全球用户,嗯)都要连接这台服务器。

    且不说并发数等限制,单从网速上看,如果将服务器放在国内,国外用户势必慢;反之亦然。更何况国内还是电信、网通、以及神奇的长宽,国外也有 N 多国家。

    如果确实要这么做,比较好的方案是使用 阿里云香港服务器,可以兼顾国内国外用户。暂时,不采用这一方案,每月省下 $19 …

    3.1) 😔 CDN

    进而,通常的做法是使用 CDN.

    CDN 确实可以有效提高不同地区、不同网络环境下的访问速度,且极大地降低对静态服务器的压力。不过,CDN 有个致命的局限:内容更新慢。尤其在更新、删除内容时,这种慢会带来业务上的问题

    比如,用户在 Klib 中分享标注后又停止,却发现之前产生的网页依然可以访问,用户会觉得这是 Bug,进而会带来很大的客服压力。于是,跳过这一方案。

    3.2) 😔 国内、国外多台服务器

    下一方案是:国内、国外各一台(或多台)服务器,通过 DNS 服务器进行分流,相当于自建 CDN。

    不料,却遇到一个坑:国内服务器的外网速度普遍较慢。比如我试了阿里云上海节点,从国外服务器使用 scp 或 rsync 传输一个 10 KB 的文件需要 4s,跌破了我的眼镜。并且,阿里云我也只买了 1 MB 带宽的小水管,并发时速度会很慢。于是,这一方案也被放弃。

    3.3) 😃 最终方案

    如何实现 全国秒开、及更多详尽分析,尽在「自在开发」公众号:

    自在开发


    3.4) 搜索引擎优化

    4) 网页的适配

    5) 其他

    5.0) 区分开发、测试、线上环境

    5.1) 服务监控

    5.2) 一定要有日志

    5.3) 一定要有备份

    6) 事务诸葛亮

    5 条回复    2017-12-18 12:36:08 +08:00
    bp0
        1
    bp0  
       2017-06-20 08:36:25 +08:00 via Android
    从安全的角度来说,黑客不需要跟你有仇。只要有利益,不交换密钥都是耍流氓。
    quietjosen
        2
    quietjosen  
    OP
       2017-06-20 09:04:19 +08:00
    @bp0 分享个读书笔记,不会引狼入室吧?好吧,我耍流氓了。
    bp0
        3
    bp0  
       2017-06-20 09:58:38 +08:00
    其实,如果只是自己做来玩玩,那没问题。
    Wichna
        4
    Wichna  
       2017-12-17 21:14:36 +08:00
    感谢分享,还是有很多干货的
    quietjosen
        5
    quietjosen  
    OP
       2017-12-18 12:36:08 +08:00
    @Wichna 不客气,能帮到别人,我还是很开心的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2871 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 13:08 · PVG 21:08 · LAX 06:08 · JFK 09:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.