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

321 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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="grid">
<div class="card">
<div class="card-title">你的场景</div>
<label class="label">主要客户端</label>
<div class="row">
<button
class="chip"
:class="{ active: client === 'web' }"
@click="client = 'web'"
>
浏览器 Web
</button>
<button
class="chip"
:class="{ active: client === 'mobile' }"
@click="client = 'mobile'"
>
移动端 App
</button>
<button
class="chip"
:class="{ active: client === 'server' }"
@click="client = 'server'"
>
服务到服务
</button>
</div>
<label class="label">是否强需求立刻注销/踢下线</label>
<div class="row">
<button
class="chip"
:class="{ active: revoke === 'yes' }"
@click="revoke = 'yes'"
>
</button>
<button
class="chip"
:class="{ active: revoke === 'no' }"
@click="revoke = 'no'"
>
</button>
</div>
<label class="label">是否需要跨域前后端分离多域名</label>
<div class="row">
<button
class="chip"
:class="{ active: cors === 'yes' }"
@click="cors = 'yes'"
>
</button>
<button
class="chip"
:class="{ active: cors === 'no' }"
@click="cors = 'no'"
>
</button>
</div>
<label class="label">服务是否会水平扩容多实例</label>
<div class="row">
<button
class="chip"
:class="{ active: scale === 'yes' }"
@click="scale = 'yes'"
>
</button>
<button
class="chip"
:class="{ active: scale === 'no' }"
@click="scale = 'no'"
>
</button>
</div>
</div>
<div class="card">
<div class="card-title">推荐</div>
<div class="recommend">
<div class="pill primary">{{ recommendation.title }}</div>
<div class="desc">{{ recommendation.desc }}</div>
</div>
<div class="box">
<div class="box-title">为什么</div>
<ul class="list">
<li v-for="(x, i) in recommendation.reasons" :key="i">{{ x }}</li>
</ul>
</div>
<div class="box">
<div class="box-title">落地建议</div>
<ul class="list">
<li v-for="(x, i) in recommendation.tips" :key="i">{{ x }}</li>
</ul>
</div>
</div>
</div>
<div class="card">
<div class="card-title">常见误区</div>
<ul class="list">
<li>
<strong>JWT 更安全</strong>JWT
只是无状态安全取决于密钥过期策略存储方式授权设计
</li>
<li>
<strong>Cookie 一定 CSRF</strong>SameSite + CSRF token
可以显著降低风险
</li>
<li>
<strong>别把第三方 OAuth token 当你系统 token</strong>用途不同
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const client = ref('web') // web | mobile | server
const revoke = ref('yes') // yes | no
const cors = ref('no') // yes | no
const scale = ref('yes') // yes | no
const recommendation = computed(() => {
// Very simple heuristic: prefer session for same-site web + revoke requirement.
const reasons = []
const tips = []
const isWeb = client.value === 'web'
const needsRevoke = revoke.value === 'yes'
const needsCors = cors.value === 'yes'
const needsScale = scale.value === 'yes'
if (isWeb && !needsCors && needsRevoke) {
reasons.push('同域 Web + 需要“立刻注销/踢下线” → Session 更直观可控。')
if (needsScale) reasons.push('多实例时用 Redis 等共享 Session 存储即可。')
tips.push('Cookie: HttpOnly + Secure + SameSite=Lax/Strict(视业务)')
tips.push('CSRFSameSite + CSRF Token(双重保险)')
tips.push('Session StoreRedis + TTL + 续期策略(滑动过期)')
return {
title: 'Session + Cookie',
desc: '传统 Web 的最稳妥方案',
reasons,
tips
}
}
// Otherwise default to token approach.
reasons.push('跨域/移动端/多服务场景更偏向 TokenAuthorization Header)。')
if (needsRevoke)
reasons.push(
'需要主动注销:用短 access token + refresh token + 黑名单/版本号。'
)
if (!needsRevoke) reasons.push('不强求“立刻注销”时,JWT 的无状态优势更明显。')
tips.push('Access Token:短过期(如 15m),Refresh Token:单独存/可轮换')
tips.push(
'存储:Web 尽量避免 localStorage;更推荐 HttpOnly Cookie 或内存 + 刷新机制(看业务)'
)
tips.push('授权:服务端做 RBAC/ABAC;不要把 role 全塞 JWT 然后永不变更')
return {
title: 'JWT Access Token(配合 Refresh',
desc: '现代 API/移动端常用组合',
reasons,
tips
}
})
</script>
<style scoped>
.session-vs-jwt-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 800;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.card-title {
font-weight: 800;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.label {
display: block;
font-weight: 800;
color: var(--vp-c-text-1);
font-size: 0.875rem;
margin: 0.75rem 0 0.35rem;
}
.row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.chip {
padding: 0.4rem 0.65rem;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
cursor: pointer;
font-weight: 700;
font-size: 0.875rem;
}
.chip.active {
border-color: rgba(var(--vp-c-brand-rgb), 0.35);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
}
.recommend {
margin-bottom: 0.75rem;
}
.pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.25rem 0.75rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
font-weight: 800;
margin-bottom: 0.5rem;
}
.pill.primary {
border-color: rgba(var(--vp-c-brand-rgb), 0.35);
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-text-1);
}
.desc {
color: var(--vp-c-text-2);
line-height: 1.75;
}
.box {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 0.75rem;
margin-top: 0.75rem;
}
.box-title {
font-weight: 800;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.list {
margin: 0;
padding-left: 1.1rem;
color: var(--vp-c-text-2);
line-height: 1.75;
}
@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>