feat(安全): 强化安全策略与配置校验

- 增加 CORS/CSP/安全响应头与代理信任配置

- 引入 URL 白名单与私网开关,校验上游与价格源

- 改善 API Key 处理与网关错误返回

- 管理端设置隐藏敏感字段并优化前端提示

- 增加计费熔断与相关配置示例

测试: go test ./...
This commit is contained in:
yangjianbo
2026-01-02 17:40:57 +08:00
parent 3fd9bd4a80
commit bd4bf00856
46 changed files with 1572 additions and 220 deletions

View File

@@ -136,16 +136,16 @@
<ol
class="list-inside list-decimal space-y-1 text-xs text-amber-700 dark:text-amber-300"
>
<li v-html="t('admin.accounts.oauth.step1')"></li>
<li v-html="t('admin.accounts.oauth.step2')"></li>
<li v-html="t('admin.accounts.oauth.step3')"></li>
<li v-html="t('admin.accounts.oauth.step4')"></li>
<li v-html="t('admin.accounts.oauth.step5')"></li>
<li v-html="t('admin.accounts.oauth.step6')"></li>
<li>{{ t('admin.accounts.oauth.step1') }}</li>
<li>{{ t('admin.accounts.oauth.step2') }}</li>
<li>{{ t('admin.accounts.oauth.step3') }}</li>
<li>{{ t('admin.accounts.oauth.step4') }}</li>
<li>{{ t('admin.accounts.oauth.step5') }}</li>
<li>{{ t('admin.accounts.oauth.step6') }}</li>
</ol>
<p
class="mt-2 text-xs text-amber-600 dark:text-amber-400"
v-html="t('admin.accounts.oauth.sessionKeyFormat')"
v-text="t('admin.accounts.oauth.sessionKeyFormat')"
></p>
</div>
@@ -390,7 +390,7 @@
>
<p
class="text-xs text-amber-800 dark:text-amber-300"
v-html="oauthImportantNotice"
v-text="oauthImportantNotice"
></p>
</div>
<!-- Proxy Warning (for non-OpenAI) -->
@@ -400,7 +400,7 @@
>
<p
class="text-xs text-yellow-800 dark:text-yellow-300"
v-html="t('admin.accounts.oauth.proxyWarning')"
v-text="t('admin.accounts.oauth.proxyWarning')"
></p>
</div>
</div>
@@ -423,7 +423,7 @@
</p>
<p
class="mb-3 text-sm text-blue-700 dark:text-blue-300"
v-html="oauthAuthCodeDesc"
v-text="oauthAuthCodeDesc"
></p>
<div>
<label class="input-label">

View File

