编写hexo-comment

逛了一圈hexo的评论插件,之前被wordpress的机器人的恶意评论搞怕了,必须要有审核功能,但公开的评论插件都无法满足审核的需求。

安全工程师的开发水平还是要跟上的,作为一个半吊子全栈工程师,花三天时间自己写了一套评论系统,本文介绍一下整个流程。

整体设计

需求分析

  1. 继承 wordpress 的评论。读取原先存放在 mysql里的数据,然后想办法复原。
  2. 后端开发。支持增、查、审核,需要存数据库,字段和类型自定义。
  3. 前端开发。在原先 hexo 的静态页面上,支持查看评论和新增评论。
  4. 部署。原先服务是普通的 apache2.4,hexo 是直接以静态文件的方式部署到 /var/www/html

选型

  1. 前端。因为是在 hexo 上进行开发,无法直接选型,hexo 前端甚至都没有引入 jquery,因此使用最原始的 javascript 进行 ajax 操作和 dom 操作即可,抄一下 CSS。
  2. 后端。hexo 纯静态,因此新加的后端可以自由发挥。由于上次 wordpress 被打挂之后的迁移和扩容非常麻烦,因此不愿意使用mysql。由于我完全不懂数据库,更愿意使用高级的接口,选择 django,仅占用本地单个 sqlite 文件,迁移非常顺滑。
  3. 部署。了解到 apache2.4 支持 python 拓展(就像支持 php 拓展一样),对于 URL,不同的路径下可以配不同的解析服务,可以保证原先静态的主站不变,新的路径指派给评论后端来处理。
  4. 交互协议,使用 RESTful 风格的 API,传 json 前后端写着都方便。

后端开发

代码位于:https://github.com/LeadroyaL/blog_comment

使用 python3写个 django 项目,没有环境限制,其实还挺好写的,随便提一下设计吧。

  • 避免参数解析的繁琐,在 path 中指定参数和类型,更加 RESTful。
    1
    2
    3
    4
    5
    6
    7
    urlpatterns = [
    path('get/<int:post_id>', views.get),
    path('put/<int:post_id>', views.put),
    path('admin', views.admin),
    path('admin/accept/<int:comment_id>', views.accept),
    path('admin/reject/<int:comment_id>', views.reject),
    ]
  • django 自带的 auth 功能过度复杂,使用 session 存放登录态,反正审核功能也只有我一个人会用到。
  • 考虑到外界输入不可信,审核界面可能会 XSS,经过调研,django 的模板输出时会自动转义,不会被 XSS。
  • 使用 django 的 models,就可以不防 SQL 注入了,功能简单也不大会有其他漏洞。

唯一坑点就是 python3.8 和 python3.5语法变化,本机自动生成的代码在服务器上编译错误,而服务器的 python3 我也懒得升级,调整一下语法即可。

后端部署

由于对 apache2 的配置完全不懂,django官网以及其他网上资料写的不准确,每个人讨论的环境都不一样,导致这里卡了非常久。

思路:安装 libapache2-mod-wsgi-py3,修改 sites-available 里的配置,配置好文件权限。

参考链接:django官网

第一步,安装

sudo apt install libapache2-mod-wsgi-py3

第二步,放好文件

这一步坑非常大,主要受制于 linux 的权限管控,error.log 信息又很模糊,只有一个 403。

apache2进程是 www-data:www-data 运行的,如果把 django 项目放到主用户的 HOME 目录下,老是加载不起来。试了很多种方法都不大行,包括修改 apache conf 里的 Directory Files Require all granted,包括 chmod、chown,都不大行。

最后我选择放到 /var/www/blog_comment 下,然后 chown 给 www-data:www-data 就行了。

之后 python3 manage.py migrate 等操作也要用 www-data 用户来操作,不然将来没权限去写sqlite3.db。

第三步,修改 apache2

原先的配置:(SSL)

1
2
3
4
5
6
7
8
9
10
11
12
13
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerName www.leadroyal.cn
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/2_www.leadroyal.cn.crt
SSLCertificateKeyFile /etc/apache2/ssl/3_www.leadroyal.cn.key
SSLCertificateChainFile /etc/apache2/ssl/1_root_bundle.crt
......
......
</VirtualHost>

