编写免注册COM组件的正确姿势

注意:此文假定你对Windows编程和微软的COM(Component Object Model组件对象模型)技术有基本的了解。并且知道ATL(Active Template Library)这个简化COM组件编写工作的库。

这里记录折腾免注册COM组件碰到的坑作为备忘。

COM技术是微软发布的一套应用程序二进制接口的规范。用COM的规范导出DLL文件想要提供的API,可以保证发布出去的新版本DLL在应用程序二进制接口(ABI)上与旧版本保持一致。所以windows下要做一套提供可升级的API的动态库,COM是个很不错的选择。但是微软开的脑洞实在是太大了,它想这个二进制兼容的接口规范实现系统级的共享。这意味着一般情况下要成功创建一个COM对象,首先这个COM组件要在注册表里注册,我们才能用CoCreateInstance函数正确创建出来这个对象。

这很不绿色很不环保!!首先把放COM对象的dll删了,注册表里还有残留痕迹。其次,注册了之后这个dll的位置不能变,DLL的位置改了之后还得重新注册。这些COM组件的注册数据放在哪里,可以随便点开一个注册表中路径是HKEY_CLASSES_ROOT\CLSID\{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}的项,能看到每个COM组件库的CLSID和都是和DLL绝对路径绑定在一起的。这些特性对绿色程序来说是不可接受的。

好在Windows XP之后微软引入了免注册COM组件的概念,但是相关的文档却很难找,google了registration-free COM关键字只找到了微软让你在DLL中嵌入Manifest文件的办法,或者通篇都在谈.NET和VB怎么弄,完全没有用c艹和ATL怎样操作的指南。我试了,照那个文档里说的仅仅把普通的manifest文件编译到DLL中是不够的。用google各种找文章在一篇博客的指引下终于找到了官方手把手教你编写免注册COM组件的文章:
https://docs.microsoft.com/en-us/previous-versions/dotnet/articles/ms973913(v=msdn.10)#rfacomwalk_topic6

照文档做的时候踩了不少坑,这里把正确的方法记录下来。

首先在Visual Studio下新建个c++控制台应用程序,然后再新建个用ATL的DLL,用VS提供的ATL模板在ATL工程里创建两个COM对象。然后在idl文件中给两个对象对应的接口分别添加一个方法。假设这之后ATL DLL项目包含的idl文件内容如下:

// RegFreeComLib.idl : IDL source for RegFreeComLib
//

// This file will be processed by the MIDL tool to
// produce the type library (RegFreeComLib.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(e237510b-fce2-48f1-ac00-e29b28ec0ddd),
    pointer_default(unique)
]
interface ITestObj1 : IUnknown
{
    [id(0), helpstring("returns \"Foo\"")] HRESULT Foo(BSTR* pVal);
};
[
    object,
    uuid(1f696df2-9532-4234-89af-1afacdc558bc),
    pointer_default(unique)
]
interface ITestObj2 : IUnknown
{
    [id(0), helpstring("return \"Bar\"")] HRESULT Bar(BSTR* pVal);
};
[
    uuid(fe8885f8-5ffb-4bdf-b738-0642d17d02a5),
    version(1.0),
]
library RegFreeComLibLib
{
    importlib("stdole2.tlb");
    [
        uuid(a75c6fc7-6656-424e-b007-b65553a45b30)
    ]
    coclass TestObj1
    {
        [default] interface ITestObj1;
    };
    [
        uuid(89c74fb1-dbb5-4b70-a48e-4706f6e3cd74)
    ]
    coclass TestObj2
    {
        [default] interface ITestObj2;
    };
};

import "shobjidl.idl";

ITestObj1::Foo会返回字符串值FooITestObj2::Bar会返回字符串值Bar

之后我们要调用CoCreateInstance创建这两个COM对象,在主程序中写下如下代码:

#include <iostream>

#include "RegFreeComLib_i.h"

#include <atlbase.h>
#include <atlcom.h>
#include <windows.h>

