Files
test-repo/docs/.vitepress/theme/components/appendix/auth-design/SessionVsJWTDemo.vue
T

891 lines
20 KiB
Vue
Raw Normal View History

<!--
SessionVsJWTDemo.vue
Session vs JWT 对比演示
-->
<template>
<div class="session-vs-jwt-demo">
<div class="header">
<div class="title">Session vs JWT全方位对比</div>
<div class="subtitle">两种主流鉴权方案的优劣势分析</div>
</div>
<div class="scenario-selector">
<div class="selector-title">选择场景</div>
<div class="scenario-buttons">
<button
v-for="scenario in scenarios"
:key="scenario.key"
class="scenario-btn"
:class="{ active: selectedScenario === scenario.key }"
@click="selectScenario(scenario.key)"
>
<span class="scenario-icon">{{ scenario.icon }}</span>
<span class="scenario-label">{{ scenario.label }}</span>
</button>
</div>
</div>
<div class="comparison-table" v-if="selectedScenario">
<table>
<thead>
<tr>
<th>对比维度</th>
<th class="session-col">Session + Cookie</th>
<th class="jwt-col">JWT</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in getCurrentScenarioRows()"
:key="index"
:class="{ highlight: row.highlight }"
>
<td class="dimension">{{ row.dimension }}</td>
<td class="session-cell">
<div class="cell-content">
<span class="cell-icon">{{ row.session.icon }}</span>
<span class="cell-text">{{ row.session.text }}</span>
<span class="cell-score" :class="row.session.scoreClass">
{{ row.session.score }}
</span>
</div>
</td>
<td class="jwt-cell">
<div class="cell-content">
<span class="cell-icon">{{ row.jwt.icon }}</span>
<span class="cell-text">{{ row.jwt.text }}</span>
<span class="cell-score" :class="row.jwt.scoreClass">
{{ row.jwt.score }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation" v-if="selectedScenario">
<div class="rec-header">
<div class="rec-icon">{{ currentRecommendation.icon }}</div>
<div class="rec-title">{{ currentRecommendation.title }}</div>
</div>
<div class="rec-content">
<div class="rec-winner">
推荐方案{{ currentRecommendation.winner }}
</div>
<div class="rec-reason">{{ currentRecommendation.reason }}</div>
</div>
</div>
<div class="quick-guide">
<div class="guide-title">快速选择指南</div>
<div class="guide-cards">
<div class="guide-card session-card">
<div class="card-header">
<div class="card-icon">🍪</div>
<div class="card-title">选择 Session + Cookie</div>
</div>
<ul class="card-list">
<li> 传统 Web 应用服务器端渲染</li>
<li> 需要服务端主动控制用户会话</li>
<li> 单体应用不需要跨域</li>
<li> 对安全性要求极高的场景</li>
</ul>
</div>
<div class="guide-card jwt-card">
<div class="card-header">
<div class="card-icon">🎫</div>
<div class="card-title">选择 JWT</div>
</div>
<ul class="card-list">
<li> 前后端分离的 SPA 应用</li>
<li> 移动端 AppiOS/Android</li>
<li> 微服务架构</li>
<li> 需要跨域访问的 API</li>
</ul>
</div>
</div>
</div>
<div class="architecture-comparison">
<div class="arch-title">架构对比</div>
<div class="arch-diagrams">
<div class="arch-item session-arch">
<div class="arch-label">Session 架构</div>
<div class="arch-content">
<div class="arch-step">
<span class="step-icon">1</span>
<span class="step-text">用户登录</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">2</span>
<span class="step-text">服务器创建 Session</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step server-storage">
<span class="step-icon">💾</span>
<span class="step-text">存储到 Redis/Memory</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">3</span>
<span class="step-text">返回 Cookie</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">4</span>
<span class="step-text">每次请求自动带上</span>
</div>
</div>
<div class="arch-note">有状态需要存储 Session</div>
</div>
<div class="arch-item jwt-arch">
<div class="arch-label">JWT 架构</div>
<div class="arch-content">
<div class="arch-step">
<span class="step-icon">1</span>
<span class="step-text">用户登录</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">2</span>
<span class="step-text">服务器生成 JWT</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step client-storage">
<span class="step-icon">💾</span>
<span class="step-text">客户端存储 Token</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">3</span>
<span class="step-text">返回 Token</span>
</div>
<div class="arch-arrow"></div>
<div class="arch-step">
<span class="step-icon">4</span>
<span class="step-text">每次请求在 Header 携带</span>
</div>
</div>
<div class="arch-note">无状态不存储用户会话</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedScenario = ref('spa')
const scenarios = [
{ key: 'spa', label: '前后端分离', icon: '🌐' },
{ key: 'mobile', label: '移动端', icon: '📱' },
{ key: 'microservice', label: '微服务', icon: '🔧' },
{ key: 'traditional', label: '传统 Web', icon: '🖥️' },
{ key: 'security', label: '高安全需求', icon: '🔒' }
]
const scenarioData = {
spa: {
rows: [
{
dimension: '跨域支持',
session: {
icon: '❌',
text: 'Cookie 无法跨域',
score: '⭐⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: 'Header 携带,完美支持',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '实现复杂度',
session: {
icon: '⚠️',
text: '需要处理 CORS 和 Cookie',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
jwt: {
icon: '✅',
text: '简单,localStorage + Header',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: false
},
{
dimension: '扩展性',
session: {
icon: '⚠️',
text: '有状态,扩展困难',
score: '⭐⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: '无状态,易于扩展',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '安全性',
session: {
icon: '⚠️',
text: '易受 CSRF 攻击',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
jwt: {
icon: '✅',
text: '天然防 CSRF',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: false
}
],
recommendation: {
icon: '🏆',
title: '推荐方案',
winner: 'JWT',
reason:
'前后端分离架构下,JWT 的无状态特性和跨域优势使其成为最佳选择。避免 Cookie 的跨域问题,且更易于扩展。'
}
},
mobile: {
rows: [
{
dimension: '移动端支持',
session: {
icon: '⚠️',
text: 'Cookie 管理复杂',
score: '⭐⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: '原生支持,存储简单',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '网络开销',
session: {
icon: '✅',
text: '只传 session_id',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '⚠️',
text: 'Token 较大',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
highlight: false
},
{
dimension: '离线能力',
session: {
icon: '❌',
text: '必须联网验证',
score: '⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: '可离线解析基本信息',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
}
],
recommendation: {
icon: '🏆',
title: '推荐方案',
winner: 'JWT',
reason:
'移动端原生应用没有浏览器的 Cookie 机制,JWT 更适合。可以轻松存储在 UserDefaults/SharedPreferences 中。'
}
},
microservice: {
rows: [
{
dimension: '服务间通信',
session: {
icon: '⚠️',
text: '需要共享 Session 存储',
score: '⭐⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: '直接传递 Token',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '水平扩展',
session: {
icon: '⚠️',
text: '需要 Redis 共享',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
jwt: {
icon: '✅',
text: '无状态,任意服务器可验证',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '性能',
session: {
icon: '⚠️',
text: '每次请求查 Redis',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
jwt: {
icon: '✅',
text: '直接验证签名',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: false
}
],
recommendation: {
icon: '🏆',
title: '推荐方案',
winner: 'JWT',
reason:
'微服务架构下,JWT 的无状态特性是巨大优势。不需要在服务间共享 Session,任何服务都可以独立验证 Token。'
}
},
traditional: {
rows: [
{
dimension: '实现难度',
session: {
icon: '✅',
text: '框架内置支持',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '⚠️',
text: '需要额外集成',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
highlight: true
},
{
dimension: '会话管理',
session: {
icon: '✅',
text: '可主动注销',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '⚠️',
text: '无法主动注销',
score: '⭐⭐',
scoreClass: 'low'
},
highlight: true
},
{
dimension: '安全性',
session: {
icon: '✅',
text: 'HttpOnly Cookie',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '⚠️',
text: 'XSS 风险',
score: '⭐⭐⭐',
scoreClass: 'medium'
},
highlight: false
}
],
recommendation: {
icon: '🏆',
title: '推荐方案',
winner: 'Session + Cookie',
reason:
'传统 Web 应用(如 PHP、Java Web)通常有成熟的 Session 机制,实现简单且安全。主动注销功能很重要。'
}
},
security: {
rows: [
{
dimension: '防篡改',
session: {
icon: '✅',
text: '服务端存储,无法篡改',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '✅',
text: '签名防篡改',
score: '⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: false
},
{
dimension: '防 CSRF',
session: {
icon: '⚠️',
text: '易受攻击',
score: '⭐⭐',
scoreClass: 'low'
},
jwt: {
icon: '✅',
text: '天然免疫',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
highlight: true
},
{
dimension: '防 XSS',
session: {
icon: '✅',
text: 'HttpOnly Cookie',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '⚠️',
text: 'localStorage 可被读取',
score: '⭐⭐',
scoreClass: 'low'
},
highlight: true
},
{
dimension: '主动注销',
session: {
icon: '✅',
text: '立即删除 Session',
score: '⭐⭐⭐⭐⭐',
scoreClass: 'high'
},
jwt: {
icon: '❌',
text: '需黑名单机制',
score: '⭐⭐',
scoreClass: 'low'
},
highlight: true
}
],
recommendation: {
icon: '⚖️',
title: '推荐方案',
winner: '视情况而定',
reason:
'高安全场景下,Session + CookieHttpOnly)通常更安全。但如果 CSRF 是主要威胁,JWT 可能更好。建议结合实际威胁模型选择。'
}
}
}
const getCurrentScenarioRows = () => {
return scenarioData[selectedScenario.value]?.rows || []
}
const currentRecommendation = computed(() => {
return (
scenarioData[selectedScenario.value]?.recommendation || {
icon: '❓',
title: '推荐方案',
winner: '未知',
reason: '请选择一个场景'
}
)
})
const selectScenario = (key) => {
selectedScenario.value = key
}
</script>
<style scoped>
.session-vs-jwt-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.3rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.scenario-selector {
margin-bottom: 1.5rem;
}
.selector-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.scenario-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.scenario-btn {
flex: 1;
min-width: 100px;
padding: 0.75rem 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.scenario-btn:hover {
border-color: var(--vp-c-brand);
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.scenario-icon {
font-size: 1.2rem;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
padding: 0.75rem;
text-align: left;
font-weight: 700;
font-size: 0.9rem;
border-bottom: 2px solid var(--vp-c-divider);
}
thead th.session-col {
color: #f59e0b;
}
thead th.jwt-col {
color: #8b5cf6;
}
tbody tr {
border-bottom: 1px solid var(--vp-c-divider);
}
tbody tr:last-child {
border-bottom: none;
}
tbody tr.highlight {
background: rgba(59, 130, 246, 0.05);
}
.dimension {
padding: 0.75rem;
font-weight: 600;
font-size: 0.85rem;
}
.cell-content {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
}
.cell-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.cell-text {
flex: 1;
font-size: 0.85rem;
}
.cell-score {
font-size: 0.85rem;
font-weight: 700;
}
.cell-score.high {
color: #22c55e;
}
.cell-score.medium {
color: #f59e0b;
}
.cell-score.low {
color: #ef4444;
}
.recommendation {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 2px solid var(--vp-c-brand);
margin-bottom: 1.5rem;
}
.rec-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.rec-icon {
font-size: 2rem;
}
.rec-title {
font-size: 1.1rem;
font-weight: 700;
}
.rec-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rec-winner {
font-size: 1rem;
font-weight: 700;
color: var(--vp-c-brand);
}
.rec-reason {
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.quick-guide {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
}
.guide-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.guide-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.guide-card {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.card-icon {
font-size: 1.5rem;
}
.card-title {
font-weight: 700;
font-size: 0.95rem;
}
.card-list {
margin: 0;
padding-left: 1.25rem;
}
.card-list li {
margin-bottom: 0.5rem;
font-size: 0.85rem;
line-height: 1.5;
}
.architecture-comparison {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.arch-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.arch-diagrams {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.arch-item {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 1.25rem;
border: 1px solid var(--vp-c-divider);
}
.arch-label {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-brand);
}
.arch-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.arch-step {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: white;
border-radius: 6px;
font-size: 0.85rem;
border: 1px solid var(--vp-c-divider);
}
.step-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.step-text {
flex: 1;
}
.arch-arrow {
text-align: center;
font-size: 1.2rem;
color: var(--vp-c-text-2);
font-weight: 700;
}
.server-storage,
.client-storage {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.arch-note {
margin-top: 1rem;
text-align: center;
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
padding: 0.5rem;
background: white;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
@media (max-width: 768px) {
.scenario-buttons {
flex-direction: column;
}
.guide-cards {
grid-template-columns: 1fr;
}
.arch-diagrams {
grid-template-columns: 1fr;
}
}
</style>