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:
sanbuphy
2026-02-14 20:23:34 +08:00
parent 81e4284b87
commit d35211071a
373 changed files with 3441 additions and 5629 deletions
@@ -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>