/comment 路径下的内容全部转发到 wsgi,并且配置 python-path。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerName www.leadroyal.cn
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/2_www.leadroyal.cn.crt
SSLCertificateKeyFile /etc/apache2/ssl/3_www.leadroyal.cn.key
SSLCertificateChainFile /etc/apache2/ssl/1_root_bundle.crt
......
......
WSGIDaemonProcess example.com python-path=/var/www/blog_comment
WSGIProcessGroup example.com
WSGIScriptAlias /comment /var/www/blog_comment/blog_comment/wsgi.py
</VirtualHost>

那三句话大概的意思是(不保证描述正确)

  1. 使用 deamon 方式启动,可能进程名字叫 example.com,配置 python-path
  2. 可能进程组叫 example.com
  3. 添加一条路由,将 /comment 开头的全都转发给 wsgi,只转发后半段,不包括 /comment 本身

如果不配置 python-path 的话,会提示 blog_comment 这个 module 找不到。非要的话,就要在 py 里硬编码 python path,不优雅。

这里还遇到过问题,提示 django 找不到,因为之前 django 被安装到了 home 目录下,www-data 又被权限管理给挡了,要装到 /usr/local 这种公共的地方。

总之,运行不来就看 error.log。

效果:访问 /comment/get/{post_id} 可以通过访问到 django 拿到该文章的评论,访问除此之外的目录,会访问到主站的 html 等静态资源。

前端开发

代码位于:https://github.com/LeadroyaL/blog_comment_frontend

前端开发是最玄学的地方,之前也就改 config,写 markdown 页面,一行代码都没写过。另外,因为 hexo 的目的是批量生成 html,显然是用模板的,如何在框架上二次开发,是需要一定学习和模仿能力的。

我使用的 theme-next,因此代码全部位于 /path/theme/next 下,也只需要对它进行修改,侵入性尽可能要小。

先对页面F12,观察文章页(即 post)的独有元素,寻找合理的插入位置,发现只有页面的底部有 “上一篇” 和 “下一篇” 的元素,叫 "post-nav",页面底部叫 "post-footer"。

找到它们的实现,在 layout/_macro/post.njk,附近有 post-footer.njk

经过观察,njk 应该是一种模板文件,可以 include 其他模板,可以访问hexo提供的与文章有关的变量和函数,最终变成 html。

此时还缺少 css 和 js,很容易搜到 source/css/_common/components/post/index.styl ,应该是 css 的模板文件。而运行时仅有一个 main.css,应该是由所有的 styl 拼接成的一个大 css。
而 js 的话,参考 {{- next_js('config.js') }} ,会生成 <script src="/js/config.js"></script>, 位于 source/js 下面,抄一下就可以自己写 js 了。

综上,共需要作出 3 处修改:

  1. layout/_macro/post.njk 中,引入评论区的 layout,

    1
    {{ partial('_partials/post/post-comments.njk', {}, {cache: theme.cache.enable}) }}
  2. source/css/_common/components/post/index.styl 中,引入评论区的 css

    1
    @import 'post-comment';
  3. 编写对应的 njk 和 styl,编写并按照规范引入 js

而 js 本身的逻辑,就是访问后端 API,拿到数据后渲染在 html 里。

加载评论的时机,选择了 IntersectionObserver 这个类,只有评论区被显示时才会触发加载请求,从而减少无意义的请求。

作为安全工程师,看到这个情景就要防XSS,我不愿意改后端代码,只能在前端上做文章。但原生 js 里自带的 escape 的功能会把日期中的冒号转义成百分号格式的,让我很恼火。最后选择是直接修改 textContent 来避免 XSS,参考 mozilla 的API文档

1
2
3
4
5
6
7
8
9
10
11
function comment_to_ele(author, time, content) {
let div = document.createElement("div");
div.className = "post-comment-item";
let p_author_time = document.createElement("h3");
p_author_time.textContent = '用户 ' + author + ' 发表于 ' + time;
let p_content = document.createElement("p");
p_author_time.textContent = content;
div.appendChild(p_author_time);
div.appendChild(p_content);
return div;
}

联调测试

本地测试的话还会遇到 mock server 的跨域问题,在 server 端配好就行,不然浏览器会丢弃来自 server 的返回数据。

还遇到了 styl 文件没有被整合到 css 里,乱试半天,模仿得惟妙惟肖才成功。

最终效果

↓快在下方评论区留言吧↓