Files
test-repo/docs/.vitepress/theme/components/appendix/auth-design/AuthInteractiveLoginDemo.vue
T
sanbuphy d35211071a 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
2026-02-14 20:23:34 +08:00

863 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<!--
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: 6px;
padding: 1.5rem;
margin: 0.5rem 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: 6px;
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: 6px;
padding: 0.75rem;
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: 6px;
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>