使用Docker部署waline


张登友,张登友的博客,张登友的网站——

最近趁着换服务器把原来的评论插件一并给换了成了waline,其中坑还是有点多,断断续续花了3天时间才搞完,这里记录下方便后期维护更新

介绍

这一段介绍摘自官方文档,不想看可以直接跳过,从docker部署waline开始直接部署

Valine

Valine 是一款样式精美,操作简单,部署高效的评论系统。第一次接触便被它精美的样式,无服务端的特性给吸引了。它不含服务端,前端直接和 LeanCloud 存储服务交互,真是太酷了!但是随着深入了解,我发现它存在着一些问题。

源码不开放

作者不知为何从 1.4.0 版本开始只推送编译后的文件到 GitHub 仓库中,源文件停止更新。可能是被开源伤了心吧。对于我这种想增加或者修改功能的用户来说,这个问题就有点难受了。

XSS 安全

从很早的版本开始就有用户报告了 Valine 的 XSS 问题,社区也在使用各种方法在修复这些问题。包括增加验证码、前端 XSS 过滤等方式。不过后来作者才明白,前端的一切验证都只能防君子,所以又去除了验证码之类的限制。

现有的逻辑里,前端发布评论的时候会将 Markdown 转换成 HTML 然后走一下前端的一个 XSS 过滤方法最后提交到 LeanCloud 中。从 LeanCloud 中拿到数据之后因为是 HTML 直接插入进行显示即可。很明显,这个流程是存在问题的。只要直接提交的是 HTML 而且拿到 HTML 之后直接进行展示的话,XSS 从根本上是无法根除的。

隐私泄露

攻击者除了可以任意存储,也可以任意读取,数据库的字段开放读取权限后,该字段的内容对攻击者是完全透明的。在评论数据中,IP 和邮箱两个字段包含了用户的敏感数据。灯大甚至专门写了一篇文章来批判该问题 《请马上停止使用 Valine.js 评论系统,除非它修复了用户隐私泄露问题》在新窗口打开。甚至掘金社区在早期使用 LeanCloud 的时候也暴出过 泄露用户手机号在新窗口打开 的安全问题。

为了规避这个问题,Valine 作者增加了 recordIP 配置用来设置是否允许记录用户 IP。由于无服务端,只能通过不存储的方式解决。该配置项仍存在一个问题: 记录配置权在网站,评论者无权管理自己的隐私。

邮箱泄露是另一个重大隐私问题。在前端计算并上报用户邮箱的 md5 用来获取 Gravatar 头像是完全可行的。但是如果需要当评论被回复时发送邮件通知,就不可避免的要存储用户邮箱的原始值。这个问题理论上可以通过 RSA 加密来解决,私钥存储在 LeanCloud 的环境变量中,客户端同时上报邮箱 md5 和公钥加密后的邮箱,LeanCloud 在发送邮件通知时在云函数中通过环境中的私钥解密得到用户邮箱。但是考虑到前端 RSA 加密库的体积与性能,实际应用可行性很小。增加一层服务端,通过服务端过滤敏感信息是一个较优的做法。

阅读统计篡改

Valien 1.2.0 增加了文章阅读统计的功能,用户访问页面就会在后台 Counter 表中根据 url 记录访问次数。由于每次访问页面都需要更新数据,所以在权限上必须设置成可写,才能进行后续的字段更新。这样就造成了一个问题,实际上该条数据是可以被更新成任意值的。

Waline 的诞生

基于以上原因,Waline 横空出世了。Waline 最初的目标仅仅是为 Valine 增加上服务端中间层,但是由于 Valine 的不开源所以只能连带前端也实现一遍。当然前端的很多代码和逻辑为了和 Valine 的配置保持一致都有参考 Valine。甚至在名字上,我也是从 Valine 上衍生的,让大家能明白这个项目是 Valine 的衍生版。

增加了服务端除了解决了上述的安全问题,之前 Valine 受限于无服务端的很多功能也可以实现了。包括但不限于邮件通知、垃圾评论过滤等。

Docker部署waline

部署过程从这里正式开始,这里使用的是Linux的发行版Debian10.5

