Skip to main content

· 5 min read
He Wei

SSH 登录后执行 zip 命令

公司阿里云 Windows 服务器上的日志需要每个月归档一下,以前都是手动操作,最近感觉太麻烦,于是研究了一下自动化的方案,因此有了这篇文章。

万事不决问 Kimi,这篇文章的主要思路和解决方案都是 Kimi 给出的,谨表谢意。

最开始给出的方案是 ssh -t ecs '...',经过研究,ssh -t 会为命令创建一个伪终端,这样命令就可以在远程服务器上以交互模式运行。

之前给另一个前端项目写的命令是调用 rm 来删除文件,这个命令在 Windows 服务器上能运行,说明调用的是服务器上的 git bash 带的命令。

但是 git bash 默认不带 zip 命令,于是搜索 git bash zip command pass variable 这组关键词之后,看到了 How to add man and zip to "git bash" installation on Windows 这篇文章,按照文章里的方法,给 git bash 安装了 zip 命令。

虽然 zip 命令安装成功了,但是执行 ssh -t ecs 'zip ...' 的时候,既不会报错,也不会执行命令。

经过研究,发现命令是没问题的,只不过 -t 参数加上之后会让命令无法执行。

于是干脆去掉这个参数,直接执行 ssh ecs '...',这样命令就能正常执行了。

将前一个月的年份和月份作为变量传递

由于需要归档的是服务器上前一个月各网站的 IIS 日志,所以需要将前一个月的年份和月份作为变量传递给远程服务器上的命令。

结合 Kimi 和 Cursor 给出的方案,最终用下面的代码实现了需求:

# 获取上一个月的年份和月份
# 即使本月是 1 月,上一个月也会自动计算为前一年的 12 月
YYYY=$(date -d "$(date) -1 month" "+%Y")
YY=$(date -d "$(date) -1 month" "+%y")
MM=$(date -d "$(date) -1 month" "+%m")

拿到了年份和月份,在 bash 中就可以用 ${YYYY}${YY}${MM} 的方式来使用了。

将指定目录下匹配规则的文件进行压缩

这个需求很简单,指定目录下的目标文件名符合 u_exYYMMDD.log 的格式,这样的文件可以用 u_ex${YY}${MM}* 的方式来匹配。

因为需要压缩后删除源文件,所以用 zip -m 参数来压缩。

另外压缩时不需要带上文件的目录结构,所以用 -j 参数。

这样完整的 zip 命令就是 zip -mj ${ZIP_FILE} ${LOG_DIR}/u_ex${YY}${MM}*

将多个目录下的文件压缩到多个对应的目录中

IIS 为每个网站创建的日志目录格式是 W3SVC1W3SVC2 这种,而自己用来存放压缩后的每个月日志的目录格式是 W3SVC1_aaaaW3SVC2_bbbb 这种,所以需要将每个网站的日志压缩到对应的目录中。

问了一下 kimi,给出了下面的方案,很好用。

declare -A websites
websites["W3SVC2"]="aaaa"
websites["W3SVC3"]="bbbb"
......

然后就可以用下面的语句来遍历这个数组,执行压缩命令了。

for index in "${!websites[@]}"; do
ZIPFILE_DIR="/path/of/archive/${index}_${websites[$index]}"
LOG_DIR="/path/of/logs/${index}"
ZIP_FILE="${ZIPFILE_DIR}/${YYYY}-${MM}.zip"

# 构建压缩命令
# -m 表示压缩后删除源文件
# -j 表示压缩时不带目录结构
CMD="ssh ecs1 \"zip -mj ${ZIP_FILE} ${LOG_DIR}/u_ex${YY}${MM}*\""

# 执行压缩命令
echo ""
echo "正在归档网站 ${websites[$index]} $YYYY年$MM月的日志..."
eval $CMD
done

总结

  • ssh ecs '...' 的方式执行命令,命令中需要用到变量时,需要用 ${...} 的方式来获取。
  • declare -A 的方式来定义数组,用 websites["W3SVC1"]="aaaa" 的方式来给数组赋值,用 websites[$index] 的方式来获取数组的值。
  • for index in "${!websites[@]}"; do ... done 的方式来遍历数组。
  • eval 的方式来执行命令。
  • zip -mj ${ZIP_FILE} ${LOG_DIR}/u_ex${YY}${MM}* 的方式来压缩并删除源文件。