int main()
{
    CoInitialize(NULL);

    // 这里要包起来,否则CoUnInitialize执行之后CComPtr指向的COM对象所属的DLL早已被卸载了,程序会崩溃
    {
        HRESULT hr = S_OK;
        ATL::CComPtr<ITestObj1> pTestObj1;
        // 查找已注册的COM组件中是否有类TestObj1的clsid,接口的guid是ITestObj1的guid,
        // 如果有就能成功创建这个COM对象,对象的指针存放在pTestObj1中
        hr = CoCreateInstance(__uuidof(TestObj1), NULL, CLSCTX_ALL, __uuidof(ITestObj1), (void**)&pTestObj1);

        ATL::CComPtr<ITestObj2> pTestObj2;
        hr = CoCreateInstance(__uuidof(TestObj2), NULL, CLSCTX_ALL, __uuidof(ITestObj2), (void**)&pTestObj2);

        if (pTestObj1 && pTestObj2)
        {
            ATL::CComBSTR str1;
            pTestObj1->Foo(&str1);
            ATL::CComBSTR str2;
            pTestObj2->Bar(&str2);
            // COM对象使用的字符串类型都是wchar_t的
            std::wcout << str1.m_str << ' ' << str2.m_str << std::endl;
        }
    }

    CoUninitialize();
}

编译,运行。记得把ATL DLL项目的生成后注册组件的选项关掉(Linker -> General -> Register Output设成false),否则运行regsvr32注册失败算编译失败。
因为编译好的ATL DLL还没有在注册表中登记,所以创建COM组件的操作一定会失败。

要把我们编写的dll变成免注册的COM组件,需要在应用程序附带的清单文件上做手脚,把我们编写的COM对象在DLL包含的Manifest文件中声明出来。在dll工程下新建一个Manifest文件,参照IDL文件写入以下内容:

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
  <!--如果Manifest文件要嵌入到DLL中,那么name的值必须是文件名-->
  <!--版本号必须是mmmmm.nnnnn.ooooo.ppppp的格式-->
  <assemblyIdentity type="win32" name="RegFreeComLib" version="1.0.0.0" />
  <!--必须与DLL文件名(包含扩展名)保持一致-->
  <file name = "RegFreeComLib.dll">
    <!--CLSID_TestObj1-->
    <comClass clsid="{a75c6fc7-6656-424e-b007-b65553a45b30}" threadingModel="Apartment" />
    <!--CLSID_TestObj2-->
    <comClass clsid="{89c74fb1-dbb5-4b70-a48e-4706f6e3cd74}" threadingModel="Apartment" />

    <typelib tlbid="{fe8885f8-5ffb-4bdf-b738-0642d17d02a5}" version="1.0" helpdir=""/>
  </file>

  <!--原教程里的这个没有看懂是什么意思,但是删掉之后免注册的机制也能正常工作-->
  <!--<comInterfaceExternalProxyStub
      name="ISideBySideClass"
      iid="{[IID_ISideBySideClass]}"
      proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
      baseInterface="{00000000-0000-0000-C000-000000000046}"
      tlbid = "{[LIBID_SideBySide]}" />-->
</assembly>

我们的主程序要引用这些免注册COM组件,也要为主程序指定一个Manifest文件:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
  manifestVersion="1.0">
  <dependency>
    <!--文件名和版本号要和刚才的COM dll包含的Manifest文件中的assemblyIdentity声明保持一致-->
    <dependentAssembly>
      <assemblyIdentity type="win32" name="RegFreeComLib" version="1.0.0.0" />
    </dependentAssembly>
  </dependency>
</assembly>

然后要把编写好的清单文件嵌入到exe和dll中:

按F5运行之后竟然提示了这个,WTF???

事件查看器可以帮助我们排错。开始菜单 -> 运行 输入eventvwr,打开事件查看器。在Windows日志 -> 应用程序栏中能看到刚才程序启动出错的信息。这里看到错误提示是免注册COM dll附带的清单文件中不允许出现requestedPrivileges这个节。

用Resource Hacker打开生成的DLL,看到生成出的Manifest文件确实是带了不该带的东西。

