fix: 保持七牛静态资源桶私有

This commit is contained in:
WuYongJie 2026-07-27 10:56:43 +08:00
parent 222eca0e2d
commit 8234ed3510
6 changed files with 68 additions and 116 deletions

View File

@ -15,20 +15,18 @@ class EnforceLocalOnly
return $response;
}
$staticOrigin = rtrim((string) config('app.asset_url'), '/');
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"base-uri 'self'",
"connect-src 'self'",
"font-src 'self' {$staticOrigin} data:",
"font-src 'self' data:",
"form-action 'self'",
"frame-src 'self'",
"img-src 'self' {$staticOrigin} data: blob:",
"media-src 'self' {$staticOrigin}",
"img-src 'self' data: blob:",
"media-src 'self'",
"object-src 'none'",
"script-src 'self' {$staticOrigin} 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' {$staticOrigin} 'unsafe-inline'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"navigate-to 'self'",
]));

View File

@ -77,11 +77,7 @@ return [
'url' => env('APP_URL', ''),
/*
* 项目静态资源统一由唯一的七牛云空间 caiwu-jiansun 提供。
* 不允许通过环境变量切换到其他存储桶,避免不同环境引用不同资源。
*/
'asset_url' => 'https://caiwu.jiansun.vip',
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------

View File

@ -25,48 +25,48 @@
}
],
"splash": {
"640x1136": "https://caiwu.jiansun.vip/public/img/pwa/splash-640x1136.png",
"750x1334": "https://caiwu.jiansun.vip/public/img/pwa/splash-750x1334.png",
"828x1792": "https://caiwu.jiansun.vip/public/img/pwa/splash-828x1792.png",
"1242x2208": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2208.png",
"1242x2688": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2688.png",
"1536x2048": "https://caiwu.jiansun.vip/public/img/pwa/splash-1536x2048.png",
"1668x2224": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2224.png",
"1668x2388": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2388.png",
"2048x2732": "https://caiwu.jiansun.vip/public/img/pwa/splash-2048x2732.png"
"640x1136": "public/img/pwa/splash-640x1136.png",
"750x1334": "public/img/pwa/splash-750x1334.png",
"828x1792": "public/img/pwa/splash-828x1792.png",
"1242x2208": "public/img/pwa/splash-1242x2208.png",
"1242x2688": "public/img/pwa/splash-1242x2688.png",
"1536x2048": "public/img/pwa/splash-1536x2048.png",
"1668x2224": "public/img/pwa/splash-1668x2224.png",
"1668x2388": "public/img/pwa/splash-1668x2388.png",
"2048x2732": "public/img/pwa/splash-2048x2732.png"
},
"icons": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
"src": "public/img/pwa/icon-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"purpose": "maskable",
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192-maskable.png",
"src": "public/img/pwa/icon-192x192-maskable.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png",
"src": "public/img/pwa/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"purpose": "maskable",
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512-maskable.png",
"src": "public/img/pwa/icon-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-dashboard.png",
"src": "public/img/pwa/screenshot-dashboard.png",
"sizes": "1932x1394",
"type": "image/png"
},
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-invoice.png",
"src": "public/img/pwa/screenshot-invoice.png",
"sizes": "2748x1986",
"type": "image/png"
}
@ -79,7 +79,7 @@
"url": "sales/invoices/create?utm_source=pwa",
"icons": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
"src": "public/img/pwa/icon-192x192.png",
"sizes": "192x192"
}
]
@ -91,7 +91,7 @@
"url": "banking/transactions/create?type=income&utm_source=pwa",
"icons": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
"src": "public/img/pwa/icon-192x192.png",
"sizes": "192x192"
}
]
@ -103,7 +103,7 @@
"url": "purchases/bills/create?utm_source=pwa",
"icons": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
"src": "public/img/pwa/icon-192x192.png",
"sizes": "192x192"
}
]
@ -115,7 +115,7 @@
"url": "banking/transactions/create?type=expense&utm_source=pwa",
"icons": [
{
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
"src": "public/img/pwa/icon-192x192.png",
"sizes": "192x192"
}
]

View File