docker版本:

终端输入docker version 即可获取

Client: Docker Engine - Community
 Version:           20.10.12
 API version:       1.41
 Go version:        go1.16.12
 Git commit:        e91ed57
 Built:             Mon Dec 13 11:45:37 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:46 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

安装docker

apt-get update -y && apt-get install curl -y  # 安装curl
curl https://get.docker.com | sh -   # 安装docker
sudo systemctl start docker  # 启动docker服务
docker version # 查看docker版本(客户端要与服务端一致)

创建容器

docker version # 查看docker版本(客户端要与服务端一致)
docker pull mysql #拉取mysql镜像

#带参数创建并运行容器
docker run -p 3306:3306 --name mysql \
-e TZ=Asia/Shanghai \
-v /www/mydocker/mysql/conf:/etc/mysql/conf.d \
-v /www/mydocker/mysql/logs:/var/log/mysql \
-v /www/mydocker/mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

MySQL相关的参数如下表所示:

环境变量名称 必填必填 默认值默认值 备注
MYSQL_HOST 127.0.0.1 MySQL 服务的地址
MYSQL_PORT 3306 MySQL 服务的端口
MYSQL_DB true MySQL 数据库库名
MYSQL_USER true MySQL 数据库的用户名
MYSQL_PASSWORD true MySQL 数据库的密码
MYSQL_PREFIX wl_ MySQL 数据表的表前缀
MYSQL_CHARSET utf8mb4 MySQL 数据表的字符集
参数解释
-p 3306:3306 --name mysql \ #将主机的3306端口映射到docker容器的3306端口。
--name mysql为运行服务名字
-e TZ=Asia/Shanghai \ #时区是使用了世界标准时间(UTC)。因为在中国使用,所以需要把时区改成东八区的。
-v /www/mydocker/mysql/conf:/etc/mysql/conf.d \
-v /www/mydocker/mysql/logs:/var/log/mysql \
-v /www/mydocker/mysql/data:/var/lib/mysql \
# -v后面要指定映射路径,Mac端必须要指定,默认根目录如果没有写权限,会导致启动失败,这里我是选择的/www路径
-e MYSQL_ROOT_PASSWORD=root \ #初始化 root 用户的密码。
-d mysql --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci # 后台程序运行,mysql--character-set-server=utf8mb4 :设置字符集,--collation-server=utf8mb4_unicode_ci:设置校对集

设置开机启动

docker update mysql --restart=always

初始化Waline的表结构

下面对该数据库执行 waline.sql 文件初始化表结构。使用终端执行或者Navicat的终端或者查询功能都OK

一、使用Navicat创建(推荐)

用Navicat连接之后新建查询,执行下面的SQL语句

CREATE DATABASE waline DEFAULT CHARACTER SET utf8mb4;