解决办法是在DLL的项目属性中关掉生成Manifest文件时指定UAC操作的选项,打开 项目属性 -> 链接器 -> 清单文件,把 启用UAC 这个选项设成 否。在此要吐槽VS把一些选项藏得太深了,不好找,有的甚至要翻vcxproj才能找到设置。

再编译运行,就能看到程序正确输出了Foo Bar,证明程序成功调用了免注册的COM组件。

附注

  • QQ的windows客户端大量应用了COM技术。用PE查看器打开QQ安装目录下的DLL文件,能看到大多数DLL都只有四个导出函数:DLLRegisterServer,DLLUnRegisterServer,DLLGetClassObject,DLLCanUnloadNow。但是奇怪的是资源区段里面的Manifest并没有像文中这样导出接口。我这里不负责任地猜测QQ客户端应该是在内部模拟了windows加载COM组件的流程,大致应该和这里描述的方法差不多。
  • ABI不兼容会造成什么后果,可以见这两篇文章。虽然我不同意第二篇文章说微软COM是反面教材的看法。
  • 因为ABI的差异,很多用C艹实现的项目要做一些额外的工作。比如说Qt的安装包里要提供通过用多种编译器编译好的Qt动态库(vc2015/vc2017/mingw64),不同编译器编译出来的Qt DLL不能混用;windows下给Node.js写原生动态库,编译好的库要链接上指定版本Node.js对应的.lib文件,在JS里面require了版本不同的原生库,就会提示was compiled against a different Node.js version不让你加载这个模块。

本站现已支持HSTS Preload

本站现已支持HSTS Preload,且已经在https://hstspreload.org/提交了preload审核。
申请的时候发现了nginx需要改一下配置

-add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

之前写的也订正了。

还有站点的首页从nginx示例页稍微改了一下,我真的不会HTML/CSS/JavaScript,所以首页看起来非常lowb。
就这样。

libstdc++的std::regex的一个坑

公司用Electron做的项目有个需求,前端的JS代码会把一张图片转成dataURL的格式传给cpp写的库,cpp把dataURL中的二进制数据提取出来通过自己的协议发给服务器。
用dataURL可以把文件的内容嵌入到URL字符串中。比如这段字符串复制到浏览器地址栏然后按回车,就可以看到一张红色的GIF图片。

data:image/gif;base64,R0lGODlhZABkAJEAAAAAAP////8AAP///yH5BAEAAAMALAAAAABkAGQAAAJzlI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8YhMKpfMpvMJjUqn1Kr1is1qt9yu9wsOi8fksvmMTqvX7Lb7DY/L5/S6/Y7P6/f8vv8PGCg4SFhoeIiYqLjI2Oj4CGlYAAA7

用DataURL的好处是较小的文件可以直接嵌入到HTML中。如果这个文件在网站中出现的次数很少,直接用dataURL就可以不用单独管理这个文件了。

所以就有个需求,判断给定的字符串是不是dataURL的类型。我想当然地就用了std::regex来判断字符串是不是dataURL,之后用std::regex_search来提取URL包含的MIME类型和包含的数据。写了这么一个正则表达式:

data:([a-zA-Z0-9/\+\-\.]*)(;(base64))?,(\S*)

当时没考虑到传过来的dataURL可能会很长,大小可能会有几个MB。之后QA说程序在linux下崩溃了,我挂上调试器按照同样的步骤重现了崩溃之后,看到了这么一个调用栈:

很明显regex_search在提取字符串的时候把调用栈弄爆了。查了libstdc++的std::regex里面的实现是用递归的,难怪容易爆栈。
https://www.zhihu.com/question/23070203/answer/84248248
改成只用regex解析文件头来判断就不崩溃了,dataURL里面包含的数据和URL头部的分界是个逗号,用第一个逗号字符出现的位置来分割应该没什么问题。

虽然程序崩了但是还是要膜这位实现出正则表达式引擎的Googler……

还有Visual Studio的linux调试器是个好东西。。。

在Debian 9环境下配置基于LNMP的WordPress博客

这其实是10月份写的文章。原来的blog在DigitalOcean上,因为ping实在是太高了,SSH和传文件很不爽,所以换了一家VPS服务商。新的VPS用的Linux发行版是openSUSE也不是Debian了。但是配置过程还是大同小异的。