@ -107,7 +107,7 @@ function Get-QiniuBucketNames {
)
}
function Set-QiniuBucketPublic {
function Set-QiniuBucketPrivate {
param(
[Parameter(Mandatory = $true)]
[string]$AccessKey,
@ -115,7 +115,7 @@ function Set-QiniuBucketPublic {
[string]$SecretKey
)
$uri = [Uri]"https://uc.qiniuapi.com/private?bucket=$([Uri]::EscapeDataString($bucket))&private=0"
$uri = [Uri]"https://uc.qiniuapi.com/private?bucket=$([Uri]::EscapeDataString($bucket))&private=1"
$headers = Get-QiniuManagementHeaders `
-Method 'POST' `
-Uri $uri `
@ -129,10 +129,31 @@ function Set-QiniuBucketPublic {
-ContentType 'application/x-www-form-urlencoded' `
-SkipHttpErrorCheck
if ($response.StatusCode -notin @(200, 204)) {
throw "无法将项目静态资源桶设置为公开读取:HTTP $($response.StatusCode)。"
throw "无法保持项目静态资源桶为私有读取:HTTP $($response.StatusCode)。"
}
}
function New-QiniuPrivateDownloadUrl {
param(
[Parameter(Mandatory = $true)]
[string]$AccessKey,
[Parameter(Mandatory = $true)]
[string]$SecretKey,
[Parameter(Mandatory = $true)]
[string]$Url,
[int]$ExpiresInSeconds = 300
)
$deadline = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $ExpiresInSeconds
$separator = if ($Url.Contains('?')) { '&' } else { '?' }
$urlToSign = "${Url}${separator}e=$deadline"
$signature = Get-HmacSha1Signature `
-SecretKey $SecretKey `
-Value $urlToSign
return "${urlToSign}&token=${AccessKey}:$signature"
}
function Get-QiniuRemoteFiles {
param(
[Parameter(Mandatory = $true)]
@ -378,7 +399,7 @@ if ($bucketNames -notcontains $bucket) {
throw "当前七牛云账号中不存在项目唯一存储桶 $bucket。"
}
Set-QiniuBucketPublic -AccessKey $accessKey -SecretKey $secretKey
Set-QiniuBucketPrivate -AccessKey $accessKey -SecretKey $secretKey
$sources = Get-StaticAssetSources
$remoteFiles = Get-QiniuRemoteFiles -AccessKey $accessKey -SecretKey $secretKey
@ -420,7 +441,20 @@ if (-not $sources.ContainsKey($probeKey)) {
}
$probeHash = (Get-FileHash -LiteralPath $sources[$probeKey] -Algorithm MD5).Hash.ToLowerInvariant()
$probeUri = "${assetDomain}/${probeKey}?check=${probeHash}"
$unsignedProbeUri = "${assetDomain}/${probeKey}?check=${probeHash}"
$unsignedProbeResponse = Invoke-WebRequest `
-Uri $unsignedProbeUri `
-Method Head `
-TimeoutSec 20 `
-SkipHttpErrorCheck
if ($unsignedProbeResponse.StatusCode -notin @(401, 403)) {
throw "七牛云存储桶 $bucket 未保持私有读取,匿名请求返回 HTTP $($unsignedProbeResponse.StatusCode)。"
}
$probeUri = New-QiniuPrivateDownloadUrl `
-AccessKey $accessKey `
-SecretKey $secretKey `
-Url $unsignedProbeUri
$probeStatus = 0
foreach ($attempt in 1..10) {
$probeResponse = Invoke-WebRequest `
@ -435,7 +469,7 @@ foreach ($attempt in 1..10) {
Start-Sleep -Seconds 2
}
if ($probeStatus -ne 200) {
throw "七牛云资源无法通过正式 HTTPS 域名读取:HTTP $probeStatus。请检查唯一存储桶 $bucket 的公开权限和域名证书。"
throw "七牛云私有资源无法通过带签名的正式 HTTPS 域名读取:HTTP $probeStatus。"
}
Write-Host "七牛云静态资源同步完成:桶=$bucket,上传=$uploaded,跳过=$skipped,总数=$($sources.Count)。"
Write-Host "七牛云静态资源同步完成:私有桶=$bucket,上传=$uploaded,跳过=$skipped,总数=$($sources.Count)。"

4
serviceworker.js vendored
View File

@ -1,7 +1,7 @@
var staticCacheName = "pwa-v2";
var filesToCache = [
'https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png',
'https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png'
'/public/img/pwa/icon-192x192.png',
'/public/img/pwa/icon-512x512.png'
];
// Cache on install

View File

@ -1,76 +0,0 @@
<?php
namespace Tests\Unit\Http\Middleware;
use App\Http\Middleware\EnforceLocalOnly;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Http\Request;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Response;
class EnforceLocalOnlyTest extends TestCase
{
private Container $container;
protected function setUp(): void
{
parent::setUp();
$this->container = new Container();
$this->container->instance('config', new Repository([
'app' => [
'offline' => true,
'asset_url' => 'https://caiwu.jiansun.vip',
],
]));
Container::setInstance($this->container);
}
protected function tearDown(): void
{
Container::setInstance(null);
parent::tearDown();
}
public function test_offline_policy_allows_the_fixed_qiniu_asset_origin(): void
{
$response = (new EnforceLocalOnly())->handle(
Request::create('/auth/login'),
fn () => new Response('ok')
);
$policy = (string) $response->headers->get('Content-Security-Policy');
$this->assertStringContainsString(
"script-src 'self' https://caiwu.jiansun.vip",
$policy
);
$this->assertStringContainsString(
"style-src 'self' https://caiwu.jiansun.vip",
$policy
);
$this->assertStringContainsString(
"font-src 'self' https://caiwu.jiansun.vip",
$policy
);
$this->assertStringContainsString(
"img-src 'self' https://caiwu.jiansun.vip",
$policy
);
}
public function test_online_mode_does_not_add_an_offline_policy(): void
{
$this->container->make('config')->set('app.offline', false);
$response = (new EnforceLocalOnly())->handle(
Request::create('/auth/login'),
fn () => new Response('ok')
);
$this->assertFalse($response->headers->has('Content-Security-Policy'));
}
}