fix: 保持七牛静态资源桶私有
This commit is contained in:
parent
222eca0e2d
commit
8234ed3510
@ -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'",
|
||||
]));
|
||||
|
||||
|
||||
@ -77,11 +77,7 @@ return [
|
||||
|
||||
'url' => env('APP_URL', ''),
|
||||
|
||||
/*
|
||||
* 项目静态资源统一由唯一的七牛云空间 caiwu-jiansun 提供。
|
||||
* 不允许通过环境变量切换到其他存储桶,避免不同环境引用不同资源。
|
||||
*/
|
||||
'asset_url' => 'https://caiwu.jiansun.vip',
|
||||
'asset_url' => env('ASSET_URL'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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
4
serviceworker.js
vendored
@ -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
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user