因为URI改了,所以原来的文章没法迁移到这(你这懒癌晚期患者真的写过什么blog文章吗?),改备份的SQL真是改到吐血。好在原来的markdown还在,所以重新导入一下就可以。然后在文章里面补充了一些新问题的解决办法。

==========原文==========

鸽了一个月,总算把博客用wordpress搭起来了。把折腾部署网站的过程记下来,做个备忘。

网站用的是nginx服务器,OS是debian9,虽然说有懒人版的lnmp脚本,但是我把手动部署的过程走了一遍。
搭建的过程主要参考了以下文章:
https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-nginx-on-ubuntu-14-04

新建一个带 sudo 权限的用户

以root身份登录服务器,新建一个带sudo权限的用户,然后切换到该用户。

adduser demo
usermod -a -G sudo demo
su demo

安装PHP nginx MySQL

安装之前先更新下服务器预装的软件包。然后安装环境。

sudo apt-get update && apt-get dist-upgrade

首先安装以下软件包:
nginx mariadb-server php php-gd php-mysql php-fpm php-ssh2

  • php-fpm: php-fastcgi的一个实现。
  • php-gd: 为php语言添加处理图像数据的支持。
  • php-ssh2: 使得wordpress能通过ssh方式访问本地服务器。

运行命令 sudo apt-get -t stretch-backports install nginx php php-gd php-mysql php-fpm php-ssh2 mariadb-server 来安装。

如果提示找不到stretch-backports仓库,在/etc/apt/sources.list文件中添加以下两行:

deb http://http.us.debian.org/debian/ stretch-backports main contrib non-free
deb-src http://http.us.debian.org/debian stretch-backports main contrib non-free

然后 sudo apt-get update 刷新一下就可以安装了。

apt-get 会顺带把Apache(软件包名字叫apache2)也安装上,这里我们不用Apache做服务器。由于先安装了nginx并启动了服务,Apache安装成功之后它的服务也会启动失败。因此先输入命令sudo systemctl disable apache2.service把Apache服务关掉。

配置全站https

安装完nginx,在浏览器中输入http://<你的VPS的IP>/ 应该能看到nginx的欢迎画面。

为了让网站的内容不会被运营商和功夫网视奸,进而水表不保。我们先把网站弄上https,并且让网站的SSL安全等级达到SSLlabs网站的A+评价。

获取给域名颁发的SSL证书

这里选择LetsEncrypt作为网站SSL证书的颁发者。首先在DNS控制台里把拥有的域名指向我们的服务器:

安装Certbot帮助我们自动签发LetsEncrypt证书,输入命令:

sudo apt-get install python-certbot-nginx -t stretch-backports

输入配置Certbot自动签发的命令:

sudo certbot certonly --authenticator standalone --pre-hook "systemctl stop nginx.service" --post-hook "systemctl start nginx.service"

配置期间会让你输入以下信息:
– 用于签发证书的邮箱
– 要签发证书的域名

输入后一路按回车,SSL证书应该就签发好了。

检验Certbot是否能自动续签证书

https://certbot.eff.org/docs/using.html#automated-renewals

具体方法是输入命令systemctl list-timers 可以看到有个计时器名叫certbot.timer,证明自动续签定时器是有效的。
要检验证书续签是否有效,输入命令:

sudo certbot renew --dry-run

配置nginx服务器应用https

打开文件 /etc/nginx/sites-enabled/default,添加或把现有配置修改为以下内容:

