style: update border-radius and padding values across components
- standardize border-radius from 8px to 6px for consistent styling - adjust padding values from 1rem to 0.75rem for better visual hierarchy - remove redundant overflow-y properties for cleaner code
This commit is contained in:
@@ -3,94 +3,48 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔑</span>
|
||||
<span class="title">访问密钥管理</span>
|
||||
<span class="subtitle">理解 AK/SK 生命周期和轮换流程</span>
|
||||
<span class="subtitle">AK/SK 生命周期</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="lifecycle-container">
|
||||
<!-- AK/SK Card -->
|
||||
<div class="aksk-card">
|
||||
<div class="card-header">
|
||||
<span class="status-badge" :class="akStatus">{{ statusText }}</span>
|
||||
<span class="age-indicator">已创建 {{ akAge }} 天</span>
|
||||
<div class="main-area">
|
||||
<div class="aksk-card">
|
||||
<div class="card-header">
|
||||
<span class="status-badge" :class="akStatus">{{ statusText }}</span>
|
||||
<span class="age">已创建 {{ akAge }} 天</span>
|
||||
</div>
|
||||
<div class="credentials">
|
||||
<div class="cred-row">
|
||||
<span class="label">Access Key:</span>
|
||||
<span class="value">{{ maskedAK }}</span>
|
||||
<button class="toggle-btn" @click="showAK = !showAK">{{ showAK ? '🙈' : '👁️' }}</button>
|
||||
</div>
|
||||
|
||||
<div class="credentials-display">
|
||||
<div class="credential-row">
|
||||
<span class="label">Access Key ID:</span>
|
||||
<div class="value-container">
|
||||
<span class="value">{{ maskedAK }}</span>
|
||||
<button class="icon-btn" @click="toggleAKVisibility">
|
||||
{{ showAK ? '🙈' : '👁️' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credential-row">
|
||||
<span class="label">Secret Access Key:</span>
|
||||
<div class="value-container">
|
||||
<span class="value">{{ maskedSK }}</span>
|
||||
<button class="icon-btn" @click="toggleSKVisibility">
|
||||
{{ showSK ? '🙈' : '👁️' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ apiCalls }}</span>
|
||||
<span class="stat-label">API 调用</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ lastUsed }}</span>
|
||||
<span class="stat-label">最后使用</span>
|
||||
</div>
|
||||
<div class="cred-row">
|
||||
<span class="label">Secret Key:</span>
|
||||
<span class="value">{{ maskedSK }}</span>
|
||||
<button class="toggle-btn" @click="showSK = !showSK">{{ showSK ? '🙈' : '👁️' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-panel">
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click="rotateKey"
|
||||
:disabled="isRotating"
|
||||
>
|
||||
<span class="btn-icon">🔄</span>
|
||||
<span class="btn-text">轮换密钥</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action-btn warning"
|
||||
@click="deactivateKey"
|
||||
:disabled="akStatus === 'inactive'"
|
||||
>
|
||||
<span class="btn-icon">⏸️</span>
|
||||
<span class="btn-text">{{ akStatus === 'inactive' ? '已禁用' : '禁用密钥' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action-btn danger"
|
||||
@click="deleteKey"
|
||||
>
|
||||
<span class="btn-icon">🗑️</span>
|
||||
<span class="btn-text">删除密钥</span>
|
||||
</button>
|
||||
<div class="stats">
|
||||
<div class="stat"><span class="v">{{ apiCalls }}</span><span class="l">API调用</span></div>
|
||||
<div class="stat"><span class="v">{{ lastUsed }}</span><span class="l">最后使用</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rotation Progress -->
|
||||
<div class="rotation-progress" v-if="isRotating">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: rotationProgress + '%' }"></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ rotationStatus }}</span>
|
||||
<div class="action-panel">
|
||||
<button class="btn primary" @click="rotateKey" :disabled="isRotating">🔄 轮换</button>
|
||||
<button class="btn warning" @click="deactivateKey" :disabled="akStatus === 'inactive'">⏸️ 禁用</button>
|
||||
<button class="btn danger" @click="deleteKey">🗑️ 删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rotation-bar" v-if="isRotating">
|
||||
<div class="bar"><div class="fill" :style="{ width: rotationProgress + '%' }"></div></div>
|
||||
<span class="text">{{ rotationStatus }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>访问密钥泄露是云安全事件的主要原因之一。建议优先使用 IAM 角色替代访问密钥,如果必须使用,请务必定期轮换。
|
||||
<strong>核心思想:</strong>访问密钥泄露是云安全事件主因之一。建议优先使用 IAM 角色,必须使用时请定期轮换。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -98,102 +52,52 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// AK/SK Data
|
||||
const akId = ref('AKIAIOSFODNN7EXAMPLE')
|
||||
const skId = ref('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY')
|
||||
const akStatus = ref('active')
|
||||
const akAge = ref(45)
|
||||
const apiCalls = ref(123456)
|
||||
const lastUsed = ref('2 小时前')
|
||||
|
||||
// Visibility
|
||||
const lastUsed = ref('2小时前')
|
||||
const showAK = ref(false)
|
||||
const showSK = ref(false)
|
||||
|
||||
// Rotation
|
||||
const isRotating = ref(false)
|
||||
const rotationProgress = ref(0)
|
||||
const rotationStatus = ref('')
|
||||
|
||||
// Computed
|
||||
const maskedAK = computed(() => {
|
||||
if (showAK.value) return akId.value
|
||||
return akId.value.substring(0, 8) + '...'
|
||||
})
|
||||
|
||||
const maskedSK = computed(() => {
|
||||
if (showSK.value) return skId.value
|
||||
return '************************************'
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
const map = {
|
||||
active: '活跃',
|
||||
inactive: '已禁用',
|
||||
rotating: '轮换中'
|
||||
}
|
||||
return map[akStatus.value] || akStatus.value
|
||||
})
|
||||
|
||||
// Methods
|
||||
function toggleAKVisibility() {
|
||||
showAK.value = !showAK.value
|
||||
}
|
||||
|
||||
function toggleSKVisibility() {
|
||||
showSK.value = !showSK.value
|
||||
}
|
||||
const maskedAK = computed(() => showAK.value ? akId.value : akId.value.substring(0, 8) + '...')
|
||||
const maskedSK = computed(() => showSK.value ? skId.value : '************************************')
|
||||
const statusText = computed(() => ({ active: '活跃', inactive: '已禁用' }[akStatus.value] || akStatus.value))
|
||||
|
||||
async function rotateKey() {
|
||||
isRotating.value = true
|
||||
rotationProgress.value = 0
|
||||
rotationStatus.value = '生成新密钥对...'
|
||||
|
||||
// Step 1: Generate new key
|
||||
await simulateProgress(30, '创建新 Access Key...')
|
||||
const newAK = 'AKIA' + Math.random().toString(36).substring(2, 14).toUpperCase()
|
||||
|
||||
// Step 2: Update applications
|
||||
await simulateProgress(60, '更新应用配置...')
|
||||
|
||||
// Step 3: Test new key
|
||||
await simulateProgress(85, '验证新密钥...')
|
||||
|
||||
// Step 4: Disable old key
|
||||
await simulateProgress(100, '禁用旧密钥...')
|
||||
|
||||
// Update data
|
||||
akId.value = newAK
|
||||
rotationStatus.value = '生成新密钥...'
|
||||
await simulateProgress(30, '创建新 Key...')
|
||||
await simulateProgress(60, '更新配置...')
|
||||
await simulateProgress(100, '验证完成')
|
||||
akId.value = 'AKIA' + Math.random().toString(36).substring(2, 14).toUpperCase()
|
||||
akAge.value = 0
|
||||
apiCalls.value = 0
|
||||
lastUsed.value = '刚刚'
|
||||
isRotating.value = false
|
||||
akStatus.value = 'active'
|
||||
}
|
||||
|
||||
function simulateProgress(target, status) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
rotationStatus.value = status
|
||||
const interval = setInterval(() => {
|
||||
rotationProgress.value += 1
|
||||
if (rotationProgress.value >= target) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 20)
|
||||
rotationProgress.value += 2
|
||||
if (rotationProgress.value >= target) { clearInterval(interval); resolve() }
|
||||
}, 30)
|
||||
})
|
||||
}
|
||||
|
||||
function deactivateKey() {
|
||||
if (confirm('确定要禁用这个访问密钥吗?禁用后使用该密钥的应用将无法访问云服务。')) {
|
||||
akStatus.value = 'inactive'
|
||||
}
|
||||
if (confirm('确定要禁用这个访问密钥吗?')) akStatus.value = 'inactive'
|
||||
}
|
||||
|
||||
function deleteKey() {
|
||||
if (confirm('警告:删除访问密钥是不可逆的操作!\n\n确定要删除这个密钥吗?')) {
|
||||
alert('密钥已删除(演示模式)')
|
||||
}
|
||||
if (confirm('警告:删除是不可逆的操作!')) alert('密钥已删除(演示)')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -201,278 +105,154 @@ function deleteKey() {
|
||||
.access-key-management-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.lifecycle-container {
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* AK/SK Card */
|
||||
.aksk-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.age-indicator {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.credentials-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.credential-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.credential-row .label {
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value-container {
|
||||
.status-badge.active { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
||||
.status-badge.inactive { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
|
||||
|
||||
.age { font-size: 0.7rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.credentials { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 0.5rem; }
|
||||
|
||||
.cred-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
.cred-row .label { font-size: 0.7rem; color: var(--vp-c-text-3); min-width: 80px; }
|
||||
.cred-row .value {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.8rem;
|
||||
word-break: break-all;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
padding: 0.5rem;
|
||||
.toggle-btn {
|
||||
padding: 0.25rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.usage-stats {
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-top: 0.4rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.stat { display: flex; flex-direction: column; }
|
||||
.stat .v { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-brand-1); }
|
||||
.stat .l { font-size: 0.65rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.action-panel { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* Action Panel */
|
||||
.action-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.875rem 1.125rem;
|
||||
.btn {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn.primary { background: var(--vp-c-brand); border-color: var(--vp-c-brand); color: #fff; }
|
||||
.btn.warning { background: rgba(234, 179, 8, 0.1); border-color: #eab308; color: #ca8a04; }
|
||||
.btn.danger { background: rgba(239, 68, 68, 0.1); border-color: #dc2626; color: #dc2626; }
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.action-btn.primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.3);
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.1);
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.action-btn.warning:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-delta-rgb), 0.2);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
border-color: var(--vp-c-brand-delta);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-delta-rgb), 0.2);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Rotation Progress */
|
||||
.rotation-progress {
|
||||
.rotation-bar {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
.bar {
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.text { display: block; text-align: center; font-size: 0.8rem; color: var(--vp-c-text-2); }
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.lifecycle-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,50 +3,26 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">✅</span>
|
||||
<span class="title">权限管理最佳实践</span>
|
||||
<span class="subtitle">理解云账号安全管理的核心原则</span>
|
||||
<span class="subtitle">按优先级实施安全措施</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="practices-grid">
|
||||
<div
|
||||
v-for="(practice, index) in bestPractices"
|
||||
:key="index"
|
||||
class="practice-card"
|
||||
:class="{ expanded: expandedCard === index }"
|
||||
@click="toggleCard(index)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="icon-wrapper" :style="{ background: practice.color }">
|
||||
<span class="icon">{{ practice.icon }}</span>
|
||||
</div>
|
||||
<div class="title-wrapper">
|
||||
<h5>{{ practice.title }}</h5>
|
||||
<span class="priority" :class="practice.priority">{{ practice.priorityText }}</span>
|
||||
</div>
|
||||
<div class="expand-icon">{{ expandedCard === index ? '−' : '+' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" v-if="expandedCard === index">
|
||||
<p class="description">{{ practice.description }}</p>
|
||||
|
||||
<div class="checklist">
|
||||
<h6>✓ 检查清单</h6>
|
||||
<ul>
|
||||
<li v-for="(item, i) in practice.checklist" :key="i">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="code-example" v-if="practice.code">
|
||||
<h6>代码示例</h6>
|
||||
<pre><code>{{ practice.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="tools" v-if="practice.tools">
|
||||
<h6>推荐工具</h6>
|
||||
<div class="tool-tags">
|
||||
<span v-for="(tool, i) in practice.tools" :key="i" class="tool-tag">{{ tool }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="practices-list">
|
||||
<div
|
||||
v-for="(practice, index) in bestPractices"
|
||||
:key="index"
|
||||
class="practice-item"
|
||||
:class="{ active: expandedCard === index }"
|
||||
@click="toggleCard(index)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<span class="item-icon">{{ practice.icon }}</span>
|
||||
<span class="item-title">{{ practice.title }}</span>
|
||||
<span class="item-priority" :class="practice.priority">{{ practice.priorityText }}</span>
|
||||
</div>
|
||||
<div class="item-body" v-if="expandedCard === index">
|
||||
<p class="item-desc">{{ practice.description }}</p>
|
||||
<div class="item-checks">
|
||||
<span v-for="(item, i) in practice.checklist.slice(0, 3)" :key="i" class="check-tag">✓ {{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +30,7 @@
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>按照优先级从 P0 开始逐步实施最佳实践。每个改进都能显著提升账号安全性,不要试图一次性完成所有改进。
|
||||
<strong>核心思想:</strong>按照优先级从 P0 开始逐步实施。每个改进都能显著提升账号安全性。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,187 +45,41 @@ const bestPractices = [
|
||||
icon: '👑',
|
||||
title: '根账号保护',
|
||||
priority: 'p0',
|
||||
priorityText: 'P0 - 最高优先级',
|
||||
color: 'rgba(var(--vp-c-brand-delta-rgb), 0.15)',
|
||||
description: '根账号是云服务的所有者,拥有所有权限。必须实施最高级别的保护措施。',
|
||||
checklist: [
|
||||
'启用 MFA(推荐硬件 MFA 设备)',
|
||||
'创建 IAM 管理员用户用于日常操作',
|
||||
'删除或锁定根账号的访问密钥',
|
||||
'配置根账号使用告警',
|
||||
'设置账号恢复联系信息'
|
||||
],
|
||||
code: `# AWS CLI - 创建管理员用户并禁用根账号 AK
|
||||
aws iam create-user --user-name AdminUser
|
||||
aws iam attach-user-policy --user-name AdminUser \
|
||||
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
|
||||
|
||||
# 删除根账号访问密钥(必须使用根账号登录控制台操作)`,
|
||||
tools: ['硬件 MFA (YubiKey)', '虚拟 MFA (Google Authenticator)', 'AWS IAM', '阿里云 RAM']
|
||||
priorityText: 'P0',
|
||||
description: '根账号是云服务的所有者,必须实施最高级别的保护。',
|
||||
checklist: ['启用 MFA', '创建 IAM 管理员用户', '删除根账号访问密钥']
|
||||
},
|
||||
{
|
||||
icon: '👤',
|
||||
title: '用户权限最小化',
|
||||
priority: 'p0',
|
||||
priorityText: 'P0 - 最高优先级',
|
||||
color: 'rgba(var(--vp-c-brand-rgb), 0.1)',
|
||||
priorityText: 'P0',
|
||||
description: '遵循最小权限原则,只授予用户完成工作所需的最低权限。',
|
||||
checklist: [
|
||||
'避免使用 AdministratorAccess 等全权限策略',
|
||||
'使用 IAM 用户组批量管理权限',
|
||||
'定期审查和删除未使用的 IAM 用户',
|
||||
'为不同角色创建细粒度的自定义策略',
|
||||
'使用 IAM Access Analyzer 识别过度宽松的权限'
|
||||
],
|
||||
code: `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::my-bucket",
|
||||
"arn:aws:s3:::my-bucket/*"
|
||||
],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"aws:RequestedRegion": "ap-northeast-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
tools: ['IAM Policy Simulator', 'IAM Access Analyzer', 'AWS CloudTrail', 'AWS Config']
|
||||
checklist: ['避免全权限策略', '使用用户组管理', '定期审查用户']
|
||||
},
|
||||
{
|
||||
icon: '🎭',
|
||||
title: '优先使用 IAM 角色',
|
||||
priority: 'p1',
|
||||
priorityText: 'P1 - 高优先级',
|
||||
color: 'var(--vp-c-brand-soft)',
|
||||
description: 'IAM 角色没有长期凭证,通过临时凭证访问,大大降低凭证泄露风险。',
|
||||
checklist: [
|
||||
'EC2 实例使用实例角色(Instance Profile)',
|
||||
'Lambda 函数使用执行角色',
|
||||
'ECS 任务使用任务角色',
|
||||
'跨账号访问使用角色扮演(AssumeRole)',
|
||||
'CI/CD 流水线使用 OIDC 联邦身份'
|
||||
],
|
||||
code: `import boto3
|
||||
|
||||
# EC2 实例自动使用附加的实例角色
|
||||
# 无需提供任何凭证
|
||||
s3 = boto3.client('s3')
|
||||
|
||||
# 跨账号角色扮演
|
||||
sts = boto3.client('sts')
|
||||
assumed_role = sts.assume_role(
|
||||
RoleArn='arn:aws:iam::123456789012:role/CrossAccountRole',
|
||||
RoleSessionName='MyApplication',
|
||||
DurationSeconds=3600
|
||||
)
|
||||
|
||||
# 使用临时凭证
|
||||
temp_creds = assumed_role['Credentials']
|
||||
s3_cross = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=temp_creds['AccessKeyId'],
|
||||
aws_secret_access_key=temp_creds['SecretAccessKey'],
|
||||
aws_session_token=temp_creds['SessionToken']
|
||||
)`,
|
||||
tools: ['IAM Roles', 'AWS STS', 'EC2 Instance Profiles', 'Lambda Execution Roles']
|
||||
priorityText: 'P1',
|
||||
description: 'IAM 角色没有长期凭证,通过临时凭证访问,降低泄露风险。',
|
||||
checklist: ['EC2 使用实例角色', 'Lambda 使用执行角色', '跨账号用 AssumeRole']
|
||||
},
|
||||
{
|
||||
icon: '🔑',
|
||||
title: '访问密钥安全管理',
|
||||
priority: 'p1',
|
||||
priorityText: 'P1 - 高优先级',
|
||||
color: 'rgba(var(--vp-c-brand-rgb), 0.1)',
|
||||
description: '如果必须使用访问密钥(AK/SK),需要实施严格的安全管理措施。',
|
||||
checklist: [
|
||||
'绝不将 AK/SK 硬编码在代码或配置文件中',
|
||||
'使用环境变量或密钥管理服务(如 AWS Secrets Manager)',
|
||||
'每 90 天轮换一次访问密钥',
|
||||
'定期审查和删除未使用的访问密钥',
|
||||
'启用 CloudTrail 记录所有 AK/SK 的使用情况'
|
||||
],
|
||||
code: `# ❌ 错误做法 - 硬编码凭证
|
||||
import boto3
|
||||
|
||||
s3 = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id='AKIAIOSFODNN7EXAMPLE',
|
||||
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
||||
)
|
||||
|
||||
# ✅ 正确做法 - 使用环境变量
|
||||
import boto3
|
||||
import os
|
||||
|
||||
s3 = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID'),
|
||||
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY')
|
||||
)
|
||||
|
||||
# ✅ 正确做法 - 使用 AWS Secrets Manager
|
||||
import boto3
|
||||
import json
|
||||
|
||||
secrets_client = boto3.client('secretsmanager')
|
||||
secret_value = secrets_client.get_secret_value(SecretId='my-app/credentials')
|
||||
credentials = json.loads(secret_value['SecretString'])
|
||||
|
||||
s3 = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=credentials['access_key_id'],
|
||||
aws_secret_access_key=credentials['secret_access_key']
|
||||
)`,
|
||||
tools: ['AWS Secrets Manager', 'HashiCorp Vault', 'Azure Key Vault', 'GCP Secret Manager']
|
||||
priorityText: 'P1',
|
||||
description: '如果必须使用 AK/SK,需要实施严格的安全管理措施。',
|
||||
checklist: ['不硬编码凭证', '使用密钥管理服务', '定期轮换密钥']
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
title: '监控与审计',
|
||||
priority: 'p2',
|
||||
priorityText: 'P2 - 中优先级',
|
||||
color: 'var(--vp-c-bg-alt)',
|
||||
description: '建立全面的监控和审计机制,及时发现和响应安全事件。',
|
||||
checklist: [
|
||||
'启用 CloudTrail 记录所有 API 调用',
|
||||
'配置关键操作的实时告警(根账号使用、策略变更等)',
|
||||
'使用 IAM Access Analyzer 持续分析权限',
|
||||
'定期审查 IAM 用户和权限配置',
|
||||
'将日志存储到独立的审计账号,防止篡改'
|
||||
],
|
||||
code: `# AWS CloudTrail 配置示例
|
||||
aws cloudtrail create-trail \
|
||||
--name OrganizationTrail \
|
||||
--s3-bucket-name my-cloudtrail-bucket \
|
||||
--is-organization-trail \
|
||||
--enable-log-file-validation \
|
||||
--is-multi-region-trail
|
||||
|
||||
# CloudWatch 告警配置 - 根账号使用
|
||||
aws cloudwatch put-metric-alarm \
|
||||
--alarm-name RootAccountUsageAlarm \
|
||||
--alarm-description "Alert when root account is used" \
|
||||
--metric-name RootAccountUsage \
|
||||
--namespace CloudTrailMetrics \
|
||||
--statistic Sum \
|
||||
--period 300 \
|
||||
--evaluation-periods 1 \
|
||||
--threshold 1 \
|
||||
--comparison-operator GreaterThanOrEqualToThreshold
|
||||
|
||||
# IAM Access Analyzer 创建分析器
|
||||
aws accessanalyzer create-analyzer \
|
||||
--analyzer-name MyOrgAnalyzer \
|
||||
--type ORGANIZATION`,
|
||||
tools: ['AWS CloudTrail', 'AWS CloudWatch', 'IAM Access Analyzer', 'AWS Config', 'AWS Security Hub']
|
||||
priorityText: 'P2',
|
||||
description: '建立全面的监控和审计机制,及时发现安全事件。',
|
||||
checklist: ['启用 CloudTrail', '配置关键操作告警', '定期审查权限']
|
||||
}
|
||||
]
|
||||
|
||||
@@ -262,236 +92,102 @@ function toggleCard(index) {
|
||||
.best-practices-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.practices-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.practices-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.practice-card {
|
||||
.practice-item {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.practice-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.practice-card.expanded {
|
||||
.practice-item:hover { border-color: var(--vp-c-brand); }
|
||||
.practice-item.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.item-icon { font-size: 1rem; }
|
||||
.item-title { font-weight: 600; font-size: 0.85rem; flex: 1; }
|
||||
|
||||
.title-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-wrapper h5 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1rem;
|
||||
.item-priority {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.priority {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.item-priority.p0 { background: var(--vp-c-danger); color: #fff; }
|
||||
.item-priority.p1 { background: var(--vp-c-warning); color: #fff; }
|
||||
.item-priority.p2 { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
||||
|
||||
.priority.p0 {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.priority.p1 {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.priority.p2 {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1.25rem;
|
||||
.item-body {
|
||||
padding: 0 0.6rem 0.6rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
margin-top: 0;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.checklist {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.checklist h6 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.checklist ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist li {
|
||||
padding: 0.375rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.checklist li:before {
|
||||
content: '☐';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.code-example {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.code-example h6 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.code-example pre {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-example code {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
.item-desc {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tools h6 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
.item-checks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
padding: 0.25rem 0.75rem;
|
||||
.check-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.title-wrapper h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,230 +3,173 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔗</span>
|
||||
<span class="title">跨账号访问</span>
|
||||
<span class="subtitle">理解跨账号访问的 AssumeRole 机制</span>
|
||||
<span class="subtitle">AssumeRole 机制</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="flow-diagram">
|
||||
<div class="account-box source">
|
||||
<div class="account-header">账号 A(源账号)</div>
|
||||
<div class="account-content">
|
||||
<div class="entity">IAM User / Application</div>
|
||||
<div class="action">调用 sts:AssumeRole</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">→</div>
|
||||
|
||||
<div class="account-box sts">
|
||||
<div class="account-header">STS 服务</div>
|
||||
<div class="account-content">
|
||||
<div class="step">1. 验证源身份</div>
|
||||
<div class="step">2. 检查信任策略</div>
|
||||
<div class="step">3. 生成临时凭证</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow">→</div>
|
||||
|
||||
<div class="account-box target">
|
||||
<div class="account-header">账号 B(目标账号)</div>
|
||||
<div class="account-content">
|
||||
<div class="entity">CrossAccountRole</div>
|
||||
<div class="resource">访问 S3 / EC2 等资源</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-diagram">
|
||||
<div class="account-box source">
|
||||
<div class="account-header">账号 A(源)</div>
|
||||
<div class="entity">IAM User</div>
|
||||
<div class="action">sts:AssumeRole</div>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="account-box sts">
|
||||
<div class="account-header">STS 服务</div>
|
||||
<div class="step">验证身份</div>
|
||||
<div class="step">生成临时凭证</div>
|
||||
</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="account-box target">
|
||||
<div class="account-header">账号 B(目标)</div>
|
||||
<div class="entity">CrossAccountRole</div>
|
||||
<div class="resource">访问 S3/EC2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h5>Python 代码示例</h5>
|
||||
<pre><code>import boto3
|
||||
|
||||
# 在账号 A 中使用 IAM 用户凭证
|
||||
sts_client = boto3.client('sts')
|
||||
|
||||
# 扮演账号 B 的角色
|
||||
assumed_role = sts_client.assume_role(
|
||||
<div class="code-block">
|
||||
<div class="code-title">Python 示例</div>
|
||||
<pre><code>sts = boto3.client('sts')
|
||||
assumed = sts.assume_role(
|
||||
RoleArn='arn:aws:iam::123456789012:role/CrossAccountRole',
|
||||
RoleSessionName='MySession',
|
||||
DurationSeconds=3600
|
||||
RoleSessionName='MySession'
|
||||
)
|
||||
|
||||
# 获取临时凭证
|
||||
credentials = assumed_role['Credentials']
|
||||
|
||||
# 使用临时凭证访问账号 B 的资源
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=credentials['AccessKeyId'],
|
||||
aws_secret_access_key=credentials['SecretAccessKey'],
|
||||
aws_session_token=credentials['SessionToken']
|
||||
)</code></pre>
|
||||
</div>
|
||||
# 使用临时凭证访问目标账号资源</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>通过角色扮演实现跨账号访问,无需在每个账号创建 IAM 用户,临时凭证自动过期,更安全更易管理。
|
||||
<strong>核心思想:</strong>通过角色扮演实现跨账号访问,临时凭证自动过期,更安全更易管理。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// No script needed for this static demo
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cross-account-access-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.account-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-width: 180px;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
padding-bottom: 0.3rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.account-content {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.entity {
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.7rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.step {
|
||||
padding: 0.25rem 0;
|
||||
padding: 0.15rem 0;
|
||||
color: var(--vp-c-text-2);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.step:last-child {
|
||||
border-bottom: none;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.resource {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 2rem;
|
||||
font-size: 1.25rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.code-example {
|
||||
.code-block {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.code-example h5 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
.code-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.code-example pre {
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-example code {
|
||||
.code-block code {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flow-diagram {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.account-box {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.flow-diagram { flex-direction: column; }
|
||||
.arrow { transform: rotate(90deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,43 +6,37 @@
|
||||
<span class="subtitle">云上权限管理的基础构件</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="structure-layers">
|
||||
<div
|
||||
v-for="(layer, index) in layers"
|
||||
:key="index"
|
||||
class="layer"
|
||||
:class="{ active: selectedLayer === index }"
|
||||
@click="selectLayer(index)"
|
||||
>
|
||||
<div class="layer-icon">{{ layer.icon }}</div>
|
||||
<div class="layer-content">
|
||||
<div class="layer-name">{{ layer.name }}</div>
|
||||
<div class="layer-desc">{{ layer.shortDesc }}</div>
|
||||
<div class="main-area">
|
||||
<div class="layers-list">
|
||||
<div
|
||||
v-for="(layer, index) in layers"
|
||||
:key="index"
|
||||
class="layer"
|
||||
:class="{ active: selectedLayer === index }"
|
||||
@click="selectLayer(index)"
|
||||
>
|
||||
<span class="layer-icon">{{ layer.icon }}</span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<span class="layer-desc">{{ layer.shortDesc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLayerData" class="layer-detail">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
|
||||
<span class="detail-name">{{ selectedLayerData.name }}</span>
|
||||
</div>
|
||||
<div class="detail-desc">{{ selectedLayerData.description }}</div>
|
||||
<div class="detail-examples">
|
||||
<div class="example-title">示例:</div>
|
||||
<ul>
|
||||
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
|
||||
{{ example }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="layer-detail">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
|
||||
<span class="detail-name">{{ selectedLayerData.name }}</span>
|
||||
</div>
|
||||
<div class="detail-desc">{{ selectedLayerData.description }}</div>
|
||||
<div class="detail-examples">
|
||||
<span class="example-label">示例:</span>
|
||||
<span v-for="(example, i) in selectedLayerData.examples.slice(0, 2)" :key="i" class="example-tag">{{ example }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>IAM 就像公司的门禁系统——根账号是老板(拥有所有钥匙),用户是员工(有特定权限),角色是临时访客证(有时效),策略是"谁能进哪些门"的规则。
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>IAM 就像公司的门禁系统——根账号是老板,用户是员工,角色是临时访客证,策略是"谁能进哪些门"的规则。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -57,61 +51,36 @@ const layers = [
|
||||
icon: '👑',
|
||||
name: '根账号',
|
||||
shortDesc: '最高权限',
|
||||
description: '云账号的所有者,拥有全部资源的完全控制权限。建议仅用于初始设置,日常操作使用子账号。',
|
||||
examples: [
|
||||
'创建/删除 IAM 用户',
|
||||
'管理账单和支付方式',
|
||||
'关闭账号',
|
||||
'恢复已删除资源'
|
||||
]
|
||||
description: '云账号的所有者,拥有全部资源的完全控制权限。建议仅用于初始设置。',
|
||||
examples: ['创建/删除 IAM 用户', '管理账单和支付方式']
|
||||
},
|
||||
{
|
||||
icon: '👤',
|
||||
name: 'IAM 用户',
|
||||
shortDesc: '个人身份',
|
||||
description: '为具体人员(如员工)创建的长期凭证,用于日常登录和操作云服务。',
|
||||
examples: [
|
||||
'开发人员账号',
|
||||
'运维人员账号',
|
||||
'只读审计账号',
|
||||
'API 调用账号'
|
||||
]
|
||||
description: '为具体人员创建的长期凭证,用于日常登录和操作云服务。',
|
||||
examples: ['开发人员账号', '运维人员账号']
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
name: '用户组',
|
||||
shortDesc: '批量管理',
|
||||
description: '将多个用户归为一组,统一分配权限,简化管理。',
|
||||
examples: [
|
||||
'开发组(开发权限)',
|
||||
'运维组(运维权限)',
|
||||
'财务组(账单权限)',
|
||||
'审计组(只读权限)'
|
||||
]
|
||||
examples: ['开发组', '运维组']
|
||||
},
|
||||
{
|
||||
icon: '🎭',
|
||||
name: '角色',
|
||||
shortDesc: '临时授权',
|
||||
description: '一种临时身份,可以被切换或赋予其他账号/服务,具有时效性更安全。',
|
||||
examples: [
|
||||
'跨账号访问角色',
|
||||
'服务角色(如 Lambda)',
|
||||
'临时运维角色',
|
||||
'第三方登录角色'
|
||||
]
|
||||
examples: ['跨账号访问角色', '服务角色']
|
||||
},
|
||||
{
|
||||
icon: '📋',
|
||||
name: '策略',
|
||||
shortDesc: '权限规则',
|
||||
description: '定义"谁可以对什么资源执行什么操作"的规则文档,以 JSON 格式编写。',
|
||||
examples: [
|
||||
'允许访问 S3 存储桶',
|
||||
'禁止删除 EC2 实例',
|
||||
'只允许查看 RDS',
|
||||
'允许特定时间段访问'
|
||||
]
|
||||
examples: ['允许访问 S3', '禁止删除 EC2']
|
||||
}
|
||||
]
|
||||
|
||||
@@ -125,53 +94,45 @@ function selectLayer(index) {
|
||||
<style scoped>
|
||||
.iam-structure {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
@media (max-width: 640px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.structure-layers {
|
||||
.layers-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
@@ -179,108 +140,71 @@ function selectLayer(index) {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.layer:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.layer:hover { border-color: var(--vp-c-brand); }
|
||||
.layer.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.layer-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.layer-icon { font-size: 1rem; }
|
||||
.layer-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.layer-desc { font-size: 0.75rem; color: var(--vp-c-text-2); margin-left: auto; }
|
||||
|
||||
.layer-detail {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.detail-icon { font-size: 1.25rem; }
|
||||
.detail-name { font-weight: 600; font-size: 0.95rem; }
|
||||
|
||||
.detail-desc {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-examples {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-examples ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
font-size: 0.8rem;
|
||||
.example-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.detail-examples li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-examples li:last-child {
|
||||
margin-bottom: 0;
|
||||
.example-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,86 +3,59 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span class="title">IAM vs RAM 对比</span>
|
||||
<span class="subtitle">理解不同云厂商的权限管理服务</span>
|
||||
<span class="subtitle">不同云厂商权限管理服务</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="comparison-container">
|
||||
<!-- AWS IAM Column -->
|
||||
<div class="platform-column aws-column">
|
||||
<div class="platform-header aws">
|
||||
<div class="logo">AWS</div>
|
||||
<h5>IAM</h5>
|
||||
<span class="subtitle">Identity and Access Management</span>
|
||||
</div>
|
||||
<div class="main-area">
|
||||
<div class="platform-col aws">
|
||||
<div class="platform-header">AWS IAM</div>
|
||||
<div
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
class="feature-item"
|
||||
:class="{ active: selectedFeature === index }"
|
||||
@click="selectedFeature = index"
|
||||
>
|
||||
<span class="icon">{{ feature.icon }}</span>
|
||||
<span class="name">{{ feature.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
<div
|
||||
v-for="(feature, index) in awsFeatures"
|
||||
:key="index"
|
||||
class="feature-item"
|
||||
:class="{ active: selectedFeature === `aws-${index}` }"
|
||||
@click="selectFeature('aws', index)"
|
||||
>
|
||||
<div class="feature-icon">{{ feature.icon }}</div>
|
||||
<div class="feature-content">
|
||||
<span class="feature-name">{{ feature.name }}</span>
|
||||
<span class="feature-desc">{{ feature.desc }}</span>
|
||||
</div>
|
||||
<div class="comparison-col">
|
||||
<div class="comparison-card" v-if="selectedFeatureData">
|
||||
<div class="comp-title">{{ selectedFeatureData.name }}</div>
|
||||
<div class="comp-row">
|
||||
<div class="comp-item aws">
|
||||
<div class="comp-label">AWS IAM</div>
|
||||
<div class="comp-desc">{{ selectedFeatureData.awsDetail }}</div>
|
||||
</div>
|
||||
<div class="comp-vs">VS</div>
|
||||
<div class="comp-item ram">
|
||||
<div class="comp-label">阿里云 RAM</div>
|
||||
<div class="comp-desc">{{ selectedFeatureData.ramDetail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comparison Details -->
|
||||
<div class="comparison-details" v-if="selectedFeatureData">
|
||||
<div class="detail-card">
|
||||
<h6>{{ selectedFeatureData.name }}</h6>
|
||||
<div class="comparison-row">
|
||||
<div class="aws-detail">
|
||||
<span class="label">AWS IAM</span>
|
||||
<p>{{ selectedFeatureData.awsDetail }}</p>
|
||||
<code v-if="selectedFeatureData.awsExample">{{ selectedFeatureData.awsExample }}</code>
|
||||
</div>
|
||||
<div class="vs-divider">VS</div>
|
||||
<div class="ram-detail">
|
||||
<span class="label">阿里云 RAM</span>
|
||||
<p>{{ selectedFeatureData.ramDetail }}</p>
|
||||
<code v-if="selectedFeatureData.ramExample">{{ selectedFeatureData.ramExample }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alibaba Cloud RAM Column -->
|
||||
<div class="platform-column ram-column">
|
||||
<div class="platform-header ram">
|
||||
<div class="logo">阿里云</div>
|
||||
<h5>RAM</h5>
|
||||
<span class="subtitle">Resource Access Management</span>
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
<div
|
||||
v-for="(feature, index) in ramFeatures"
|
||||
:key="index"
|
||||
class="feature-item"
|
||||
:class="{ active: selectedFeature === `ram-${index}` }"
|
||||
@click="selectFeature('ram', index)"
|
||||
>
|
||||
<div class="feature-icon">{{ feature.icon }}</div>
|
||||
<div class="feature-content">
|
||||
<span class="feature-name">{{ feature.name }}</span>
|
||||
<span class="feature-desc">{{ feature.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="platform-col ram">
|
||||
<div class="platform-header">阿里云 RAM</div>
|
||||
<div
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
class="feature-item"
|
||||
:class="{ active: selectedFeature === index }"
|
||||
@click="selectedFeature = index"
|
||||
>
|
||||
<span class="icon">{{ feature.icon }}</span>
|
||||
<span class="name">{{ feature.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>IAM 和 RAM 的核心概念基本一致,只是术语和实现细节略有不同。掌握一个平台后,可以快速迁移到另一个平台。
|
||||
<strong>核心思想:</strong>IAM 和 RAM 核心概念基本一致,只是术语和实现细节略有不同。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -90,85 +63,36 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selectedFeature = ref(null)
|
||||
const selectedFeature = ref(0)
|
||||
|
||||
const featureDetails = [
|
||||
{
|
||||
name: '用户管理',
|
||||
awsDetail: '使用 IAM User,支持编程访问和控制台访问,可分配独立 AK/SK',
|
||||
ramDetail: '使用 RAM 用户,功能与 IAM User 类似,支持子账号登录控制台',
|
||||
awsExample: 'arn:aws:iam::123456789012:user/alice',
|
||||
ramExample: 'acs:ram::123456789012:user/alice'
|
||||
},
|
||||
{
|
||||
name: '用户组管理',
|
||||
awsDetail: 'IAM Group 用于批量管理用户权限,一个用户可属于多个组',
|
||||
ramDetail: 'RAM 用户组功能类似,支持按部门或项目分组管理',
|
||||
awsExample: 'arn:aws:iam::123456789012:group/Developers',
|
||||
ramExample: 'acs:ram::123456789012:group/Developers'
|
||||
},
|
||||
{
|
||||
name: '角色与扮演',
|
||||
awsDetail: 'IAM Role 支持跨账号访问和服务角色,使用 STS AssumeRole',
|
||||
ramDetail: 'RAM 角色支持跨云账号访问和临时授权,使用 STS AssumeRole',
|
||||
awsExample: 'arn:aws:iam::123456789012:role/CrossAccountRole',
|
||||
ramExample: 'acs:ram::123456789012:role/CrossAccountRole'
|
||||
},
|
||||
{
|
||||
name: '权限策略',
|
||||
awsDetail: 'IAM Policy 使用 JSON 格式,支持 Action/Resource/Condition',
|
||||
ramDetail: 'RAM Policy 语法类似,支持阿里云服务特定的 Action',
|
||||
awsExample: '"Action": "s3:GetObject"',
|
||||
ramExample: '"Action": "oss:GetObject"'
|
||||
},
|
||||
{
|
||||
name: '身份联合',
|
||||
awsDetail: '支持 SAML 2.0 和 OIDC,可与 AD、Okta 等 IdP 集成',
|
||||
ramDetail: '支持 SAML 2.0 和企业 AD/LDAP,支持钉钉等国内 IdP',
|
||||
awsExample: 'SAML Provider: arn:aws:iam::123:saml-provider/Okta',
|
||||
ramExample: 'SAML Provider: acs:ram::123:saml-provider/DingTalk'
|
||||
},
|
||||
{
|
||||
name: '访问密钥',
|
||||
awsDetail: 'IAM User 可创建 AK/SK,支持定期轮换和访问分析',
|
||||
ramDetail: 'RAM 用户支持 AccessKey,提供密钥使用分析和安全建议',
|
||||
awsExample: 'AKIAIOSFODNN7EXAMPLE',
|
||||
ramExample: 'LTAI...'
|
||||
}
|
||||
const features = [
|
||||
{ icon: '👤', name: '用户管理' },
|
||||
{ icon: '👥', name: '用户组' },
|
||||
{ icon: '🎭', name: '角色扮演' },
|
||||
{ icon: '📋', name: '权限策略' },
|
||||
{ icon: '🔗', name: '身份联合' },
|
||||
{ icon: '🔑', name: '访问密钥' }
|
||||
]
|
||||
|
||||
const awsFeatures = featureDetails.map((f, i) => ({
|
||||
icon: ['👤', '👥', '🎭', '📋', '🔗', '🔑'][i],
|
||||
name: f.name,
|
||||
desc: f.awsDetail.slice(0, 30) + '...'
|
||||
}))
|
||||
const featureDetails = [
|
||||
{ name: '用户管理', awsDetail: 'IAM User,支持编程访问和控制台访问', ramDetail: 'RAM 用户,功能类似,支持子账号登录' },
|
||||
{ name: '用户组管理', awsDetail: 'IAM Group 批量管理用户权限', ramDetail: 'RAM 用户组,按部门分组管理' },
|
||||
{ name: '角色与扮演', awsDetail: 'IAM Role + STS AssumeRole', ramDetail: 'RAM 角色 + STS AssumeRole' },
|
||||
{ name: '权限策略', awsDetail: 'JSON 格式 Policy', ramDetail: '语法类似的权限策略' },
|
||||
{ name: '身份联合', awsDetail: 'SAML 2.0 / OIDC,支持 AD/Okta', ramDetail: 'SAML 2.0,支持钉钉等' },
|
||||
{ name: '访问密钥', awsDetail: 'AK/SK,支持轮换和分析', ramDetail: 'AccessKey,提供安全建议' }
|
||||
]
|
||||
|
||||
const ramFeatures = featureDetails.map((f, i) => ({
|
||||
icon: ['👤', '👥', '🎭', '📋', '🔗', '🔑'][i],
|
||||
name: f.name,
|
||||
desc: f.ramDetail.slice(0, 30) + '...'
|
||||
}))
|
||||
|
||||
const selectedFeatureData = computed(() => {
|
||||
if (!selectedFeature.value) return null
|
||||
const [platform, index] = selectedFeature.value.split('-')
|
||||
return featureDetails[parseInt(index)]
|
||||
})
|
||||
|
||||
function selectFeature(platform, index) {
|
||||
selectedFeature.value = `${platform}-${index}`
|
||||
}
|
||||
const selectedFeatureData = computed(() => featureDetails[selectedFeature.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iam-ram-comparison-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
@@ -182,191 +106,110 @@ function selectFeature(platform, index) {
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.demo-content { margin-bottom: 0.75rem; }
|
||||
|
||||
.comparison-container {
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr 1fr;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.platform-column {
|
||||
@media (max-width: 768px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.platform-col {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.platform-header {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.platform-header.aws {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.platform-header.ram {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.platform-header .logo {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.platform-header h5 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.platform-header .subtitle {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.features-list { padding: 0.75rem; }
|
||||
.platform-col.aws .platform-header { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
||||
.platform-col.ram .platform-header { background: rgba(239, 68, 68, 0.1); color: #dc2626; }
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.feature-item:last-child { border-bottom: none; }
|
||||
.feature-item:hover { background: var(--vp-c-bg-alt); }
|
||||
.feature-item.active { background: var(--vp-c-brand-soft); }
|
||||
|
||||
.feature-item .icon { font-size: 1rem; }
|
||||
.feature-item .name { font-size: 0.8rem; color: var(--vp-c-text-1); }
|
||||
|
||||
.comparison-col { min-width: 0; }
|
||||
|
||||
.comparison-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.75rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.feature-item:hover,
|
||||
.feature-item.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateX(4px);
|
||||
.comp-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-icon { font-size: 1.2rem; }
|
||||
|
||||
.feature-content {
|
||||
.comp-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.comparison-details {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-card { text-align: center; }
|
||||
|
||||
.detail-card h6 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.comparison-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.aws-detail,
|
||||
.ram-detail {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
text-align: left;
|
||||
.comp-item {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.aws-detail .label,
|
||||
.ram-detail .label {
|
||||
display: block;
|
||||
.comp-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.comp-item.aws .comp-label { color: var(--vp-c-brand-1); }
|
||||
.comp-item.ram .comp-label { color: #dc2626; }
|
||||
|
||||
.comp-desc { font-size: 0.75rem; color: var(--vp-c-text-2); line-height: 1.4; }
|
||||
|
||||
.comp-vs {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.375rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.aws-detail .label { color: var(--vp-c-brand-1); }
|
||||
.ram-detail .label { color: var(--vp-c-brand-delta); }
|
||||
|
||||
.aws-detail p,
|
||||
.ram-detail p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.aws-detail code,
|
||||
.ram-detail code {
|
||||
display: block;
|
||||
padding: 0.375rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.6rem;
|
||||
word-break: break-all;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vs-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-3);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.comparison-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.comparison-details { order: -1; }
|
||||
.comparison-row { flex-direction: column; }
|
||||
.vs-divider { padding: 0.5rem 0; }
|
||||
}
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,44 +3,35 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span class="title">身份提供商集成</span>
|
||||
<span class="subtitle">理解企业 SSO 单点登录流程</span>
|
||||
<span class="subtitle">企业 SSO 单点登录流程</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="sso-flow">
|
||||
<div class="flow-step" v-for="(step, index) in steps" :key="index"
|
||||
:class="{ active: currentStep === index, completed: currentStep > index }"
|
||||
@click="goToStep(index)">
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<span class="step-title">{{ step.title }}</span>
|
||||
<span class="step-desc">{{ step.desc }}</span>
|
||||
</div>
|
||||
<div class="step-arrow" v-if="index < steps.length - 1">→</div>
|
||||
</div>
|
||||
<div class="flow-steps">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="step"
|
||||
:class="{ active: currentStep === index }"
|
||||
@click="currentStep = index"
|
||||
>
|
||||
<span class="step-num">{{ index + 1 }}</span>
|
||||
<span class="step-title">{{ step.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel" v-if="currentStepData">
|
||||
<h5>{{ currentStepData.title }}</h5>
|
||||
<p>{{ currentStepData.detail }}</p>
|
||||
|
||||
<div class="code-block" v-if="currentStepData.code">
|
||||
<pre><code>{{ currentStepData.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="entity-flow" v-if="currentStepData.flow">
|
||||
<div class="flow-row" v-for="(row, i) in currentStepData.flow" :key="i">
|
||||
<span class="entity" :class="row.from.type">{{ row.from.name }}</span>
|
||||
<span class="action">{{ row.action }}</span>
|
||||
<span class="entity" :class="row.to.type">{{ row.to.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-panel">
|
||||
<div class="detail-title">{{ currentStepData.title }}</div>
|
||||
<p class="detail-desc">{{ currentStepData.detail }}</p>
|
||||
<div class="flow-row" v-if="currentStepData.flow">
|
||||
<span class="entity user">{{ currentStepData.flow[0].from.name }}</span>
|
||||
<span class="action">{{ currentStepData.flow[0].action }}</span>
|
||||
<span class="entity cloud">{{ currentStepData.flow[0].to.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>通过企业 IdP 统一管理用户身份,避免在每个云平台单独创建账号,提高安全性和管理效率。
|
||||
<strong>核心思想:</strong>通过企业 IdP 统一管理用户身份,避免在每个云平台单独创建账号。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -51,356 +42,140 @@ import { ref, computed } from 'vue'
|
||||
const currentStep = ref(0)
|
||||
|
||||
const steps = [
|
||||
{ title: '用户访问应用', desc: '用户尝试访问企业应用' },
|
||||
{ title: '重定向到 IdP', desc: '应用将用户重定向到身份提供商' },
|
||||
{ title: '用户登录认证', desc: '用户在 IdP 输入企业账号密码' },
|
||||
{ title: '颁发 SAML 令牌', desc: 'IdP 验证成功后颁发 SAML Assertion' },
|
||||
{ title: '返回应用', desc: '携带令牌返回企业应用' },
|
||||
{ title: '换取云凭证', desc: '应用使用令牌换取云临时凭证' },
|
||||
{ title: '访问云资源', desc: '使用临时凭证访问云资源' }
|
||||
{ title: '访问应用' },
|
||||
{ title: '重定向 IdP' },
|
||||
{ title: '用户登录' },
|
||||
{ title: '颁发令牌' },
|
||||
{ title: '返回应用' },
|
||||
{ title: '换取凭证' },
|
||||
{ title: '访问资源' }
|
||||
]
|
||||
|
||||
const stepDetails = [
|
||||
{
|
||||
title: '用户访问企业应用',
|
||||
detail: '用户打开浏览器,访问企业内部的业务系统(如 CRM、ERP 等)。此时用户尚未登录,应用检测到用户没有有效的会话。',
|
||||
flow: [
|
||||
{ from: { name: '用户', type: 'user' }, action: '访问 →', to: { name: '企业应用', type: 'app' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '应用重定向到 IdP',
|
||||
detail: '应用发现用户未登录,生成 SAML Request,将用户浏览器重定向到企业的身份提供商(IdP,如 Azure AD、Okta 等)。',
|
||||
code: `// SAML Request 示例
|
||||
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
Destination="https://idp.example.com/saml/sso"
|
||||
ID="_1234567890"
|
||||
IssueInstant="2024-01-15T10:00:00Z">
|
||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||
https://app.example.com
|
||||
</saml:Issuer>
|
||||
</samlp:AuthnRequest>`,
|
||||
flow: [
|
||||
{ from: { name: '企业应用', type: 'app' }, action: '重定向 →', to: { name: 'IdP', type: 'idp' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '用户在 IdP 登录',
|
||||
detail: '用户在 IdP 的登录页面输入企业账号和密码。IdP 验证用户身份,可能还需要进行 MFA 多因素认证。',
|
||||
flow: [
|
||||
{ from: { name: '用户', type: 'user' }, action: '登录 →', to: { name: 'IdP', type: 'idp' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'IdP 颁发 SAML Assertion',
|
||||
detail: '用户认证成功后,IdP 生成包含用户身份和属性的 SAML Assertion(断言),并使用 IdP 的私钥签名。',
|
||||
code: `<!-- SAML Response 示例 -->
|
||||
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
ID="_response123"
|
||||
InResponseTo="_1234567890"
|
||||
IssueInstant="2024-01-15T10:01:00Z">
|
||||
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
ID="_assertion456"
|
||||
IssueInstant="2024-01-15T10:01:00Z">
|
||||
<saml:Subject>
|
||||
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
|
||||
user@example.com
|
||||
</saml:NameID>
|
||||
</saml:Subject>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="Role">
|
||||
<saml:AttributeValue>Admin</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
</samlp:Response>`,
|
||||
flow: [
|
||||
{ from: { name: 'IdP', type: 'idp' }, action: '颁发令牌 →', to: { name: '用户浏览器', type: 'user' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '返回企业应用',
|
||||
detail: 'IdP 通过浏览器将 SAML Response POST 到企业应用。应用验证 SAML 断言的签名,确认用户身份,建立用户会话。',
|
||||
flow: [
|
||||
{ from: { name: '用户浏览器', type: 'user' }, action: 'POST 令牌 →', to: { name: '企业应用', type: 'app' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '换取云临时凭证',
|
||||
detail: '企业应用使用 SAML Assertion 向云厂商的 STS 服务请求临时安全凭证。云服务验证 SAML 断言后,颁发临时 AK/SK/Token。',
|
||||
code: `# Python 示例:使用 SAML 换取 AWS 临时凭证
|
||||
import boto3
|
||||
|
||||
# 创建 STS 客户端
|
||||
sts = boto3.client('sts')
|
||||
|
||||
# 使用 SAML 断言请求临时凭证
|
||||
response = sts.assume_role_with_saml(
|
||||
RoleArn='arn:aws:iam::123456789012:role/SAML-Role',
|
||||
PrincipalArn='arn:aws:iam::123456789012:saml-provider/Okta',
|
||||
SAMLAssertion='VGhpcyBpcyBhIHRlc3QgU0FNTCBhc3NlcnRpb24=',
|
||||
DurationSeconds=3600
|
||||
)
|
||||
|
||||
# 获取临时凭证
|
||||
credentials = response['Credentials']
|
||||
print(f"Access Key: {credentials['AccessKeyId']}")
|
||||
print(f"Secret Key: {credentials['SecretAccessKey']}")
|
||||
print(f"Session Token: {credentials['SessionToken']}")`,
|
||||
flow: [
|
||||
{ from: { name: '企业应用', type: 'app' }, action: 'AssumeRole →', to: { name: '云 STS', type: 'cloud' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '访问云资源',
|
||||
detail: '企业应用使用获取到的临时凭证,调用云服务的 API 访问资源(如 S3、EC2、数据库等)。临时凭证有过期时间,到期后需要重新获取。',
|
||||
flow: [
|
||||
{ from: { name: '企业应用', type: 'app' }, action: '访问资源 →', to: { name: '云服务', type: 'cloud' } }
|
||||
]
|
||||
}
|
||||
{ title: '用户访问企业应用', detail: '用户打开浏览器访问企业业务系统,应用检测到用户没有有效会话。', flow: [{ from: { name: '用户' }, action: '访问 →', to: { name: '企业应用' } }] },
|
||||
{ title: '应用重定向到 IdP', detail: '应用生成 SAML Request,将用户重定向到企业身份提供商。', flow: [{ from: { name: '应用' }, action: '重定向 →', to: { name: 'IdP' } }] },
|
||||
{ title: '用户在 IdP 登录', detail: '用户在 IdP 登录页面输入企业账号密码,可能需要 MFA 认证。', flow: [{ from: { name: '用户' }, action: '登录 →', to: { name: 'IdP' } }] },
|
||||
{ title: 'IdP 颁发 SAML 令牌', detail: '用户认证成功后,IdP 生成包含用户身份的 SAML Assertion。', flow: [{ from: { name: 'IdP' }, action: '颁发 →', to: { name: '令牌' } }] },
|
||||
{ title: '返回企业应用', detail: 'IdP 通过浏览器将 SAML Response POST 到企业应用。', flow: [{ from: { name: '浏览器' }, action: 'POST →', to: { name: '应用' } }] },
|
||||
{ title: '换取云临时凭证', detail: '应用使用 SAML 向云 STS 服务请求临时安全凭证。', flow: [{ from: { name: '应用' }, action: 'AssumeRole →', to: { name: '云 STS' } }] },
|
||||
{ title: '访问云资源', detail: '应用使用临时凭证调用云服务 API 访问资源。', flow: [{ from: { name: '应用' }, action: '访问 →', to: { name: '云服务' } }] }
|
||||
]
|
||||
|
||||
const currentStepData = computed(() => {
|
||||
if (currentStep.value === null) return null
|
||||
return stepDetails[currentStep.value]
|
||||
})
|
||||
|
||||
function goToStep(index) {
|
||||
currentStep.value = index
|
||||
}
|
||||
const currentStepData = computed(() => stepDetails[currentStep.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.identity-provider-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sso-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 120px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.flow-step:hover,
|
||||
.flow-step.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.step:hover { border-color: var(--vp-c-brand); }
|
||||
.step.active { background: var(--vp-c-brand-soft); border-color: var(--vp-c-brand); }
|
||||
|
||||
.flow-step.completed {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.step-num {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.flow-step.active .step-number {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.6rem;
|
||||
color: var(--vp-c-text-3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.step.active .step-num { background: var(--vp-c-brand); color: #fff; }
|
||||
|
||||
.step-arrow {
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.step-title { font-size: 0.75rem; font-weight: 500; color: var(--vp-c-text-1); }
|
||||
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-panel h5 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.detail-panel p {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
.detail-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.75rem;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.4;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.entity-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.entity {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entity.user {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.entity.app {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.entity.idp {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.entity.cloud {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entity.user { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
||||
.entity.cloud { background: rgba(239, 68, 68, 0.1); color: #dc2626; }
|
||||
|
||||
.action { font-size: 0.7rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sso-flow {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,49 +3,47 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span class="title">多因素认证</span>
|
||||
<span class="subtitle">理解 MFA 双因素认证流程</span>
|
||||
<span class="subtitle">MFA 双因素认证流程</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="main-area">
|
||||
<div class="mfa-flow">
|
||||
<div class="auth-step" :class="{ active: step >= 1, completed: step > 1 }">
|
||||
<div class="step-icon">🔐</div>
|
||||
<div class="step-label">密码验证</div>
|
||||
<span class="step-icon">🔐</span>
|
||||
<span class="step-label">密码</span>
|
||||
</div>
|
||||
<div class="step-arrow">→</div>
|
||||
<span class="step-arrow">→</span>
|
||||
<div class="auth-step" :class="{ active: step >= 2, completed: step > 2 }">
|
||||
<div class="step-icon">📱</div>
|
||||
<div class="step-label">MFA 验证</div>
|
||||
<span class="step-icon">📱</span>
|
||||
<span class="step-label">MFA</span>
|
||||
</div>
|
||||
<div class="step-arrow">→</div>
|
||||
<span class="step-arrow">→</span>
|
||||
<div class="auth-step" :class="{ active: step >= 3 }">
|
||||
<div class="step-icon">✅</div>
|
||||
<div class="step-label">登录成功</div>
|
||||
<span class="step-icon">✅</span>
|
||||
<span class="step-label">成功</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-panel" v-if="step === 1">
|
||||
<h5>请输入密码</h5>
|
||||
<input type="password" v-model="password" placeholder="输入密码" @keyup.enter="verifyPassword" />
|
||||
<div class="panel-title">请输入密码</div>
|
||||
<input type="password" v-model="password" placeholder="输入任意密码" @keyup.enter="verifyPassword" />
|
||||
<button @click="verifyPassword" :disabled="!password">验证密码</button>
|
||||
</div>
|
||||
|
||||
<div class="auth-panel" v-if="step === 2">
|
||||
<h5>MFA 验证</h5>
|
||||
<div class="panel-title">MFA 验证码</div>
|
||||
<div class="totp-display">
|
||||
<span class="totp-code">{{ totpCode }}</span>
|
||||
<div class="totp-timer">
|
||||
<div class="timer-bar" :style="{ width: timerWidth + '%' }"></div>
|
||||
</div>
|
||||
<div class="totp-hint">模拟验证码</div>
|
||||
</div>
|
||||
<input type="text" v-model="userCode" placeholder="输入6位验证码" maxlength="6" @keyup.enter="verifyMFA" />
|
||||
<input type="text" v-model="userCode" placeholder="输入上方验证码" maxlength="6" @keyup.enter="verifyMFA" />
|
||||
<button @click="verifyMFA" :disabled="userCode.length !== 6">验证</button>
|
||||
</div>
|
||||
|
||||
<div class="success-message" v-if="step === 3">
|
||||
<div class="success-icon">🎉</div>
|
||||
<h5>登录成功!</h5>
|
||||
<p>已通过 MFA 双因素认证</p>
|
||||
<div class="success-panel" v-if="step === 3">
|
||||
<span class="success-icon">🎉</span>
|
||||
<div class="success-title">登录成功!</div>
|
||||
<div class="success-desc">已通过 MFA 双因素认证</div>
|
||||
<button @click="reset">重新演示</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,43 +56,27 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const step = ref(1)
|
||||
const password = ref('')
|
||||
const userCode = ref('')
|
||||
const totpCode = ref('123456')
|
||||
const timerWidth = ref(100)
|
||||
let timerInterval = null
|
||||
|
||||
function generateTOTP() {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString()
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
timerWidth.value = 100
|
||||
if (timerInterval) clearInterval(timerInterval)
|
||||
timerInterval = setInterval(() => {
|
||||
timerWidth.value -= 1.67
|
||||
if (timerWidth.value <= 0) {
|
||||
totpCode.value = generateTOTP()
|
||||
timerWidth.value = 100
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function verifyPassword() {
|
||||
if (password.value) {
|
||||
step.value = 2
|
||||
totpCode.value = generateTOTP()
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function verifyMFA() {
|
||||
if (userCode.value.length === 6) {
|
||||
step.value = 3
|
||||
if (timerInterval) clearInterval(timerInterval)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,55 +84,39 @@ function reset() {
|
||||
step.value = 1
|
||||
password.value = ''
|
||||
userCode.value = ''
|
||||
if (timerInterval) clearInterval(timerInterval)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (step.value === 2) startTimer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timerInterval) clearInterval(timerInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mfa-security-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
.main-area {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mfa-flow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -158,13 +124,13 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
gap: 0.25rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-step.active {
|
||||
@@ -173,178 +139,107 @@ onUnmounted(() => {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.auth-step.completed {
|
||||
opacity: 1;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.step-icon { font-size: 1.25rem; }
|
||||
.step-label { font-size: 0.7rem; font-weight: 500; }
|
||||
.step-arrow { font-size: 1rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.auth-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.auth-panel h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
.panel-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.auth-panel input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-panel input::placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.auth-panel button {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-panel button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-panel button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.3);
|
||||
}
|
||||
.auth-panel button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.auth-panel button:hover:not(:disabled) { opacity: 0.9; }
|
||||
|
||||
.totp-display {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.totp-code {
|
||||
display: block;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
letter-spacing: 0.2em;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.totp-timer {
|
||||
height: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timer-bar {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success-message h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.success-message p {
|
||||
margin: 0 0 1.25rem 0;
|
||||
color: var(--vp-c-text-2);
|
||||
.totp-hint {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.success-message button {
|
||||
padding: 0.75rem 2rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.success-panel {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon { font-size: 2rem; display: block; margin-bottom: 0.5rem; }
|
||||
.success-title { font-size: 1rem; font-weight: 700; color: var(--vp-c-text-1); margin-bottom: 0.25rem; }
|
||||
.success-desc { font-size: 0.8rem; color: var(--vp-c-text-2); margin-bottom: 0.75rem; }
|
||||
|
||||
.success-panel button {
|
||||
padding: 0.4rem 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 1rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-message button:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mfa-flow {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🏛️</span>
|
||||
<span class="title">权限层级结构</span>
|
||||
<span class="subtitle">理解不同权限级别的范围差异</span>
|
||||
<span class="subtitle">不同权限级别的范围差异</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="hierarchy-container">
|
||||
<div class="main-area">
|
||||
<div class="levels-list">
|
||||
<div
|
||||
v-for="(level, index) in hierarchyLevels"
|
||||
:key="index"
|
||||
@@ -15,55 +15,33 @@
|
||||
:class="{ active: selectedLevel === index }"
|
||||
@click="selectLevel(index)"
|
||||
>
|
||||
<div class="level-icon">{{ level.icon }}</div>
|
||||
<div class="level-content">
|
||||
<span class="level-icon">{{ level.icon }}</span>
|
||||
<div class="level-info">
|
||||
<span class="level-name">{{ level.name }}</span>
|
||||
<span class="level-scope">{{ level.scope }}</span>
|
||||
</div>
|
||||
<div class="permission-badges">
|
||||
<span
|
||||
v-for="(perm, i) in level.permissions.slice(0, 3)"
|
||||
:key="i"
|
||||
class="badge"
|
||||
>
|
||||
{{ perm }}
|
||||
</span>
|
||||
<span v-if="level.permissions.length > 3" class="badge more">
|
||||
+{{ level.permissions.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel" v-if="selectedLevelData">
|
||||
<h5>{{ selectedLevelData.name }} 详情</h5>
|
||||
<div class="detail-section">
|
||||
<span class="label">权限范围:</span>
|
||||
<div class="detail-title">{{ selectedLevelData.name }}</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">范围:</span>
|
||||
<span class="value">{{ selectedLevelData.scope }}</span>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<span class="label">典型场景:</span>
|
||||
<div class="detail-row">
|
||||
<span class="label">场景:</span>
|
||||
<span class="value">{{ selectedLevelData.scenario }}</span>
|
||||
</div>
|
||||
<div class="detail-section permissions-list">
|
||||
<span class="label">拥有权限:</span>
|
||||
<div class="permissions-grid">
|
||||
<span
|
||||
v-for="(perm, i) in selectedLevelData.permissions"
|
||||
:key="i"
|
||||
class="perm-tag"
|
||||
:class="perm.type"
|
||||
>
|
||||
{{ perm.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="perms-list">
|
||||
<span v-for="(perm, i) in selectedLevelData.permissions.slice(0, 3)" :key="i" class="perm-tag">{{ perm.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>最小权限原则——始终授予用户完成工作所需的最小权限。从低权限开始,根据实际需求逐步提升,而不是一开始就授予高权限。
|
||||
<strong>核心思想:</strong>最小权限原则——始终授予用户完成工作所需的最小权限。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,71 +54,42 @@ const selectedLevel = ref(0)
|
||||
const hierarchyLevels = [
|
||||
{
|
||||
icon: '👑',
|
||||
name: '根账号 (Root)',
|
||||
name: '根账号',
|
||||
scope: '全账号最高权限',
|
||||
scenario: '账号所有者,拥有云服务的所有权限',
|
||||
permissions: [
|
||||
{ name: '完全管理权限', type: 'full' },
|
||||
{ name: '账单管理', type: 'billing' },
|
||||
{ name: '组织架构管理', type: 'org' },
|
||||
{ name: '关闭账号', type: 'critical' },
|
||||
{ name: '恢复已删除资源', type: 'admin' }
|
||||
]
|
||||
scenario: '账号所有者,拥有所有权限',
|
||||
permissions: [{ name: '完全管理' }, { name: '账单管理' }, { name: '关闭账号' }]
|
||||
},
|
||||
{
|
||||
icon: '👤',
|
||||
name: 'IAM 管理员',
|
||||
scope: 'IAM 全权限',
|
||||
scenario: '管理所有 IAM 用户、角色、策略',
|
||||
permissions: [
|
||||
{ name: '创建/删除用户', type: 'user' },
|
||||
{ name: '创建/删除角色', type: 'role' },
|
||||
{ name: '管理策略', type: 'policy' },
|
||||
{ name: '查看凭证报告', type: 'audit' }
|
||||
]
|
||||
permissions: [{ name: '创建/删除用户' }, { name: '管理策略' }, { name: '查看凭证' }]
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
name: '普通 IAM 用户',
|
||||
name: '普通用户',
|
||||
scope: '受限权限',
|
||||
scenario: '日常开发人员,只能访问特定资源',
|
||||
permissions: [
|
||||
{ name: '只读访问 EC2', type: 'read' },
|
||||
{ name: '读写指定 S3 桶', type: 'limited' },
|
||||
{ name: '查看 CloudWatch 日志', type: 'read' },
|
||||
{ name: '无法创建 IAM 资源', type: 'deny' }
|
||||
]
|
||||
scenario: '日常开发,只能访问特定资源',
|
||||
permissions: [{ name: '只读 EC2' }, { name: '读写 S3' }, { name: '查看日志' }]
|
||||
},
|
||||
{
|
||||
icon: '🎭',
|
||||
name: '临时角色 (Role)',
|
||||
name: '临时角色',
|
||||
scope: '按策略定义',
|
||||
scenario: '跨账号访问、服务角色、临时授权',
|
||||
permissions: [
|
||||
{ name: '临时凭证 (1-12小时)', type: 'temp' },
|
||||
{ name: '按信任策略授权', type: 'conditional' },
|
||||
{ name: '可跨账号使用', type: 'cross' },
|
||||
{ name: '无长期凭证', type: 'secure' }
|
||||
]
|
||||
scenario: '跨账号访问、临时授权',
|
||||
permissions: [{ name: '临时凭证' }, { name: '跨账号' }, { name: '无长期凭证' }]
|
||||
},
|
||||
{
|
||||
icon: '🔑',
|
||||
name: '服务账号 / 应用',
|
||||
scope: 'API 访问权限',
|
||||
scenario: '应用程序、CI/CD 流水线、自动化脚本',
|
||||
permissions: [
|
||||
{ name: 'AK/SK 或临时凭证', type: 'api' },
|
||||
{ name: '特定服务 API 权限', type: 'service' },
|
||||
{ name: '无控制台访问', type: 'programmatic' },
|
||||
{ name: '建议定期轮换密钥', type: 'security' }
|
||||
]
|
||||
name: '服务账号',
|
||||
scope: 'API 访问',
|
||||
scenario: '应用程序、CI/CD 流水线',
|
||||
permissions: [{ name: 'AK/SK' }, { name: '特定 API' }, { name: '定期轮换' }]
|
||||
}
|
||||
]
|
||||
|
||||
const selectedLevelData = computed(() => {
|
||||
if (selectedLevel.value === null) return null
|
||||
return hierarchyLevels[selectedLevel.value]
|
||||
})
|
||||
const selectedLevelData = computed(() => hierarchyLevels[selectedLevel.value])
|
||||
|
||||
function selectLevel(index) {
|
||||
selectedLevel.value = index
|
||||
@@ -151,248 +100,102 @@ function selectLevel(index) {
|
||||
.permission-hierarchy-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hierarchy-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.levels-list { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
|
||||
.level-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.level-row:hover,
|
||||
.level-row.active {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.level-row:hover { border-color: var(--vp-c-brand); }
|
||||
.level-row.active { border-color: var(--vp-c-brand); background: var(--vp-c-brand-soft); }
|
||||
|
||||
.level-icon {
|
||||
font-size: 1.6rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.level-icon { font-size: 1.25rem; }
|
||||
|
||||
.level-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.level-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.level-scope {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.permission-badges {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.65rem;
|
||||
white-space: nowrap;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.badge.more {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.level-info { display: flex; flex-direction: column; }
|
||||
.level-name { font-weight: 600; font-size: 0.85rem; color: var(--vp-c-text-1); }
|
||||
.level-scope { font-size: 0.7rem; color: var(--vp-c-text-2); }
|
||||
|
||||
.detail-panel {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-panel h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 0.75rem;
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-section .label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 80px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.detail-row .label { color: var(--vp-c-text-2); }
|
||||
.detail-row .value { color: var(--vp-c-text-1); }
|
||||
|
||||
.detail-section .value {
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
}
|
||||
.perms-list { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.5rem; }
|
||||
|
||||
.perm-tag {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.perm-tag.full {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.perm-tag.read,
|
||||
.perm-tag.user,
|
||||
.perm-tag.readonly {
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.perm-tag.limited,
|
||||
.perm-tag.role {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.perm-tag.deny,
|
||||
.perm-tag.critical {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.perm-tag.temp,
|
||||
.perm-tag.conditional,
|
||||
.perm-tag.service {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.perm-tag.admin,
|
||||
.perm-tag.org,
|
||||
.perm-tag.billing {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.15);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.perm-tag.api,
|
||||
.perm-tag.programmatic,
|
||||
.perm-tag.security {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.perm-tag.cross,
|
||||
.perm-tag.secure,
|
||||
.perm-tag.audit,
|
||||
.perm-tag.policy {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.level-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.permission-badges {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
max-width: none;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
@@ -98,10 +98,10 @@ const effectList = computed(() => {
|
||||
<style scoped>
|
||||
.policy-editor-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.editor-layout {
|
||||
|
||||
@@ -3,77 +3,44 @@
|
||||
<div class="demo-header">
|
||||
<span class="icon">🎭</span>
|
||||
<span class="title">角色与策略</span>
|
||||
<span class="subtitle">理解角色如何关联多个策略</span>
|
||||
<span class="subtitle">策略叠加原理</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="visualization-container">
|
||||
<!-- Central Role -->
|
||||
<div class="central-role">
|
||||
<div class="role-core" @click="toggleRoleDetails"
|
||||
:class="{ expanded: showRoleDetails }">
|
||||
<div class="role-icon">🎭</div>
|
||||
<div class="role-info">
|
||||
<span class="role-name">{{ roleName }}</span>
|
||||
<span class="role-type">{{ roleType }}</span>
|
||||
</div>
|
||||
<div class="expand-icon">{{ showRoleDetails ? '▼' : '▶' }}</div>
|
||||
<div class="main-area">
|
||||
<div class="role-section">
|
||||
<div class="role-card" @click="showTrust = !showTrust">
|
||||
<span class="role-icon">🎭</span>
|
||||
<div class="role-info">
|
||||
<span class="role-name">CrossAccountS3AccessRole</span>
|
||||
<span class="role-type">跨账号访问角色</span>
|
||||
</div>
|
||||
|
||||
<!-- Trust Policy -->
|
||||
<div class="trust-policy" v-if="showRoleDetails">
|
||||
<div class="policy-header">
|
||||
<span class="policy-icon">🔐</span>
|
||||
<span class="policy-title">信任策略 (Trust Policy)</span>
|
||||
</div>
|
||||
<div class="policy-content">
|
||||
<div class="policy-item" v-for="(trust, i) in trustPolicy" :key="i">
|
||||
<span class="principal">{{ trust.principal }}</span>
|
||||
<span class="action">可执行: {{ trust.action }}</span>
|
||||
<span class="condition" v-if="trust.condition">条件: {{ trust.condition }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="expand-icon">{{ showTrust ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div class="trust-policy" v-if="showTrust">
|
||||
<div class="trust-title">🔐 信任策略</div>
|
||||
<div class="trust-item" v-for="(t, i) in trustPolicy" :key="i">
|
||||
<span class="principal">{{ t.principal }}</span>
|
||||
<span class="action">{{ t.action }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Lines (SVG) -->
|
||||
<svg class="connection-lines" v-if="mounted">
|
||||
<line
|
||||
v-for="(line, index) in connectionLines"
|
||||
:key="index"
|
||||
:x1="line.x1"
|
||||
:y1="line.y1"
|
||||
:x2="line.x2"
|
||||
:y2="line.y2"
|
||||
:class="['connection-line', line.type, { active: hoveredPolicy === line.policyIndex }]"
|
||||
@mouseenter="hoveredPolicy = line.policyIndex"
|
||||
@mouseleave="hoveredPolicy = null"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Attached Policies -->
|
||||
<div class="attached-policies">
|
||||
<div
|
||||
v-for="(policy, index) in attachedPolicies"
|
||||
:key="index"
|
||||
class="policy-card"
|
||||
:class="{ active: hoveredPolicy === index, selected: selectedPolicy === index }"
|
||||
:style="getPolicyPosition(index)"
|
||||
@mouseenter="hoveredPolicy = index"
|
||||
@mouseleave="hoveredPolicy = null"
|
||||
@click="selectPolicy(index)"
|
||||
>
|
||||
<div class="policy-header">
|
||||
<span class="policy-icon">{{ policy.icon }}</span>
|
||||
<span class="policy-name">{{ policy.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="policy-permissions" v-if="selectedPolicy === index">
|
||||
<div class="permission-item" v-for="(perm, i) in policy.permissions" :key="i">
|
||||
<span class="perm-effect" :class="perm.effect">{{ perm.effect }}</span>
|
||||
<span class="perm-action">{{ perm.action }}</span>
|
||||
<span class="perm-resource">{{ perm.resource }}</span>
|
||||
</div>
|
||||
<div class="policies-section">
|
||||
<div
|
||||
v-for="(policy, index) in attachedPolicies"
|
||||
:key="index"
|
||||
class="policy-card"
|
||||
:class="{ selected: selectedPolicy === index }"
|
||||
@click="selectedPolicy = index"
|
||||
>
|
||||
<div class="policy-header">
|
||||
<span class="policy-icon">{{ policy.icon }}</span>
|
||||
<span class="policy-name">{{ policy.name }}</span>
|
||||
</div>
|
||||
<div class="policy-perms" v-if="selectedPolicy === index">
|
||||
<div class="perm" v-for="(p, i) in policy.permissions" :key="i">
|
||||
<span class="effect" :class="p.effect.toLowerCase()">{{ p.effect }}</span>
|
||||
<span class="action">{{ p.action }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,418 +49,173 @@
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>策略叠加——一个角色可以附加多个策略,最终的权限是所有策略的叠加结果。Deny 策略优先级高于 Allow。
|
||||
<strong>核心思想:</strong>策略叠加——一个角色可附加多个策略,最终权限是所有策略的叠加结果。Deny 优先级高于 Allow。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Role Data
|
||||
const roleName = ref('CrossAccountS3AccessRole')
|
||||
const roleType = ref('跨账号访问角色')
|
||||
const showRoleDetails = ref(false)
|
||||
const showTrust = ref(false)
|
||||
const selectedPolicy = ref(0)
|
||||
|
||||
const trustPolicy = ref([
|
||||
{ principal: '账号 A (123456789012)', action: 'sts:AssumeRole', condition: 'ExternalId 匹配' },
|
||||
{ principal: '特定 IAM 用户', action: 'sts:AssumeRole', condition: 'IP 白名单' }
|
||||
])
|
||||
const trustPolicy = [
|
||||
{ principal: '账号 A (123456789012)', action: 'sts:AssumeRole' },
|
||||
{ principal: '特定 IAM 用户', action: 'sts:AssumeRole' }
|
||||
]
|
||||
|
||||
// Policies Data
|
||||
const attachedPolicies = ref([
|
||||
const attachedPolicies = [
|
||||
{
|
||||
name: 'S3ReadWritePolicy',
|
||||
icon: '📦',
|
||||
permissions: [
|
||||
{ effect: 'Allow', action: 's3:GetObject', resource: 'arn:aws:s3:::bucket/*' },
|
||||
{ effect: 'Allow', action: 's3:PutObject', resource: 'arn:aws:s3:::bucket/*' },
|
||||
{ effect: 'Allow', action: 's3:ListBucket', resource: 'arn:aws:s3:::bucket' }
|
||||
{ effect: 'Allow', action: 's3:GetObject' },
|
||||
{ effect: 'Allow', action: 's3:PutObject' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'CloudWatchLogsPolicy',
|
||||
icon: '📊',
|
||||
permissions: [
|
||||
{ effect: 'Allow', action: 'logs:CreateLogGroup', resource: '*' },
|
||||
{ effect: 'Allow', action: 'logs:CreateLogStream', resource: '*' },
|
||||
{ effect: 'Allow', action: 'logs:PutLogEvents', resource: '*' }
|
||||
{ effect: 'Allow', action: 'logs:CreateLogGroup' },
|
||||
{ effect: 'Allow', action: 'logs:PutLogEvents' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'DenySensitiveData',
|
||||
icon: '🚫',
|
||||
permissions: [
|
||||
{ effect: 'Deny', action: 's3:GetObject', resource: 'arn:aws:s3:::bucket/sensitive/*' },
|
||||
{ effect: 'Deny', action: 's3:DeleteObject', resource: 'arn:aws:s3:::bucket/*' }
|
||||
{ effect: 'Deny', action: 's3:GetObject (sensitive/*)' },
|
||||
{ effect: 'Deny', action: 's3:DeleteObject' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// State
|
||||
const hoveredPolicy = ref(null)
|
||||
const selectedPolicy = ref(0)
|
||||
const mounted = ref(false)
|
||||
const connectionLines = ref([])
|
||||
|
||||
// Methods
|
||||
function toggleRoleDetails() {
|
||||
showRoleDetails.value = !showRoleDetails.value
|
||||
}
|
||||
|
||||
function selectPolicy(index) {
|
||||
selectedPolicy.value = index
|
||||
}
|
||||
|
||||
function getPolicyPosition(index) {
|
||||
const positions = [
|
||||
{ top: '0%', right: '0%' },
|
||||
{ top: '35%', right: '5%' },
|
||||
{ top: '70%', right: '0%' }
|
||||
]
|
||||
return positions[index] || positions[0]
|
||||
}
|
||||
|
||||
function calculateConnections() {
|
||||
connectionLines.value = attachedPolicies.value.map((_, index) => ({
|
||||
x1: 50,
|
||||
y1: 50,
|
||||
x2: 80 + (index * 5),
|
||||
y2: 20 + (index * 30),
|
||||
type: index === 2 ? 'deny' : 'allow',
|
||||
policyIndex: index
|
||||
}))
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
mounted.value = true
|
||||
calculateConnections()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
mounted.value = false
|
||||
})
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-policy-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 800;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.visualization-container {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* Central Role */
|
||||
.central-role {
|
||||
position: absolute;
|
||||
left: 5%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 280px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.role-core {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.role-core:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.role-core.expanded {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
display: block;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.role-type {
|
||||
display: block;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Trust Policy */
|
||||
.trust-policy {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.policy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.policy-icon {
|
||||
font-size: 1.2rem;
|
||||
.demo-header .icon { font-size: 1.25rem; }
|
||||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.policy-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85rem;
|
||||
@media (max-width: 640px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.policy-content {
|
||||
.role-section { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
|
||||
.role-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.policy-item {
|
||||
background: var(--vp-c-bg-alt);
|
||||
.role-card:hover { border-color: var(--vp-c-brand); }
|
||||
|
||||
.role-icon { font-size: 1.5rem; }
|
||||
.role-info { flex: 1; }
|
||||
.role-name { display: block; font-weight: 600; font-size: 0.85rem; color: var(--vp-c-text-1); }
|
||||
.role-type { display: block; font-size: 0.7rem; color: var(--vp-c-text-2); }
|
||||
.expand-icon { font-size: 0.7rem; color: var(--vp-c-text-3); }
|
||||
|
||||
.trust-policy {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.principal {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-1);
|
||||
.trust-title { font-size: 0.75rem; font-weight: 600; margin-bottom: 0.4rem; color: var(--vp-c-text-1); }
|
||||
|
||||
.trust-item {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
padding: 0.3rem 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.trust-item .principal { font-weight: 600; color: var(--vp-c-brand-1); display: block; }
|
||||
.trust-item .action { color: var(--vp-c-text-2); }
|
||||
|
||||
.condition {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* Connection Lines SVG */
|
||||
.connection-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
stroke: var(--vp-c-divider);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.connection-line.allow {
|
||||
stroke: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.connection-line.deny {
|
||||
stroke: var(--vp-c-brand-delta);
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
|
||||
.connection-line:hover,
|
||||
.connection-line.active {
|
||||
stroke-width: 4;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Attached Policies */
|
||||
.attached-policies {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 240px;
|
||||
}
|
||||
.policies-section { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
|
||||
.policy-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.policy-card:hover,
|
||||
.policy-card.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateX(-4px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.policy-card:hover { border-color: var(--vp-c-brand); }
|
||||
.policy-card.selected { border-color: var(--vp-c-brand); background: var(--vp-c-bg-alt); }
|
||||
|
||||
.policy-card.selected {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
.policy-header { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.policy-icon { font-size: 1rem; }
|
||||
.policy-name { font-weight: 600; font-size: 0.8rem; color: var(--vp-c-text-1); }
|
||||
|
||||
.policy-card .policy-header {
|
||||
.policy-perms { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px solid var(--vp-c-divider); }
|
||||
|
||||
.perm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.policy-card .policy-icon {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.policy-card .policy-name {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.policy-permissions {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.permission-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem;
|
||||
margin-bottom: 0.25rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
gap: 0.3rem;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.perm-effect {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
.effect {
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.perm-effect.Allow {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.perm-effect.Deny {
|
||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||
color: var(--vp-c-brand-delta);
|
||||
}
|
||||
|
||||
.perm-action {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.perm-resource {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.6rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.effect.allow { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
||||
.effect.deny { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
|
||||
|
||||
.perm .action { font-family: var(--vp-font-family-mono); color: var(--vp-c-text-2); }
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.visualization-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.central-role,
|
||||
.attached-policies {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.connection-lines {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.info-box .icon { flex-shrink: 0; }
|
||||
.info-box strong { color: var(--vp-c-text-1); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user