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

863 lines
19 KiB
Vue
Raw Normal View History

<!--
AuthInteractiveLoginDemo.vue
交互式登录流程演示
用途
通过模拟真实的登录流程让用户直观理解认证和授权的概念
互动功能
- 模拟登录输入用户名密码看到完整的认证流程
- 可视化数据流HTTP 请求和响应的实时展示
- 两种模式对比Session vs JWT
-->
<template>
<div class="auth-interactive-login">
<div class="header">
<div class="title">🔐 认证流程演示</div>
<div class="subtitle">模拟登录过程理解认证与授权的区别</div>
</div>
<!-- 模式切换 -->
<div class="mode-selector">
<div class="mode-label">选择鉴权方式</div>
<div class="mode-buttons">
<button
class="mode-btn"
:class="{ active: mode === 'session' }"
@click="switchMode('session')"
>
🍪 Session + Cookie
</button>
<button
class="mode-btn"
:class="{ active: mode === 'jwt' }"
@click="switchMode('jwt')"
>
🎫 JWT Token
</button>
</div>
</div>
<div class="main-content">
<!-- 登录表单 -->
<div class="login-section">
<div class="form-container">
<div class="form-title">登录表单</div>
<div class="form-fields">
<div class="field-group">
<label>用户名</label>
<input
type="text"
v-model="username"
placeholder="输入用户名"
:disabled="locked"
/>
</div>
<div class="field-group">
<label>密码</label>
<input
type="password"
v-model="password"
placeholder="输入密码"
:disabled="locked"
@keyup.enter="startDemo"
/>
</div>
<button
class="login-btn"
@click="startDemo"
:disabled="!username || !password || locked"
>
开始演示
</button>
</div>
<div class="hints">
<div class="hint-title">💡 提示</div>
<div class="hint-text">
试试用户名 <code>admin</code>密码 <code>123456</code>
</div>
</div>
<div class="stepper" v-if="flowStep > 0">
<div class="stepper-title">
当前步骤{{ flowStep }} / {{ maxStep }}
<span class="stepper-hint"
>手动推进避免自动下一步误解</span
>
</div>
<div class="stepper-actions">
<button
class="step-btn"
@click="prevStep"
:disabled="flowStep <= 1"
>
上一步
</button>
<button
class="step-btn primary"
@click="nextStep"
:disabled="flowStep >= maxStep"
>
下一步
</button>
<button class="step-btn" @click="resetDemo">重置</button>
</div>
</div>
</div>
<!-- 实时数据流 -->
<div class="data-flow">
<div class="flow-title">📊 数据流可视化</div>
<!-- 请求阶段 -->
<div class="flow-stage request-stage" v-if="currentStage >= 1">
<div class="stage-header">
<span class="stage-badge">{{
currentStage === 1 ? '📤' : '✅'
}}</span>
<span class="stage-name">1. 客户端发送登录请求</span>
</div>
<div class="request-content" v-if="currentStage >= 1">
<div class="request-line">
<span class="method">POST</span>
<span class="path">/api/login</span>
</div>
<div class="request-body">
<div class="body-title">Body:</div>
<pre>
{
"username": "{{ username }}",
"password": "******"
}</pre
>
</div>
</div>
</div>
<div class="flow-arrow" v-if="currentStage >= 1"></div>
<!-- 服务器验证阶段 -->
<div class="flow-stage server-stage" v-if="currentStage >= 2">
<div class="stage-header">
<span class="stage-badge">{{
currentStage === 2 ? '⚙️' : '✅'
}}</span>
<span class="stage-name">2. 服务器验证身份</span>
</div>
<div class="server-content" v-if="currentStage >= 2">
<div class="verification-steps">
<div
class="verify-step"
:class="{ success: verificationStep >= 1 }"
>
<span class="step-icon">{{
verificationStep >= 1 ? '✅' : '⏳'
}}</span>
<span class="step-text">查询用户数据库</span>
</div>
<div
class="verify-step"
:class="{ success: verificationStep >= 2 }"
>
<span class="step-icon">{{
verificationStep >= 2 ? '✅' : '⏳'
}}</span>
<span class="step-text">验证密码哈希</span>
</div>
<div
class="verify-step"
:class="{ success: verificationStep >= 3 }"
>
<span class="step-icon">{{
verificationStep >= 3 ? '✅' : '⏳'
}}</span>
<span class="step-text"
>生成{{
mode === 'session' ? 'Session' : 'JWT Token'
}}</span
>
</div>
</div>
</div>
</div>
<div class="flow-arrow" v-if="currentStage >= 2"></div>
<!-- 响应阶段 -->
<div class="flow-stage response-stage" v-if="currentStage >= 3">
<div class="stage-header">
<span class="stage-badge">{{
currentStage === 3 ? '📥' : '✅'
}}</span>
<span class="stage-name">3. 服务器返回认证结果</span>
</div>
<div class="response-content" v-if="authResult">
<div class="response-status success"> 登录成功</div>
<div class="response-body">
<div class="body-title">Response:</div>
<pre v-if="mode === 'session'">
{
"status": "success",
"user": {
"id": 123,
"username": "{{ username }}"
}
}</pre
>
<pre v-else>
{
"status": "success",
"token": "{{ authResult?.token }}",
"user": {
"id": 123,
"username": "{{ username }}"
}
}</pre
>
</div>
<div class="auth-mechanism" v-if="currentStage >= 4">
<div class="mechanism-title">
{{ mode === 'session' ? '🍪 Cookie 设置' : '🎫 Token 存储' }}
</div>
<div class="mechanism-content">
<div v-if="mode === 'session'">
<code
>Set-Cookie: session_id={{ authResult?.sessionId }};
HttpOnly; Secure</code
>
</div>
<div v-else>
<code
>localStorage.setItem("token", "{{
authResult?.token
}}")</code
>
</div>
</div>
</div>
</div>
</div>
<!-- 后续请求 -->
<div class="flow-stage request-stage" v-if="currentStage >= 5">
<div class="stage-header">
<span class="stage-badge">🔄</span>
<span class="stage-name">4. 后续请求自动携带认证信息</span>
</div>
<div class="subsequent-request">
<div class="request-line">
<span class="method">GET</span>
<span class="path">/api/user/profile</span>
</div>
<div class="auth-header">
<div class="header-title">Header:</div>
<div v-if="mode === 'session'">
<code>Cookie: session_id={{ authResult?.sessionId }}</code>
</div>
<div v-else>
<code>Authorization: Bearer {{ authResult?.token }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 状态说明 -->
<div class="state-description" v-if="currentStage >= 4">
<div class="description-title">
📖 {{ mode === 'session' ? 'Session' : 'JWT' }} 工作原理
</div>
<div class="description-content">
<p v-if="mode === 'session'">
<strong>Session 模式</strong>服务器在内存或 Redis 中创建一个
Session存储用户信息 服务器返回一个
<code>session_id</code> 给客户端客户端后续请求会自动在 Cookie
中携带这个 ID 服务器根据 ID 查找对应的 Session从而识别用户身份
</p>
<p v-else>
<strong>JWT 模式</strong>服务器将用户信息编码成 JWT
Token直接返回给客户端 客户端将 Token 存储在
localStorage后续请求在 <code>Authorization</code> Header 中携带
服务器验证 Token 的签名即可识别用户无需存储状态
</p>
</div>
</div>
<!-- 重置按钮 -->
<div class="reset-section" v-if="currentStage >= 5">
<button class="reset-btn" @click="resetDemo">🔄 重新演示</button>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const mode = ref('session') // 'session' or 'jwt'
const username = ref('')
const password = ref('')
const flowStep = ref(0) // 0 = not started, 1..maxStep = manual steps
const authResult = ref(null)
const locked = ref(false)
const maxStep = 7
const currentStage = computed(() => {
if (flowStep.value === 0) return 0
if (flowStep.value === 1) return 1
if (flowStep.value >= 2 && flowStep.value <= 4) return 2
if (flowStep.value === 5) return 3
if (flowStep.value === 6) return 4
return 5
})
const verificationStep = computed(() => {
if (flowStep.value < 2) return 0
if (flowStep.value === 2) return 1
if (flowStep.value === 3) return 2
return 3
})
const switchMode = (newMode) => {
mode.value = newMode
resetDemo()
}
const buildAuthResult = () => {
if (mode.value === 'session') {
return {
sessionId: 'sess_' + Math.random().toString(36).substring(2, 15)
}
}
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
const payload = btoa(
JSON.stringify({
user_id: 123,
username: username.value,
exp: Math.floor(Date.now() / 1000) + 3600
})
)
const signature = btoa(`${header}.${payload}.secret`)
return {
token: `${header}.${payload}.${signature}`.substring(0, 50) + '...'
}
}
const startDemo = () => {
if (!username.value || !password.value) return
// Start at step 1 (request). Other steps are manual via Next.
authResult.value = buildAuthResult()
flowStep.value = 1
locked.value = true
}
const nextStep = () => {
flowStep.value = Math.min(maxStep, flowStep.value + 1)
}
const prevStep = () => {
flowStep.value = Math.max(1, flowStep.value - 1)
}
const resetDemo = () => {
username.value = ''
password.value = ''
flowStep.value = 0
authResult.value = null
locked.value = false
}
</script>
<style scoped>
.auth-interactive-login {
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: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.3rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
/* 模式切换 */
.mode-selector {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.mode-label {
font-weight: 600;
font-size: 0.95rem;
}
.mode-buttons {
display: flex;
gap: 0.5rem;
}
.mode-btn {
padding: 0.6rem 1.2rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s ease;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
}
.mode-btn.active {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
border-color: var(--vp-c-brand);
}
/* 主内容 */
.main-content {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
/* 登录表单 */
.login-section {
display: contents;
}
.form-container {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.stepper {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.stepper-title {
font-weight: 700;
color: var(--vp-c-text-1);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.stepper-hint {
margin-left: 0.5rem;
font-weight: 500;
color: var(--vp-c-text-2);
}
.stepper-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.step-btn {
padding: 0.5rem 0.8rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
}
.step-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.step-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.form-fields {
display: flex;
flex-direction: column;
gap: 1rem;
}
.field-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.field-group label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.field-group input {
padding: 0.6rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
transition: border-color 0.2s;
}
.field-group input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.field-group input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn {
padding: 0.75rem;
border: none;
border-radius: 6px;
background: var(--vp-c-brand);
color: white;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
}
.login-btn:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.hints {
margin-top: 1rem;
padding: 0.75rem;
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid var(--vp-c-brand);
border-radius: 6px;
}
.hint-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.3rem;
color: var(--vp-c-brand);
}
.hint-text {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.hint-text code {
background: var(--vp-c-bg);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
}
/* 数据流 */
.data-flow {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.flow-stage {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
animation: slideIn 0.4s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stage-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.stage-badge {
font-size: 1.2rem;
}
.stage-name {
font-weight: 600;
font-size: 0.95rem;
}
.request-line {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.method {
font-weight: 700;
color: #16a34a;
font-family: 'Courier New', monospace;
}
.path {
font-family: 'Courier New', monospace;
color: var(--vp-c-text-1);
}
.request-body,
.response-body {
background: #1e293b;
border-radius: 6px;
padding: 0.75rem;
margin-top: 0.5rem;
}
.body-title,
.header-title {
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 0.4rem;
}
.request-body pre,
.response-body pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.75rem;
color: #e2e8f0;
line-height: 1.5;
}
/* 验证步骤 */
.verification-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.verify-step {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: white;
border-radius: 6px;
font-size: 0.85rem;
transition: all 0.3s ease;
}
.verify-step.success {
background: rgba(34, 197, 94, 0.1);
}
.step-icon {
font-size: 1rem;
}
.step-text {
flex: 1;
}
/* 响应 */
.response-status {
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.response-status.success {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
/* 认证机制 */
.auth-mechanism {
margin-top: 1rem;
padding: 0.75rem;
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid var(--vp-c-brand);
border-radius: 6px;
}
.mechanism-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-brand);
}
.mechanism-content code {
display: block;
background: #1e293b;
color: #e2e8f0;
padding: 0.5rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.75rem;
word-break: break-all;
}
/* 后续请求 */
.subsequent-request {
margin-top: 0.5rem;
}
.auth-header {
margin-top: 0.5rem;
}
.auth-header code {
display: block;
background: #1e293b;
color: #e2e8f0;
padding: 0.5rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.75rem;
word-break: break-all;
}
.flow-arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-2);
margin: 0.5rem 0;
}
/* 状态说明 */
.state-description {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
}
.description-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.75rem;
}
.description-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.description-content code {
background: var(--vp-c-bg-soft);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: var(--vp-c-brand);
}
/* 重置 */
.reset-section {
text-align: center;
}
.reset-btn {
padding: 0.75rem 2rem;
border: none;
border-radius: 8px;
background: #64748b;
color: white;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
}
.reset-btn:hover {
background: #475569;
transform: translateY(-1px);
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.mode-selector {
flex-direction: column;
align-items: flex-start;
}
.mode-buttons {
width: 100%;
}
.mode-btn {
flex: 1;
}
}
</style>