# 监听80端口,把所有的http请求通过返回301值重定向到https上
server {
    listen 80 default_server;              # IPv4
    listen [::]:80 default_server;         # IPv6
    server_name http_redirect;             # 服务器配置名
    return 301 https://$host$request_uri;  # http请求全都通过301重定向到同名的https URL上
}
# 监听443端口,提供https服务
server {
    listen 443 ssl;                        # 监听443端口,且开启ssl
    listen [::]:443 ssl;                   # v6

    # 使用的SSL 证书,这里用之前letsEncrypt签发证书存放的位置
    ssl_certificate /etc/letsencrypt/live/<your domain here>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<your domain here>/privkey.pem;

    # 复用SSL连接
    ssl_session_cache          shared:SSL:1m;
    ssl_session_timeout        5m;

    # SSL加密方式这里用的是nginx的默认设置
    ssl_ciphers                HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    ssl_protocols TLSv1.2;                 # 只支持TLS v1.2协议

    # 访问日志
    access_log  /var/log/nginx/host.access.log;
    error_log   /var/log/nginx/host.error.log;

    # 开启 HSTS 支持
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    server_name https_web;

    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }
}

要达到A+级评分,还需要一个强度较高的用于DH密钥交换的key。否则评价会限制在B级上。

首先输入命令 openssl dhparam -out /etc/nginx/dhparam.pem 2048 生成一个新密钥。
然后打开/etc/nginx/nginx.conf,在http栏添加配置:

ssl_dhparam dhparam.pem;

以上步骤都完成之后,输入systemctl restart nginx.service 重启nginx服务。
这时在浏览器里面输入域名,应该能正确跳转到https连接,并且证书是有效的,在SSLlabs网站上检测也能拿到A+的评价。

配置MySQL(MariaDB)服务器

输入命令mysql -u root -p以root用户身份登录MariaDB服务器,提示输入密码时输入root账户的密码,登录到MariaDB数据库。

我们现在要为WordPress新建一个数据库,然后建立一个新用户供WordPress程序使用。该用户只能读写WordPress数据库中的内容。

为了保证数据库的安全,先移除掉MariaDB安装后的一些默认设置。
运行脚本:

mysql_secure_installation

脚本会提示你重新设置root密码,移除掉匿名用户,禁止root用户远程登录MariaDB服务器,移除MariaDB安装时附带的测试数据库。

之后配置数据库。
输入以下命令:

create database wordpress;
create user wp@localhost identified by 'dontusethispassword'; -- 只允许本地登录(不要用这个密码!)
grant all privileges on wordpress.* to wp@localhost; -- 给用户 wp 对数据库 wordpress 所有的操作权限 
flush privileges; -- 刷新权限配置

配置完成,输入\q退出MySQL控制台。

配置WordPress网站并初始化博客网站

下载WordPress最新版本

从WordPress 官方网站下载最新版本的Wordpress压缩包,放置在本地目录并解压:

cd ~
curl -O https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz

设置WordPress的wp-config.php

进入解压出来的WordPress目录,将示例配置文件wp-config-sample.php复制一份,命名为wp-config.php

cd wordpress
cp wp-config-sample.php wp-config.php

打开wp-config.php,将以下内容

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'database_name_here');

/** MySQL database username */
define('DB_USER', 'username_here');

/** MySQL database password */
define('DB_PASSWORD', 'password_here');

修改为:

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'wordpress');      // WordPress要使用的数据库名

/** MySQL database username */
define('DB_USER', 'wp');                      // MySQL数据库用户名

/** MySQL database password */
define('DB_PASSWORD', 'dontusethispassword'); // 之前建立的用户 wp 的密码。

https://api.wordpress.org/secret-key/1.1/salt/ 获取存储用户验证信息时要加的盐(不要原样复制这里的数据!):