CREATE TABLE `wl_Comment` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `comment` text,
  `insertedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `ip` varchar(100) DEFAULT '',
  `link` varchar(255) DEFAULT NULL,
  `mail` varchar(255) DEFAULT NULL,
  `nick` varchar(255) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  `sticky` int(11) DEFAULT NULL,
  `status` varchar(50) NOT NULL DEFAULT '',
  `ua` text,
  `url` varchar(255) DEFAULT NULL,
  `createdAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `wl_Counter` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `time` int(11) DEFAULT NULL,
  `url` varchar(255) NOT NULL DEFAULT '',
  `createdAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `wl_Users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `display_name` varchar(255) NOT NULL DEFAULT '',
  `email` varchar(255) NOT NULL DEFAULT '',
  `password` varchar(255) NOT NULL DEFAULT '',
  `type` varchar(50) NOT NULL DEFAULT '',
  `url` varchar(255) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `github` varchar(255) DEFAULT NULL,
  `twitter` varchar(255) DEFAULT NULL,
  `facebook` varchar(255) DEFAULT NULL,
  `google` varchar(255) DEFAULT NULL,
  `weibo` varchar(255) DEFAULT NULL,
  `qq` varchar(255) DEFAULT NULL,
  `createdAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

创建数据库

显示OK即为创建成功

二、命令行的方式
docker exec -it mysql bash  #exec退出容器终端不会导致容器的停止, -it解释-i:交互式操作,一个是 -t 终端
mysql -u 数据库用户名 -p密码 # 接着输入密码,即可登录Docker容器内的mysql
然后再执行上面的SQL语句创建数据库 

需要使用单独数据库用户名使用可参考一下步骤, 若直接使用root用户可省略以下步骤

CREATE USER '数据库用户名'@'127.0.0.1' IDENTIFIED BY '密码'; # 创建新的用户
GRANT ALL PRIVILEGES ON zdy.* TO 'zdy'@'%'; # 授予数据库管理权限(高版本mysql可能会报错)解决方法如下
update user set host='%' where user='数据库用户名'; #更改mysql数据表中用户的host
Grant all privileges on 数据库名.* to '数据库用户名'@'%'; #再来执行授予权限
FLUSH PRIVILEGES; #最后刷新权限, 使设置生效
alter user 数据库用户名 identified with mysql_native_password by '密码'; #因为mysql8的加密方式和Navicat不一样, 如果Navicat链接出错,请执行这句修改加密方式

mysql8的分配权限不能带密码隐士创建账号了,要先创建账号再设置权限(这里踩了坑)

设置数据库的时区

docker exec -it mysql bash                 # 进入mysql容器内部
date -R                                    # 查看当前时区
cp /usr/share/zoneinfo/PRC /etc/localtime  # 修改为当地时区
exit                                       # 退出mysql容器
docker restart mysql                       # 重启mysql容器

运行效果如图所示

creat_table

拉取并部署Waline的Docker镜像

原作者的官方docker镜像lizheming/waline,使用docker pull命令拉取下来。

docker search waline # 搜索镜像
docker pull lizheming/waline:latest  #拉取镜像

拉取镜像

运行容器

使用docker run命令创建实例容器并运行,参数填写

你可以在点击这里打开查看所有支持的邮箱运营商。

docker run -d --name waline -p 8360:8360 \
  -v /mydocker/waline/data:/app/data \
  -e TZ="Asia/Shanghai" \
  -e MYSQL_HOST="公网IP" \
  -e MYSQL_DB="waline" \
  -e MYSQL_USER="数据库密码" \
  -e MYSQL_PASSWORD="数据库密码" \
  -e AUTHOR_EMAIL="zdyas@qq.com" \
  -e SITE_NAME="张登友的博客" \
  -e SENDER_NAME="博客留言" \
  -e SITE_URL="https://www.zdynb.cn" \
  -e SMTP_SERVICE="QQ" \ #可以查询支持的服务商
  -e SMTP_USER="您的邮箱" \
  -e SMTP_PASS="授权码" \
  -e 	SECURE_DOMAINS="安全域名" \
  -e DISABLE_USERAGENT="false" \ #是否开启浏览器标识
  -e AVATAR_PROXY="https://avatar.75cdn.workers.dev" \ #头像加速镜像地址
  -e IPQPS="30" \ #ip发言频率
  --restart always \
  lizheming/waline:latest

这时用浏览器访问http://ip地址:8360/ui/register地址,便能看到注册界面。

注册界面

测试评论效果

测试waline

添加反向代理

添加配置字段

方便我们使用域名进行访问, 我的NGINX配置目录为/usr/local/nginx/conf/vhost , 需要把80和443两个server段都加上以下内容

location / {
         # 反向代理
         proxy_pass http://公网IP:端口号;
         proxy_set_header Host $host:$server_port;
         proxy_set_header X-NginX-Proxy true;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header REMOTE-HOST $remote_addr;

         # 缓存
         add_header X-Cache $upstream_cache_status;
         add_header Cache-Control no-cache;
         expires 12h;
}
完整配置如下

我这里添加了ssl协议,所以还需要在443端口的配置添加一个。

配置完成,重启NGINX即可完成反向代理

server
    {
        listen 80;
        #listen [::]:80;
        server_name 想要代理的域名 ;
        index index.html index.htm index.php default.html default.htm default.php;

        include rewrite/none.conf;
        #error_page   404   /404.html;

        # Deny access to PHP files in specific directory
        #location ~ /(wp-content|uploads|wp-includes|images)/.*\.php$ { deny all; }

        include enable-php.conf;

        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
        }

        location ~ .*\.(js|css)?$
        {
            expires      12h;
        }

        location ~ /.well-known {
            allow all;
        }

        location ~ /\.
        {
            deny all;
        }
	location / {
         # 反向代理
         proxy_pass http://公网IP:端口号;
         proxy_set_header Host $host:$server_port;
         proxy_set_header X-NginX-Proxy true;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header REMOTE-HOST $remote_addr;

         # 缓存
         add_header X-Cache $upstream_cache_status;
         add_header Cache-Control no-cache;
         expires 12h;
     }
        access_log off;
    }

server
    {
        listen 443 ssl http2;
        #listen [::]:443 ssl http2;
        server_name 想要代理的域名 ;
        index index.html index.htm index.php default.html default.htm default.php;

        ssl_certificate /usr/local/nginx/conf/ssl/mes.zdynb.cn/fullchain.cer;
        ssl_certificate_key /usr/local/nginx/conf/ssl/mes.zdynb.cn/mes.zdynb.cn.key;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers "TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCD-SHA256:EEDDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+ACS256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5";
        ssl_session_cache builtin:1000 shared:SSL:10m;
        # openssl dhparam -out /usr/local/nginx/conf/ssl/dhparam.pem 2048
        ssl_dhparam /usr/local/nginx/conf/ssl/dhparam.pem;

        include rewrite/none.conf;
        #error_page   404   /404.html;

        # Deny access to PHP files in specific directory
        #location ~ /(wp-content|uploads|wp-includes|images)/.*\.php$ { deny all; }

        include enable-php.conf;

        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
        }

        location ~ .*\.(js|css)?$
        {
            expires      12h;
        }

        location ~ /.well-known {
            allow all;
        }

        location ~ /\.
        {
            deny all;
        }
	location / {
         # 反向代理
         proxy_pass http://公网IP:端口号;
         proxy_set_header Host $host:$server_port;
         proxy_set_header X-NginX-Proxy true;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header REMOTE-HOST $remote_addr;

         # 缓存
         add_header X-Cache $upstream_cache_status;
         add_header Cache-Control no-cache;
         expires 12h;
     }
        access_log off;
    }
配置效果

此处已经可以通过域名进行访问了

反向代理

嵌入网站

<div id="waline"></div> <!-- 新建一个放置waline容器,id名可随意 -->
<script src="https://cdn.jsdelivr.net/npm/@waline/client/dist/Waline.min.js"></script> 
    <script>
        const waline = new Waline({
            el: '#waline', /* 此处需要对应HTML部分中的ID名 */
            serverURL: 'https://mes.zdynb.cn',
            /* 后端URL(必填) */
            visitor: true,
            /* 文章访问量统计 */
            dark: 'auto',
            /* 黑暗模式适配 */
            login: 'enable',
            /* 登录模式状态,默认值enable */
            wordLimit: 500,
            /* 评论字数限制,0为不限制,默认值为0 */
            pageSize: 10,
            /* 评论列表分页,数字为条数,默认值10 */
            highlight: true,
            /* 代码高亮,默认true */
            meta: ['nick', 'mail', 'link'],
            /* 评论者相关属性,默认['nick', 'mail', 'link'] */
            requiredMeta: [],
            /* 设置评论者属性必填项,默认[](即匿名) */
            placeholder: '填写邮箱我可以通过邮箱及时回复您',
            /* 评论框占位提示符,默认'欢迎评论' */
            copyright: false,
            /* 是否显示页脚版权信息 */
            avatar: 'mp',
            login: false,
        });
    </script>

朋友帮忙测试的效果(这都要占下便宜。。。)

hexo_waline

参考以下博客:

官方文档: https://waline.js.org/guide/server/vps-deploy.html#docker-部署

https://www.techgrow.cn/posts/ae18fb85.html

https://www.eula.club/使用Waline给Hexo静态博客添加评论系统.html


文章作者: 张登友
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张登友 !
  目录