· One min read
He Wei

在 iOS 系统中,如果打开文件下载链接,会像下图这样,不显示文件名称,只显示一个 null。

image

解决办法就是用下面这段代码实现下载功能:

function download (url, fileName) {
const x = new XMLHttpRequest()
x.responseType = 'blob'
x.open('GET', url, true)
x.send()
x.onload = () => {
const downloadElement = document.createElement('a')
const href = window.URL.createObjectURL(x.response) // create download url
downloadElement.href = href
downloadElement.download = fileName // set filename (include suffix)
document.body.appendChild(downloadElement) // append <a>
downloadElement.click() // click download
document.body.removeChild(downloadElement) // remove <a>
window.URL.revokeObjectURL(href) // revoke blob
}
}

参考链接:微信内置浏览器下载pdf的时候标题为null?

· One min read
He Wei

· One min read
He Wei

无效命令

  1. rm + Windows 格式的路径 ssh -t ecs1 "'rm -r e:\upcweb\uppbook\yd\_nuxt\*'"

有效命令

  1. rm + Linux 格式的路径 ssh -t ecs1 "'rm -r /e/upcweb/uppbook/yd/_nuxt/*'"

注意:按照 这里 的说明,需要执行的命令先用双引号包裹,然后再用单引号包裹,这样才能成功执行。

· 4 min read
He Wei

前情提要

有一个业务,在 PC 端以网站的形式呈现,在手机端以小程序的形式提供服务。

为了统一使用一套用户身份,需要在 PC 端网站上实现微信扫码登录功能,将用户在微信小程序中的 openId 传给网页端。

最开始调研的方案,是在后端生成小程序码并发送给前端。后来在 V2EX 咨询了一下,发现这种方案有每分钟的调用频率显示,并且对服务端消耗比较大,对方建议采用普通二维码跳转小程序的方式。

官方文档:扫普通链接二维码打开小程序

参考文章:微信小程序踩坑系列之扫普通链接二维码打开小程序扫描普通二维码进入小程序

官方文档说得还是不够清楚,又看了几篇个人分享的心得,结合自己的实际操作,总结了一下。

整体流程

以下仅列出关键步骤。

  1. 在服务端随机生成一个 UUID,返回给 PC Web 端。
  2. PC Web 端生成一个普通二维码,内容为 https://www.abc.com/biz/?q=${uuid}
  3. 手机微信扫描二维码,跳转到所配置的小程序页面,并传入上面二维码对应的 URL,其中包含 UUID。小程序将用户 openId 和 UUID 传给服务端。
  4. 服务端将 UUID 和 openId 关联为 key-value,以便后续使用。
  5. PC Web 端轮询服务端,获取 UUID 关联的 openId,如果获取成功,则登录。

配置二维码跳转小程序

在小程序管理后台的 开发设置 页面,可配置 扫普通链接二维码打开小程序 的功能。

先配置 二维码规则,填写 https://www.abc.com/biz/

然后配置 前缀占用规则,如果选择是,则对于前一步配置的二维码 URL,当前规则将独占所有匹配的子规则,即 https://www.abc.com/biz/*,其他跳转规则将不能再使用满足条件的子规则。

再配置要跳转到的 小程序功能页面,扫码后会跳转到这个页面。

接着配置 测试范围,这个选项在规则发布之后也可以修改。

最后配置 测试链接。注意,在跳转规则未发布的情况下,测试链接也是有效的。这样方便进行功能测试,以免发布无效的规则,浪费每月额度。

· 4 min read
He Wei

整体流程

公司的业务需求是需要一个邮箱验证服务,用户注册后需要验证邮箱,才能继续使用服务。这个服务的基本流程如下:

  1. 用户注册时,填写邮箱地址作为用户名。
  2. 后端收到注册请求时,生成一个随机的验证码,发送到用户的邮箱。
  3. 用户收到邮件后,在注册页面输入邮件中的验证码,并继续注册。
  4. 后端接口验证验证码是否正确,以及是否与邮箱匹配。
  5. 验证成功后,注册该用户。
  6. 验证失败后,提示用户重新验证。
  7. 验证码的有效期为 24 小时。
  8. 验证码只能使用一次。
  9. 验证码错误次数超过 3 次,验证码失效。
  10. 验证码失效后,用户需要重新注册。