@@ -85,7 +85,7 @@
</button>
</div>
<!-- Code Content -->
<pre class="p-4 text-sm font-mono text-gray-100 overflow-x-auto"><code v-html="file.highlighted"></code></pre>
<pre class="p-4 text-sm font-mono text-gray-100 overflow-x-auto"><code v-text="file.content"></code></pre>
</div>
</div>
</div>
@@ -142,7 +142,6 @@ interface TabConfig {
interface FileConfig {
path: string
content: string
highlighted: string
hint?: string // Optional hint message for this file
}
@@ -227,13 +226,6 @@ const platformNote = computed(() => {
})
// Syntax highlighting helpers
const keyword = (text: string) => `<span class="text-purple-400">${text}</span>`
const variable = (text: string) => `<span class="text-cyan-400">${text}</span>`
const string = (text: string) => `<span class="text-green-400">${text}</span>`
const operator = (text: string) => `<span class="text-yellow-400">${text}</span>`
const comment = (text: string) => `<span class="text-gray-500">${text}</span>`
const key = (text: string) => `<span class="text-blue-400">${text}</span>`
// Generate file configs based on platform and active tab
const currentFiles = computed((): FileConfig[] => {
const baseUrl = props.baseUrl || window.location.origin
@@ -249,37 +241,29 @@ const currentFiles = computed((): FileConfig[] => {
function generateAnthropicFiles(baseUrl: string, apiKey: string): FileConfig[] {
let path: string
let content: string
let highlighted: string
switch (activeTab.value) {
case 'unix':
path = 'Terminal'
content = `export ANTHROPIC_BASE_URL="${baseUrl}"
export ANTHROPIC_AUTH_TOKEN="${apiKey}"`
highlighted = `${keyword('export')} ${variable('ANTHROPIC_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
${keyword('export')} ${variable('ANTHROPIC_AUTH_TOKEN')}${operator('=')}${string(`"${apiKey}"`)}`
break
case 'cmd':
path = 'Command Prompt'
content = `set ANTHROPIC_BASE_URL=${baseUrl}
set ANTHROPIC_AUTH_TOKEN=${apiKey}`
highlighted = `${keyword('set')} ${variable('ANTHROPIC_BASE_URL')}${operator('=')}${baseUrl}
${keyword('set')} ${variable('ANTHROPIC_AUTH_TOKEN')}${operator('=')}${apiKey}`
break
case 'powershell':
path = 'PowerShell'
content = `$env:ANTHROPIC_BASE_URL="${baseUrl}"
$env:ANTHROPIC_AUTH_TOKEN="${apiKey}"`
highlighted = `${keyword('$env:')}${variable('ANTHROPIC_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
${keyword('$env:')}${variable('ANTHROPIC_AUTH_TOKEN')}${operator('=')}${string(`"${apiKey}"`)}`
break
default:
path = 'Terminal'
content = ''
highlighted = ''
}
return [{ path, content, highlighted }]
return [{ path, content }]
}
function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
@@ -301,40 +285,20 @@ base_url = "${baseUrl}"
wire_api = "responses"
requires_openai_auth = true`
const configHighlighted = `${key('model_provider')} ${operator('=')} ${string('"sub2api"')}
${key('model')} ${operator('=')} ${string('"gpt-5.2-codex"')}
${key('model_reasoning_effort')} ${operator('=')} ${string('"high"')}
${key('network_access')} ${operator('=')} ${string('"enabled"')}
${key('disable_response_storage')} ${operator('=')} ${keyword('true')}
${key('windows_wsl_setup_acknowledged')} ${operator('=')} ${keyword('true')}
${key('model_verbosity')} ${operator('=')} ${string('"high"')}
${comment('[model_providers.sub2api]')}
${key('name')} ${operator('=')} ${string('"sub2api"')}
${key('base_url')} ${operator('=')} ${string(`"${baseUrl}"`)}
${key('wire_api')} ${operator('=')} ${string('"responses"')}
${key('requires_openai_auth')} ${operator('=')} ${keyword('true')}`
// auth.json content
const authContent = `{
"OPENAI_API_KEY": "${apiKey}"
}`
const authHighlighted = `{
${key('"OPENAI_API_KEY"')}: ${string(`"${apiKey}"`)}
}`
return [
{
path: `${configDir}/config.toml`,
content: configContent,
highlighted: configHighlighted,
hint: t('keys.useKeyModal.openai.configTomlHint')
},
{
path: `${configDir}/auth.json`,
content: authContent,
highlighted: authHighlighted
content: authContent
}
]
}

View File

@@ -63,6 +63,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getPublicSettings } from '@/api/auth'
import { sanitizeUrl } from '@/utils/url'
const siteName = ref('Sub2API')
const siteLogo = ref('')
@@ -74,7 +75,7 @@ onMounted(async () => {
try {
const settings = await getPublicSettings()
siteName.value = settings.site_name || 'Sub2API'
siteLogo.value = settings.site_logo || ''
siteLogo.value = sanitizeUrl(settings.site_logo || '', { allowRelative: true })
siteSubtitle.value = settings.site_subtitle || 'Subscription to API Conversion Platform'
} catch (error) {
console.error('Failed to load public settings:', error)