张登友,张登友的博客,张登友的网站——
最近趁着换服务器把原来的评论插件一并给换了成了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容器
运行效果如图所示
拉取并部署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
地址,便能看到注册界面。
测试评论效果
添加反向代理
添加配置字段
方便我们使用域名进行访问, 我的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>
朋友帮忙测试的效果(这都要占下便宜。。。)
参考以下博客:
官方文档: https://waline.js.org/guide/server/vps-deploy.html#docker-部署