后端服务

后端服务使用 Strapi + @strapi/provider-email-nodemailer 这个插件。

配置插件

安装好上面的插件之后,在 Strapi 项目的 config/plugins.js 中配置邮箱服务的信息。

module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST', 'smtp.qiye.aliyun.com'),
port: env('SMTP_PORT', 465),
secure: true,
auth: {
user: env('**@**.com'),
pass: env('****'),
},
},
settings: {
defaultFrom: '**@**.com',
defaultReplyTo: 'hello@example.com',
},
},
},
})

以上配置在本地开发环境也是可以正常使用的,自己在 Chrome 中切换到了 Charles 代理模式,访问邮箱所属域名下的 URL,通过 Charles 的 Rewrite 功能把请求重写到了本地,这样就能在本地方便地测试邮件发送功能了。

这里有几点要注意:

  1. host 那里填写的是你的邮箱服务商的 SMTP 服务器地址,由于用的是阿里云的企业邮箱,所以参考这篇文档 阿里邮箱IMAP、POP、SMTP地址和端口信息 里的地址。
  2. port 那里要填写对应的端口号,阿里邮箱的 SMTP 端口是 465,同时 secure 要设置为 true
  3. 在邮箱所属域名的 DNS 解析设置里,同样要把 SMTP 服务器地址填写进去,这样才能正常发送邮件。知道这一点,是因为在用阿里云企业邮箱给自己的 QQ 邮箱发邮件失败,QQ 邮箱给的错误信息里有相关文档:什么是SPF?如何设置SPF来防止我的邮件被拒收呢?,阿里云也搜到了相关文档:退信提示“spf check failed”,按照阿里云的设置搞定了。注意 DNS 解析修改后,要等待一段时间(10 分钟)才能生效。
  4. 再次发送测试邮件时,QQ 邮箱又返回了 550 Mail content denied 这个错误信息,原来是邮件内容涉嫌大量群发。看了一下,是代码自动生成的内容,修改了之后就可以了。

· 6 min read
He Wei

配置 ECS

系统选择 Ubuntu 22.04,密码则在实例管理控制台页面,通过重置密码方式进行设置。

但是设置密码之后,依然无法远程 SSH 登录,猜测可能是系统防火墙的问题,先不管了,先把整体环境配置好。

安装 Nginx

sudo apt update

sudo apt install nginx

配置防火墙

sudo ufw app list

sudo ufw allow 'Nginx HTTP'

sudo ufw allow 'OpenSSH'

sudo ufw enable

sudo ufw status

curl -4 icanhazip.com

安装配置 MySQL

sudo apt install mysql-server

sudo mysql_secure_installation

MEDIUM Length >= 8, numeric, mixed case, and special characters

Remove anonymous users? (Press y|Y for Yes, any other key for No) : y

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y

Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y

安装 PHP

sudo apt install php8.1-fpm php-mysql php-simplexml

配置 Nginx

sudo nano /etc/nginx/sites-available/your_domain

server {
listen 80;
server_name your_domain www.your_domain;
root /var/www/your_domain;

index index.html index.htm index.php;

location / {
try_files $uri $uri/ =404;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}

location ~ /\.ht {
deny all;
}

}

sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/

sudo unlink /etc/nginx/sites-enabled/default

sudo nginx -t

sudo systemctl reload nginx

sudo mkdir /var/www/your_domain

sudo nano /var/www/your_domain/index.html

<html>
<head>
<title>your_domain website</title>
</head>
<body>
<h1>Hello World!</h1>

<p>This is the landing page of <strong>your_domain</strong>.</p>
</body>
</html>

http://server_domain_or_IP

测试 PHP 解析

sudo nano /var/www/your_domain/info.php

<?php
phpinfo();

access http://server_domain_or_IP/info.php

sudo rm /var/www/your_domain/info.php

测试 PHP 连接 MySQL

sudo mysql

CREATE DATABASE example_database;

CREATE USER 'example_user'@'%' IDENTIFIED WITH mysql_native_password BY 'xxxx';

GRANT ALL ON example_database.* TO 'example_user'@'%';

exit

mysql -u example_user -p

SHOW DATABASES;