define('AUTH_KEY',         '/ (+4E5i---qv ^ql_!6SYfy1 <DO_NOT_COPY_VALUES_HERE> S w!GFli/}0mGA-5X!H5MgophTWq8*$#-s|Nt<O');
define('SECURE_AUTH_KEY',  'zyJ0+.lntK(pmoLz}{&2r4hIZ <DO_NOT_COPY_VALUES_HERE> PSU+$;OwVmZ-M-~FMOCOsCcqui9Q^mhUG*V]F=M');
define('LOGGED_IN_KEY',    'FqO}:DqL&_(YEh(G;90u_?]9h <DO_NOT_COPY_VALUES_HERE> .$)>:G`i.%(Imechhw@]lHuaF3Mvqt<q%NhM>Kj');
define('NONCE_KEY',        'ENG`i~-XP}o|-qe]tks~FVB@5 <DO_NOT_COPY_VALUES_HERE> ~;p;SVy6A)kNEqZ9Mw(Rgm`w={N&>*KXK)eh_|;');
define('AUTH_SALT',        'xgMO]-tb^*YZhRG(sU0N{u2Wi <DO_NOT_COPY_VALUES_HERE> 9$0B0/SpB}|C6(,+deb];=RYb=-R!d$_& 9qHDr');
define('SECURE_AUTH_SALT', ')-/GgH`>p6SU2bx*)nV?=}+3c <DO_NOT_COPY_VALUES_HERE> 2L6#8Pqn0@-bRm&D}T+2L.+nA638|y+|:`zF,4T');
define('LOGGED_IN_SALT',   '+Y|Pm`ExaNg|KoU<h`D,Kaw0u <DO_NOT_COPY_VALUES_HERE> b.QEa-=[g*<E/$EIBcOa^O|eNQFmnsB+D-w?:uW');
define('NONCE_SALT',       '_/4.-?-QB[)p0<U#M{MCqCum* <DO_NOT_COPY_VALUES_HERE> d!$bs4v%J:imVE^WRaLyZ 51m|&-ckqaV=dzvM_');

替换掉wp-config.php中原有的内容:

define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY',  'put your unique phrase here');
define('LOGGED_IN_KEY',    'put your unique phrase here');
define('NONCE_KEY',        'put your unique phrase here');
define('AUTH_SALT',        'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT',   'put your unique phrase here');
define('NONCE_SALT',       'put your unique phrase here');

保存并退出编辑器。

复制WordPress文件至网站根目录,配置目录权限

复制WordPress至网站根目录,然后进入该目录。

sudo rsync -avP ~/wordpress/ /var/www/wordpress
cd /var/www/wordpress

运行nginx进程的用户所在的组有www-data,让nginx能读写WordPress所在的目录:

