fix: 保持七牛静态资源桶私有
This commit is contained in:
parent
222eca0e2d
commit
8234ed3510
@ -15,20 +15,18 @@ class EnforceLocalOnly
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$staticOrigin = rtrim((string) config('app.asset_url'), '/');
|
|
||||||
|
|
||||||
$response->headers->set('Content-Security-Policy', implode('; ', [
|
$response->headers->set('Content-Security-Policy', implode('; ', [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
"font-src 'self' {$staticOrigin} data:",
|
"font-src 'self' data:",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
"frame-src 'self'",
|
"frame-src 'self'",
|
||||||
"img-src 'self' {$staticOrigin} data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
"media-src 'self' {$staticOrigin}",
|
"media-src 'self'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"script-src 'self' {$staticOrigin} 'unsafe-inline' 'unsafe-eval'",
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||||
"style-src 'self' {$staticOrigin} 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"navigate-to 'self'",
|
"navigate-to 'self'",
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
@ -77,11 +77,7 @@ return [
|
|||||||
|
|
||||||
'url' => env('APP_URL', ''),
|
'url' => env('APP_URL', ''),
|
||||||
|
|
||||||
/*
|
'asset_url' => env('ASSET_URL'),
|
||||||
* 项目静态资源统一由唯一的七牛云空间 caiwu-jiansun 提供。
|
|
||||||
* 不允许通过环境变量切换到其他存储桶,避免不同环境引用不同资源。
|
|
||||||
*/
|
|
||||||
'asset_url' => 'https://caiwu.jiansun.vip',
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@ -25,48 +25,48 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"splash": {
|
"splash": {
|
||||||
"640x1136": "https://caiwu.jiansun.vip/public/img/pwa/splash-640x1136.png",
|
"640x1136": "public/img/pwa/splash-640x1136.png",
|
||||||
"750x1334": "https://caiwu.jiansun.vip/public/img/pwa/splash-750x1334.png",
|
"750x1334": "public/img/pwa/splash-750x1334.png",
|
||||||
"828x1792": "https://caiwu.jiansun.vip/public/img/pwa/splash-828x1792.png",
|
"828x1792": "public/img/pwa/splash-828x1792.png",
|
||||||
"1242x2208": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2208.png",
|
"1242x2208": "public/img/pwa/splash-1242x2208.png",
|
||||||
"1242x2688": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2688.png",
|
"1242x2688": "public/img/pwa/splash-1242x2688.png",
|
||||||
"1536x2048": "https://caiwu.jiansun.vip/public/img/pwa/splash-1536x2048.png",
|
"1536x2048": "public/img/pwa/splash-1536x2048.png",
|
||||||
"1668x2224": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2224.png",
|
"1668x2224": "public/img/pwa/splash-1668x2224.png",
|
||||||
"1668x2388": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2388.png",
|
"1668x2388": "public/img/pwa/splash-1668x2388.png",
|
||||||
"2048x2732": "https://caiwu.jiansun.vip/public/img/pwa/splash-2048x2732.png"
|
"2048x2732": "public/img/pwa/splash-2048x2732.png"
|
||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
"src": "public/img/pwa/icon-192x192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"purpose": "maskable",
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png",
|
"src": "public/img/pwa/icon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"purpose": "maskable",
|
"purpose": "maskable",
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512-maskable.png",
|
"src": "public/img/pwa/icon-512x512-maskable.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-dashboard.png",
|
"src": "public/img/pwa/screenshot-dashboard.png",
|
||||||
"sizes": "1932x1394",
|
"sizes": "1932x1394",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-invoice.png",
|
"src": "public/img/pwa/screenshot-invoice.png",
|
||||||
"sizes": "2748x1986",
|
"sizes": "2748x1986",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"url": "sales/invoices/create?utm_source=pwa",
|
"url": "sales/invoices/create?utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
"src": "public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"url": "banking/transactions/create?type=income&utm_source=pwa",
|
"url": "banking/transactions/create?type=income&utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
"src": "public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -103,7 +103,7 @@
|
|||||||
"url": "purchases/bills/create?utm_source=pwa",
|
"url": "purchases/bills/create?utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
"src": "public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"url": "banking/transactions/create?type=expense&utm_source=pwa",
|
"url": "banking/transactions/create?type=expense&utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
"src": "public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -107,7 +107,7 @@ function Get-QiniuBucketNames {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Set-QiniuBucketPublic {
|
function Set-QiniuBucketPrivate {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$AccessKey,
|
[string]$AccessKey,
|
||||||
@ -115,7 +115,7 @@ function Set-QiniuBucketPublic {
|
|||||||
[string]$SecretKey
|
[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 `
|
$headers = Get-QiniuManagementHeaders `
|
||||||
-Method 'POST' `
|
-Method 'POST' `
|
||||||
-Uri $uri `
|
-Uri $uri `
|
||||||
@ -129,10 +129,31 @@ function Set-QiniuBucketPublic {
|
|||||||
-ContentType 'application/x-www-form-urlencoded' `
|
-ContentType 'application/x-www-form-urlencoded' `
|
||||||
-SkipHttpErrorCheck
|
-SkipHttpErrorCheck
|
||||||
if ($response.StatusCode -notin @(200, 204)) {
|
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 {
|
function Get-QiniuRemoteFiles {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -378,7 +399,7 @@ if ($bucketNames -notcontains $bucket) {
|
|||||||
throw "当前七牛云账号中不存在项目唯一存储桶 $bucket。"
|
throw "当前七牛云账号中不存在项目唯一存储桶 $bucket。"
|
||||||
}
|
}
|
||||||
|
|
||||||
Set-QiniuBucketPublic -AccessKey $accessKey -SecretKey $secretKey
|
Set-QiniuBucketPrivate -AccessKey $accessKey -SecretKey $secretKey
|
||||||
|
|
||||||
$sources = Get-StaticAssetSources
|
$sources = Get-StaticAssetSources
|
||||||
$remoteFiles = Get-QiniuRemoteFiles -AccessKey $accessKey -SecretKey $secretKey
|
$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()
|
$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
|
$probeStatus = 0
|
||||||
foreach ($attempt in 1..10) {
|
foreach ($attempt in 1..10) {
|
||||||
$probeResponse = Invoke-WebRequest `
|
$probeResponse = Invoke-WebRequest `
|
||||||
@ -435,7 +469,7 @@ foreach ($attempt in 1..10) {
|
|||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
}
|
}
|
||||||
if ($probeStatus -ne 200) {
|
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 staticCacheName = "pwa-v2";
|
||||||
var filesToCache = [
|
var filesToCache = [
|
||||||
'https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png',
|
'/public/img/pwa/icon-192x192.png',
|
||||||
'https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png'
|
'/public/img/pwa/icon-512x512.png'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Cache on install
|
// 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