diff --git a/deploy/configure-dingtalk-callback.sh b/deploy/configure-dingtalk-callback.sh index cfeccf1c..1f9fc172 100644 --- a/deploy/configure-dingtalk-callback.sh +++ b/deploy/configure-dingtalk-callback.sh @@ -39,7 +39,7 @@ probe="$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 15 \ "http://$SERVER_IP:8442/auth/dingtalk/callback?state=configuration-probe")" status="${probe%% *}" redirect_url="${probe#* }" -expected_redirect="https://caiwu.jiansuntuzhi.cn:8442/auth/login" +expected_redirect="https://caiwu.jiansuntuzhi.cn:8442/auth/dingtalk/callback?state=configuration-probe" if [ "$status" != "302" ] || [ "$redirect_url" != "$expected_redirect" ]; then install -m 644 "$BACKUP_DIR/caiwuxitong-backend-8442.conf" "$TARGET_CONFIG" diff --git a/deploy/host-nginx-8442-dingtalk.conf b/deploy/host-nginx-8442-dingtalk.conf index 436f7440..43b01e78 100644 --- a/deploy/host-nginx-8442-dingtalk.conf +++ b/deploy/host-nginx-8442-dingtalk.conf @@ -9,26 +9,15 @@ server { ssl_protocols TLSv1.2 TLSv1.3; client_max_body_size 12m; - # Plain HTTP is accepted only for the DingTalk OAuth callback registered - # by the enterprise administrator. Laravel must still generate HTTPS - # destinations after consuming the callback because every following page - # on port 8442 is HTTPS-only. + # The legacy registered HTTP callback must reach the browser's canonical + # HTTPS origin before Laravel reads its session. Proxying it with a forged + # Host loses the domain cookie and defeats browser-bound OAuth state. error_page 497 = @plain_http_request; location @plain_http_request { - if ($request_uri !~ "^/auth/dingtalk/callback(?:[?]|$)") { - return 308 https://caiwu.jiansuntuzhi.cn:8442$request_uri; - } - - proxy_pass http://127.0.0.1:18088; - proxy_http_version 1.1; - proxy_set_header Host caiwu.jiansuntuzhi.cn; - proxy_set_header X-Forwarded-Host caiwu.jiansuntuzhi.cn; - proxy_set_header X-Forwarded-Port 8442; - 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 https; - proxy_read_timeout 30s; + add_header Cache-Control "no-store" always; + add_header Referrer-Policy "no-referrer" always; + return 302 https://caiwu.jiansuntuzhi.cn:8442$request_uri; } location / { diff --git a/docs/钉钉花名册登录.md b/docs/钉钉花名册登录.md index 837704d5..18b201c3 100644 --- a/docs/钉钉花名册登录.md +++ b/docs/钉钉花名册登录.md @@ -60,4 +60,6 @@ - 开发节点通过实际只读入口抽查 3 名有有效钉钉唯一值的财务花名册人员,3 名均被新准入查询识别;不输出人员身份、手机号或原始 HR 行。 - 生产节点使用现有只读查询入口读到两个确认的财务根部门,说明该节点可独立访问 HR 来源。本次没有在生产执行开户或写入。 - 浏览器检查实际 Blade 渲染的虚构账号页面:桌面和 390px 窄屏首次登录提示、管理员绑定页新说明和开户审计。权限保存与后续工作台跳转通过 HTTP 功能测试验证。 -- 尚未发布,尚未进行真实钉钉扫码开户验收。 +- `0743f29` 已发布;生产 3 名花名册有效样本判断通过,原 11 个账号保留,10 个旧 remember 令牌已失效,使用新 session cookie 名隔离旧会话(旧会话文件保留于备份)。 +- 真实扫码发现钉钉开放平台尚未登记域名回调,且当前平台登录账号没有开发者权限。已恢复旧 IP 回调配置;Nginx 必须先通过 302 跳回 HTTPS 域名,不能将旧 IP 请求伪装成域名请求交给 Laravel,否则无法取得浏览器会话。域名登记后再切换生产回调变量。 +- 自动开户的真实扫码验收仍在进行,不能用只读样本或登录页跳转代替最终开户证据。 diff --git a/tests/Unit/Deployment/DingTalkCallbackNginxTest.php b/tests/Unit/Deployment/DingTalkCallbackNginxTest.php index bce4a34d..94ff7d04 100644 --- a/tests/Unit/Deployment/DingTalkCallbackNginxTest.php +++ b/tests/Unit/Deployment/DingTalkCallbackNginxTest.php @@ -6,14 +6,16 @@ use Tests\TestCase; class DingTalkCallbackNginxTest extends TestCase { - public function test_plain_http_is_limited_to_the_registered_dingtalk_callback(): void + public function test_plain_http_redirects_to_domain_before_laravel_reads_session(): void { $config = file_get_contents(base_path('deploy/host-nginx-8442-dingtalk.conf')); $this->assertStringContainsString('error_page 497 = @plain_http_request;', $config); - $this->assertStringContainsString('if ($request_uri !~ "^/auth/dingtalk/callback(?:[?]|$)")', $config); + $this->assertStringContainsString('return 302 https://caiwu.jiansuntuzhi.cn:8442$request_uri;', $config); + $this->assertStringContainsString('add_header Cache-Control "no-store" always;', $config); + $this->assertStringNotContainsString('proxy_set_header Host caiwu.jiansuntuzhi.cn;', $config); $this->assertStringNotContainsString('proxy_set_header X-Forwarded-Proto http;', $config); - $this->assertSame(2, substr_count($config, 'proxy_set_header X-Forwarded-Proto https;')); + $this->assertSame(1, substr_count($config, 'proxy_set_header X-Forwarded-Proto https;')); $this->assertStringContainsString('return 308 https://caiwu.jiansuntuzhi.cn:8442$request_uri;', $config); $this->assertStringContainsString('ssl_certificate /etc/nginx/caiwu-jiansuntuzhi-fullchain.pem;', $config); $this->assertStringNotContainsString('caiwuxitong-backend.crt', $config); @@ -25,7 +27,7 @@ class DingTalkCallbackNginxTest extends TestCase $this->assertStringContainsString('nginx -t', $script); $this->assertStringContainsString('the previous configuration was restored', $script); - $this->assertStringContainsString("expected_redirect=\"https://caiwu.jiansuntuzhi.cn:8442/auth/login\"", $script); + $this->assertStringContainsString("expected_redirect=\"https://caiwu.jiansuntuzhi.cn:8442/auth/dingtalk/callback?state=configuration-probe\"", $script); $this->assertStringContainsString("[ \"\$status\" != \"302\" ] || [ \"\$redirect_url\" != \"\$expected_redirect\" ]", $script); $this->assertStringContainsString('DINGTALK_HTTP_CALLBACK_READY=', $script); }