CREATE TABLE example_database.todo_list (
item_id INT AUTO_INCREMENT,
content VARCHAR(255),
PRIMARY KEY(item_id)
);

INSERT INTO example_database.todo_list (content) VALUES ("My first important item");

SELECT * FROM example_database.todo_list;

exit

sudo nano /var/www/your_domain/todo_list.php

<?php
$user = "example_user";
$password = "xxxx";
$database = "example_database";
$table = "todo_list";

try {
$db = new PDO("mysql:host=localhost;dbname=$database", $user, $password);
echo "<h2>TODO</h2><ol>";
foreach($db->query("SELECT content FROM $table") as $row) {
echo "<li>" . $row['content'] . "</li>";
}
echo "</ol>";
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
die();
}

http://server_domain_or_IP/todo_list.php

sudo rm /var/www/your_domain/todo_list.php

安装配置 WordPress

先在 MySQL 中创建数据库和用户。

# 如果需要多语言,则创建两个数据库
CREATE DATABASE wordpress;
CREATE DATABASE wordpress_en;

CREATE USER 'wordpress_admin'@'%' IDENTIFIED WITH mysql_native_password BY 'xxxx';

# 用同一个 DB ADMIN 账户管理两个数据库
GRANT ALL ON wordpress.* TO 'wordpress_admin'@'%';
GRANT ALL ON wordpress_en.* TO 'wordpress_admin'@'%';

sudo apt install unzip

将 WordPress 文件解压到 /var/www/your_domain,再复制一份到 your_domain/en。

将两处的 wp-config-sample.php 复制为 wp-config.php。

vi /var/www/your_domain/wp-config.php,修改数据库连接信息,en 子目录下的相同文件也做同样操作。

访问 http://server_domain_or_IP/wp-admin/install.php,按照提示完成安装,en 子目录下的相同。

如果在输入密码的地方报错 password strength is unknown,有可能是在用 https 协议访问。可以在 wp-config.php 中添加一行:

$_SERVER['HTTPS'] = true; 

搜索 Password strength unknown,在 https://wordpress.stackexchange.com/a/350453 中找到的这个解决方法。

