为ThinkPHP6项目配置HTTPS,经常会遇到一种情况:证书装好、Nginx也做了301跳转,但页面上的静态资源还是走的http,登录状态也时好时坏。问题通常不在证书本身,而是框架从哪个字段判断当前请求是HTTPS,以及代理层有没有把原始协议原样传回来。所以先不要急着改业务代码,按下面的步骤排查。
对于ThinkPHP6项目,框架内部通过`Request::isSsl()`方法判断当前请求是否为HTTPS。该方法读取`$_SERVER['HTTPS']`,但使用Nginx等反向代理时,这个值往往缺失。如果项目部署在负载均衡或代理之后,则需要在Nginx的配置中强制添加`X-Forwarded-Proto $scheme`,并通过`$_SERVER['HTTP_X_FORWARDED_PROTO']`来判定。检查时,可以先在控制器中临时输出`var_dump(Request::isSsl())`,若返回`false`,再查看`$_SERVER`中是否有`HTTP_X_FORWARDED_PROTO`字段,确认代理请求头已保留。常见坑是只拦截`HTTP`请求,而没有考虑代理的透传,造成循环重定向。
在Nginx的80端口server块中,通常使用`if ($scheme = http) { return 301 https://$host$request_uri; }`来实现强制跳转。但要注意,如果项目位于代理之后,`$scheme`始终是`http`,此时需要判断`$http_x_forwarded_proto`。更稳妥的写法是:`if ($http_x_forwarded_proto != 'https') { return 301 https://$host$request_uri; }`,同时通过`proxy_set_header X-Forwarded-Proto $scheme;`转发原始协议。配置完成后,使用`curl -I http://example.com`,查看响应状态码是否为301…
先别急着改配置,确认框架能否感知HTTPS
对于ThinkPHP6项目,框架内部通过Request::isSsl()方法判断当前请求是否为HTTPS。该方法读取$_SERVER['HTTPS'],但使用Nginx等反向代理时,这个值往往缺失。如果项目部署在负载均衡或代理之后,则需要在Nginx的配置中强制添加X-Forwarded-Proto $scheme,并通过$_SERVER['HTTP_X_FORWARDED_PROTO']来判定。检查时,可以先在控制器中临时输出var_dump(Request::isSsl()),若返回false,再查看$_SERVER中是否有HTTP_X_FORWARDED_PROTO字段,确认代理请求头已保留。常见坑是只拦截HTTP请求,而没有考虑代理的透传,造成循环重定向。
这里的关键是“代理之后”的判断。如果你的Nginx同时承担443端口的SSL终止,那么$_SERVER['HTTPS']通常会被设为on;但如果是负载均衡器把443端口转发到后端Nginx的80端口,后端Nginx看到的就是普通HTTP,此时必须依赖HTTP_X_FORWARDED_PROTO。建议在入口控制器临时写一个dump($_SERVER),对照检查这几个字段,避免靠猜。
Nginx跳转规则要区分“直连”和“代理后”
在Nginx的80端口server块中,通常使用if ($scheme = http) { return 301 https://$host$request_uri; }来实现强制跳转。但要注意,如果项目位于代理之后,$scheme始终是http,此时需要判断$http_x_forwarded_proto。更稳妥的写法是:if ($http_x_forwarded_proto != 'https') { return 301 https://$host$request_uri; },同时通过proxy_set_header X-Forwarded-Proto $scheme;转发原始协议。配置完成后,使用curl -I http://example.com,查看响应状态码是否为301…
上面的if写法适用于只有80端口跳转的场景。如果同一台Nginx还处理443端口,那么需要在443的server块里确认proxy_set_header X-Forwarded-Proto $scheme;放在了location之前,避免被覆盖。同时要注意,$host会保留原始域名,如果域名本身带着端口,可能需要在跳转地址里显式加上server_port,但大多数情况下不需要。
URL生成协议不对,页面里的链接全是http
当页面需要生成绝对URL时,ThinkPHP6的url()函数会依据当前请求的域名与协议拼接。如果请求是通过代理转发的HTTPS,但$_SERVER['HTTPS']未设置,框架生成的链接仍会使用http。此时可以打开全局的Request对象,手动设置$request->setServer('HTTP_X_FORWARDED_PROTO', 'https')或直接修改$_SERVER['SERVER_PORT']为443。推荐在AppService::initialize()中统一处理,这样在任何地方生成URL都会带上正确的协议。检查方法:在模板中调用url('index/index'),渲染后用view-source查看属性,确认是https://开头。若出现协议不一致…
这里要补充一个容易忽略的点:Request::setServer()只在当前请求生命周期内有效,如果项目中有异步队列、定时任务,或者通过命令行生成的链接,就不会经过这个Request对象。所以更稳妥的做法是同时把对外域名写到配置里,而不是完全依赖请求头。下面这段代码可以放在AppService::initialize()里:
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}这样做的好处是框架内部的Request::isSsl()和url()都会感知到HTTPS,不用逐个业务方法去设置。但要注意,这段代码是基于“代理已经正确转发”的前提。如果代理没有转发X-Forwarded-Proto,上面的判断永远不会成立。
Cookie的Secure标志,开之前想清楚HTTP和HTTPS是否并存
启用HTTPS后,务必为session和cookie加上Secure标志,否则浏览器的开发者工具会提示"This cookie was not set with the Secure attribute"。在ThinkPHP6中,编辑config/cookie.php,将'secure' => false改为'secure' => true;同时确认config/session.php中的cookie_secure项也保持同步。但若项目同时运行在HTTP和HTTPS下,不要全局开启Secure,否则HTTP请求无法写入cookie,导致登录失效。此时可以编写中间件,在检测到Request::isSsl()时动态设置cookie参数。另外注意,Secure标志只保证cookie…
这里其实是在提醒你:不要因为上线HTTPS就无脑加Secure。如果只是测试环境中同时通过HTTP和HTTPS访问,建议先用中间件做动态判断,等确认HTTP流量全部清掉以后,再在配置文件里写死true。动态判断的核心是拿到Request::isSsl()的结果,再决定是否设置$config['secure']。
混合内容:图片不加载、样式丢失,先看Console的Mixed Content
页面切换为HTTPS后,浏览器会对所有http协议的静态资源、XHR请求进行拦截。最常见的表现是图片不加载、样式丢失。先在控制台的Console面板查看"Mixed Content"报错,它会明确列出被拦截的URL。修复时,尽量将静态资源引用改为相对路径(如/static/js/app.js)或协议相对路径(//cdn.example.com/xxx.js)。同时,检查页面里是否存在JavaScript硬编码的http://链接,例如在某些模板中写死http://的api地址。这属于配置盲区,在Nginx转发HTTPS之后往往被忽略。修改后可通过curl -k https://example.com抓取页面,再用grep过滤http://资源,逐一排查。
如果项目里用了CDN,协议相对路径通常能解决大部分问题,但要注意CDN是否支持HTTPS。如果CDN不支持,只能在HTTPS页面里继续使用http链接,这样仍然会被拦截。排查时建议先抓首页,再抓内页,因为很多API地址是异步请求里拼出来的。
最后,.env里自定义APP_DOMAIN并让config/app.php读取,适合需要稳定获取站点域名的场景。但要注意修改.env后,如果配置缓存未刷新,会影响生效,通常需要删除runtime/下的缓存文件。多环境共用一份配置时,特别容易出问题,建议在部署脚本里按环境写入.env。