sudo chown -R demo:www-data /var/www/wordpress/*

新建wp-content/uploads目录,然后设置权限,使得我们在网页端能上传文件。

mkdir wp-content/uploads
sudo chown -R :www-data ./wp-content/uploads

配置nginx

打开/etc/nginx/sites-enabled/default文件,修改配置:

    root /var/www/wordpress;
    index index.php index.html index.htm index.nginx-debian.html;

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

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

    location ~ /\.ht {
        deny all;
    }

最终修改完成的配置文件如下:

server {
    listen 80 default_server;              # IPv4
    listen [::]:80 default_server;         # IPv6
    server_name http_redirect;             # 服务器配置名
    return 301 https://$host$request_uri;  # http请求全都通过301重定向到同名的https URL上
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    # SSL configuration
    #
    # listen 443 ssl default_server;
    # listen [::]:443 ssl default_server;
    #
    # Note: You should disable gzip for SSL traffic.
    # See: https://bugs.debian.org/773332
    #
    # Read up on ssl_ciphers to ensure a secure configuration.
    # See: https://bugs.debian.org/765782
    #
    # Self signed certs generated by the ssl-cert package
    # Don't use them in a production server!
    #
    # include snippets/snakeoil.conf;
    # 使用的SSL 证书,这里用之前letsEncrypt签发证书存放的位置
    ssl_certificate /etc/letsencrypt/live/<your domain here>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<your domain here>/privkey.pem;
    # 复用SSL连接
    ssl_session_cache          shared:SSL:1m;
    ssl_session_timeout        5m;
    # SSL加密方式这里用的是nginx的默认设置
    ssl_ciphers                HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    ssl_protocols TLSv1.2;                 # 只支持TLS v1.2协议

    # 访问日志
    access_log  /var/log/nginx/host.access.log;
    error_log   /var/log/nginx/host.error.log;

    # 添加 HSTS 支持
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    server_name https_web;

    root /var/www/wordpress;

    # Add index.php to the list if you are using PHP
    index index.php index.html index.htm index.nginx-debian.html;

    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }

    # pass PHP scripts to FastCGI serve
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # With php-fpm (or other unix sockets):
        fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
        include fastcgi_params;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    location ~ /\.ht {
        deny all;
    }
}

重启nginx和php-fpm服务:

sudo systemctl restart nginx.service
sudo systemctl restart php7.0-fpm.service

访问网站完成WordPress配置

这时在浏览器地址里面输入你的域名,应该能显示WordPress的安装页了。输入站点名、用户名、密码、邮箱,wordpress会自动帮你配置好其余的东西。

然后WordPress会提示你登录,登录后进入WordPress管理员页面。


这时WordPress的配置就完成了。

排错

遇到网页打不开的情况时,可以打开wp-config.php,然后打开调试开关:

define('WP_DEBUG', true);
define('WP_DEBUGLOG', true);

之后可以在wp-content/debug.log找到调试日志。

碰到css加载不出来之类的问题,去翻/var/log/nginx/host.access.log/var/log/nginx/host.access.log这两个文件。

后续配置

博客启动后的后续配置目前碰到了一些坑,这里记录已经遇到的问题和解决办法。

在WordPress管理员页面不能安装插件,需要FTP/SSH用户凭据

如图,安装插件时WordPress提示需要FTP或者SSH凭据。

解决方法是在wp-config.php中加上:

define('FS_METHOD','direct');

然后运行命令,给组赋予写的权限

sudo chmod g+w <your wordpress dir>/wp-content/* -R

即可解决该问题。

我想更改管理员邮箱,但是收不到确认信件

原因可能是主机商把mail()禁用了。检验方法是进入WordPress登录页面,点忘记密码,然后输入你的管理员邮箱,点获取新密码。如果出现以下提示,提示mail()函数被主机商禁用,那么就是这种情况。

解决的办法除了有联系主机商提供支持之外,还可以使用第三方的SMTP服务。这里以gmail为例:

  • 安装 WP Mail SMTP by WPForms 插件。

  • 在插件设定页中指定你要发邮件的gmail邮箱:

  • 登录 https://console.developers.google.com/flows/enableapi?apiid=gmail&pli=1
    • 先选择一个Project,没有则新建一个。
    • 在Add credentials to your project页面配置如下:
    • 选择新建一个OAuth client ID:

    • 把生成出来的凭据填进插件设置页对应的输入框中:
    • 点保存设置,然后点击Allow plugin to send emails using your Google account,授权插件使用该gmail邮箱发送邮件。
    • 最后可以进入Email Test页发送邮件,测试发送服务是否已经生效。生效可能需要一段时间。

我更改了WordPress的Permalink设置,然后博客上所有的链接都404了

解决方法是在/etc/nginx/sites-enabled/default中的https服务器配置中添加以下配置(仅适用于把WordPress作为网站根目录的情况):

    if (!-e $request_filename) {
        rewrite ^.*$ /index.php last;
    }

WordPress上传不了大小超过2MB的东西

要更改PHP所允许的最大上传大小和nginx能接受的请求包的最大的大小。

  • 首先找到PHP加载的配置文件的位置,输入命令php -i | grep "Loaded Configuration File"
  • 这里得到php.ini的位置在 /etc/php/7.0/cli/php.ini
  • 修改 php.ini 中的配置项upload_max_filesizepost_max_size至想要的大小数值。这里设成64MB。
  post_max_size = 64M
  upload_max_filesize = 64M
  • 保存php.ini后,重启php7.0-fpm.service
  systemctl restart php7.0-fpm.serivice
  • 如果还是不行,那还需要修改nginx的配置。修改/etc/nginx/nginx.conf,在http栏加上配置:
  client_max_body_size 64M;
  • 之后重启服务nginx.service

参考资料

配置LNMP/wordpress: https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-nginx-on-ubuntu-14-04

签发Let’s Encrypt证书: https://certbot.eff.org/lets-encrypt/debianstretch-nginx

提升SSLlabs评价分数: https://michael.lustfield.net/nginx/getting-a-perfect-ssl-labs-score

安装插件: https://www.digitalocean.com/community/questions/wordpress-asking-for-ftp-credentials

Permalink设置:https://www.digitalocean.com/community/questions/404-when-using-pretty-permalinks-on-new-wordpress-site-on-lemp-nginx