用户名:lxklsh2024,密码:r0pI%y(15um#h&kRfn

https://developer.wordpress.org/advanced-administration/before-install/howto-install/

使用 WP 主题

将主题的 zip 文件上传到服务器。

如果 Nginx 报错 413 Request Entity Too Large,可以修改 /etc/nginx/nginx.conf,添加或修改 client_max_body_size 为 100M。

http {
client_max_body_size 100M;
}

点击上传按钮后报错 Unable to create directory wp-content/uploads/2024/06. Is its parent directory writable by the server?,可以通过以下命令解决:

sudo chown -R www-data:www-data /var/www/your_domain/wp-content/uploads

但是这样的话,WordPress 还要求配置 FTP 用户名和密码,这不麻烦了吗,不折腾。

家里电脑没法通过 SSH 连接服务器,但是办公室可以,于是就用办公室电脑的 Termius 的 SFTP 功能上传了主题文件。

由于 ecs-user 账户权限有限,所以先上传到 /home/ecs-user,然后再用 sudo mv 命令移动到 /var/www/your_domain/wp-content/themes。

接着用 sudo unzip 解压到压缩包所在目录下。

导出/导入数据库

# 导出数据库
sudo mysqldump -u wordpress_admin -p --databases wordpress > ~/wordpress.sql

# 导入数据库
sudo mysql -u wordpress_admin -p < ~/wordpress.sql

问题记录

在 WordPress 后台启用主题之后,刷新页面报错:

There has been a critical error on this website. Please check your site admin email inbox for instructions.

Learn more about troubleshooting WordPress.

解决方法:将主题文件夹重命名即可。但是这样的话就没法用这个主题了,这样并不能从根本上解决问题。

参考文章:https://kinsta.com/knowledgebase/there-has-been-a-critical-error-on-your-website/。

问了一下客服,让按照 https://themebetter.com/wp-debug.html 的方法,开启 DEBUG 模式。

开启之后,报错信息如下:

Fata error: Uncaugnt Error: call to undefined function simplexml_load_string() in
/var/www/your_domain/wp-content/themes/mok/inc/update.php:72 Stack trace: #0
/var/www/your_domain/wp-content/themes/mok/inc/update.php(4): get_latest_theme_version() #1
/var/www/your_domain/wp-includes/class-wp-hook.php(324): update_notifier_menu() #2
/var/www/your_domain/wp-includes/class-wp-hook.php(348): WP Hook->apply_filters() #3
/var/www/your_domain/wp-includes/plugin.php(517): WP Hook->do_action() #4
/var/www/your_domain/p-admin/includes/menu.php(161): do_action() #5
/var/www/your_domain/wpadmin/menu,php(422): require_once('...') #6
/var/www/lxklsh,com/wp-admin/admin.php(158):reguire('...') #7
/var/www/your_domain/wp-admin/index.php(10): require_once('...') #8
{main} thrown in /var/www/your_domain/wp-content/themes/mok/inc/update.php on line 72

客服说是没有装 PHP 的 simplexml 扩展,装了之后就没问题了。

相关资料

整体流程参考 https://www.digitalocean.com/community/tutorials/how-to-install-linux-nginx-mysql-php-lemp-stack-on-ubuntu。

· 6 min read
He Wei

前情提要

接手了一个应用,是别人用 React Native + Expo 开发的,现在需要在自己的电脑上继续开发。

一开始以为只需要用 pnpm 装一下依赖,然后运行 npm start 就可以了,结果项目虽然能跑起来,但是不知道怎么在电脑浏览器中调试。

项目运行起来之后,控制台显示一个二维码,说用 Android 上的 Expo Go 或者 iPhone 的摄像头扫这个二维码就可以。

结果用 iPhone 扫码并调起 Expo Go 之后,APP 里显示一大堆报错信息,那就上网先搜搜教程。

安装依赖

安装配置 React Native 环境

React Native 基于Expo开发(一)项目搭建 这篇文章中,说先要配置 React Native 的环境。

于是安装文章中提供的 搭建开发环境,先把 Android Studio 装上了。

如果用 Google 到的国外地址,下载起来要么很慢,要么下载链接打不开。上网搜索了一下,还是在 V2EX 找到了好办法,就是去 Android Studio 的 中国官网 下载,速度飞快。

下载完成之后就安装 Android Studio,在安装前是可以配置 Proxy 的,配置好之后下载需要的 SDK、模拟器镜像什么的就不会卡住了。

然后就是 安装 Android SDK,不过如果在前一步安装好 Android Studio 之后按照提示安装过了 SDK,就不用再安装了。

最后是 配置 ANDROID_HOME 环境变量,以及 把一些工具目录添加到环境变量 Path

配置完成之后,启动 Android Studio,在 More Action 下拉菜单中,点击 Virtual Device Manager,能够看到已经安装的 Android 模拟器,这次默认安装的是 Pixel_3a_API_34(Android 14)。

运行模拟器里的 Chrome 浏览器,可以正常打开网站,说明模拟器没问题。

安装配置 Expo 项目

接着就是安装配置 Expo 项目了。

Expo 官网 上,有详细的安装步骤。不过这次是 clone 的现有项目,所以流程不太一样。

把项目下载过来之后,先用 yarn 安装依赖。注意这里不要不要用 pnpm 安装依赖,,可能是软链接的原因,始终报错 None of these files exist node_modules\expo\AppEntry node_modules\expo\AppEntry\index。按照 这里 的方法,用 npx expo start --clear 命令启动项目也还是报这个错误,最后想了想,删除了 node_modules 目录,再用 yarn 安装依赖,就没有问题了。

然后运行 yarn start,控制台会显示一个二维码,下面还有一些可用的命令。

因为这次是在模拟器上调试,所以用 a 命令启动 Android 模拟器,模拟器上会自动打开 Expo Go APP,并且加载项目。

在加载项目的过程中,还会下载一些依赖,所以 Android Studio 的 Proxy 配置不要关掉,保持开启即可。

功能开发

页面跳转

一开始照着现有的代码,新增了一个跳转语句,想着用户在点击按钮之后,直接跳转到自己要开发的页面。结果控制台报下面的错误:

The action 'NAVIGATE' with payload {"name":"Reward"} was not handled by any navigator.

Do you have a screen named 'Reward'?

又看了一下 React Native 基于Expo开发(三)路由,跳转 这篇文章,发现 MainStackScreen.js 这个文件里有 import stacks from './index' 这么一条语句,从 index 文件里引入了项目用到的所有页面。

再查看自己本地的项目,index.js 里下面两句应该是注册了整个程序,类似于 Vue 的初始化。

import App from "./App";
registerRootComponent(App);

再打开 App.tsx 文件,发现 import Navigations from './src/navigations/Navigations'; 这句引入了 Navigations 文件。

再打开 Navigations.tsx 文件,发现这里面引入了所有页面,然后用 createStackNavigator 创建了一个 Stack,在 Stack 里注册了所有页面,包括页面的名称和一些其他参数。

比如有 <Stack.Screen name="EmailLogin" options={{ headerShown: false }}> 这么一个页面定义,那么就可以用 navigation.navigate('EmailLogin') 来跳转到这个页面了。

· 3 min read
He Wei

需求

由于现在的项目分布在不同的 GitHub 账号下,如果在本地的 Git 全局配置中记录其中一个 GitHub 账号的信息,那么在与 GitHub 同步另一个账号下的项目时,每次都会弹出烦人的对话框,询问要选择哪个 GitHub 账号进行同步。

解决过程

上网搜索了一下,得知 GitHub 官方就提供这种解决方案。

简单来说就是在本地新建一个 SSH key,把私钥添加到本地的 ssh-agent,再把公钥添加到 GitHub 对应的账号下面。然后用 GitHub 项目的 SSH 链接来 fork 项目,之后在与 GitHub 同步项目的时候,就不会弹出烦人的对话框了。

新建 SSH key

参考 Generating a new SSH key and adding it to the ssh-agent,在 ~\.ssh 目录下面执行命令 ssh-keygen -t ed25519 -C "your_email@example.com" 一路回车,按默认设置来即可。

如果不按默认设置来,手动修改了生成的 SSH key 的名称,那么在后面将私钥添加到本地的 ssh-agent 这一步时会失败。

将私钥添加到 ssh-agent

执行 Adding your SSH key to the ssh-agent 这里的步骤即可。

将公钥添加到 GitHub

Adding a new SSH key to your GitHub account 这里的流程来即可。

测试 SSH 配置是否有效

Testing your SSH connection 这里的步骤操作即可。

如果报错,可以先在官方文档的 Troubleshooting SSH 这一节查找对应报错信息。

有时候因为众所周知的网络原因,执行测试命令失败,可以按照 这里 的方法配置一下 SSH,然后再测试,应当就 OK 了。

注意

有时候将一台电脑上生成的 SSH key 复制到另一台电脑上,再按照上面的流程配置,发现不能用。那就按照上面的流程重新生成新的 SSH key,再把公钥添加到 GitHub 即可。

· 9 min read
He Wei

前情提要

之前在 V2EX 咨询过 阿里云免费 SSL 证书的替代方案,考虑到阿里云服务器目前的操作系统是 Windows Server 2012,所以基于 Linux 或者 Docker 的方案就都 pass 了。

再考虑到自动化更新 SSL 证书的需求,所以在经过一番调研之后,最终确定使用 win-acme 来完成这一工作。因为虽然 Caddy 也能完成这项工作,但是还需要把在 IIS 中配置好的网站再重新配置一遍,还不知道会有什么新问题。本着尽量不要增加复杂度的理念,所以就没有采用 Caddy。

由于网站前面还有一层阿里云 WAF(Web 应用防火墙),各网站的流量都是先由 WAF 检查一遍,过滤掉非法请求之后,才能最终到达服务器。而要使用 WAF 的话,各域名的 DNS 都是解析到 WAF 的地址上的,这也给后面的工作和问题排查带来了一些问题,不过这是后话了。

基本流程测试

把 win-acme 下载并解压到服务器上之后,运行程序,按照网上的教程一步步操作。为了不影响现有各域名上的业务,在阿里云服务器的 IIS 上配置了一个新的域名,并且在阿里云 WAF 里面接入了这个二级域名。

按照教程的操作步骤,成功用 win-acme 申请到了这个二级域名的 SSL 证书,但是默认步骤只会生成一个 Windows IIS 所需的 pfx 格式的证书。如果要让阿里云 WAF 能够正常过滤 HTTPS 流量,还需要上传证书和对应的私钥到阿里云的数字证书管理服务中,然后在阿里云 WAF 的网站接入设置中选择所上传的证书才行。这样一来,还需要让 win-acme 生成 PEM 格式的证书和私钥。

所以基础流程就是:申请证书 → 保存 PFX 格式证书 → 保存 PEM 格式证书。

配置阿里云 WAF

在用 win-acme 给各个域名申请证书的时候,在验证域名所有权的那一步,有的域名能够验证成功,有的域名就总会失败。考虑到 IIS 上各个域名的配置是一样的,又看了一下 DNS 解析也是一样的格式,那应该就是阿里云 WAF 的问题了。

对比之后发现,有的域名在阿里云 WAF 的配置中同时勾选了 HTTP 和 HTTPS 协议,但是有的只勾选了 HTTPS 协议。对于只勾选了 HTTPS 协议的域名,有的还没有开启 HTTP 到 HTTPS 的强制跳转。

加上域名的 DNS 解析是指向阿里云 WAF 的,于是猜测是 WAF 这里的设置导致了域名所有权的验证失败。于是给全部域名的 WAF 配置都同时勾选了 HTTP 和 HTTPS 协议,并且禁止了 HTTP 到 HTTPS 的强制跳转,这个时候,各个域名的所有权验证终于都能通过了。

Debug 阿里云 Cli

有了 win-acme,SSL 证书的自动续期就搞定了。但是证书每次续期之后,还需要再把新的证书上传到阿里云的数字证书管理服务中,然后在阿里云 WAF 的网站接入设置中更新证书。既然是 Windows 系统,那就用 PowerShell 写个脚本来实现这个操作好了。

而上面的需求需要调用阿里云的 API,如果想要用脚本来实现这一功能,就还要用到阿里云 Cli。在配置阿里云 Cli 的用户凭证时,AccessKey 凭证和 EcsRamRole 凭证方式一开始都有问题,最后把之前的配置信息都删了,新建了一个具有 管理云盾应用防火墙(WAF)的权限 的用户,然后用该用户的 AccessKey 才终于成功调用了阿里云的 API。

Debug PowerShell

在测试阿里云 API 的调用时,还遇到了一个有些隐蔽的问题,就是在阿里云的 OpenAPI 平台上测试接口调用的时候是没问题的,但是在 PowerShell 脚本中调用就是有问题。到了最后把两种方式的完整命令复制出来对比,才发现 PowerShell 的 Get-Content 命令读取到的 SSL 证书和私钥的文本,换行符都没了,这尼玛!

解决了这个问题后,续期后的新证书和私钥自动上传至阿里云并在 WAF 中更新的功能也就实现了,以后这项工作就不需要自己再手动操作了。

总结

最后再说一下完整的流程和注意事项吧。

  1. 用 win-acme 申请 SSL 证书并实现自动续期。这个程序能够自动把从 IIS 中读取网站信息,申请新证书关联到对应的网站,很省心。
  2. 如果用到了阿里云 WAF 之类的服务,要想防护 HTTPS 流量,就需要上传 SSL 证书和私钥,这样的话需要配置 win-acme 再额外保存一份 PEM 格式的证书和私钥。其中 -chain.pem 后缀的文件是需要导入 WAF 的网站证书及中间证书,-key.pem 后缀的文件则是需要导入 WAF 的私钥文件。
  3. 如果用到了阿里云 WAF 之类的服务,并且各业务域名的 DNS 都解析到了 WAF 上,那么就要在 WAF 配置中同时启用 HTTP 和 HTTPS,并且禁止 HTTP 到 HTTPS 的强制跳转,不然有可能在域名所有权验证那一步失败。
  4. 配置阿里云 Cli 的时候,如果配置的各种凭证方式都不管用,可以尝试把旧的配置都删除,包括 Cli 里的配置和阿里云网页端控制台的配置,然后重新来一遍。
  5. PowerShell Get-Content 命令拿到的证书和私钥的文本会丢失换行符,可以参考 https://stackoverflow.com/a/15041925/2667665 这里的方法来解决。
  6. 上传证书和私钥调用的是 WAF 中的 CreateCertificate 这个 API,将证书关联至 WAF 中接入域名是 CreateCertificateByCertificateId API。

背景知识