2026-01-18 12:21:49 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
SessionCookieDemo.vue
|
2026-01-19 11:25:10 +08:00
|
|
|
|
Session + Cookie(手动推进,更贴近真实 Web 登录态)
|
2026-01-18 12:21:49 +08:00
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="session-demo">
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="header">
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="title">🍪 Session + Cookie:有状态登录</div>
|
|
|
|
|
|
<div class="subtitle">
|
|
|
|
|
|
默认手动推进:先看清楚状态再进入下一步(避免“自动下一步”误解)。
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<button class="btn primary" @click="start" :disabled="step !== 0">
|
|
|
|
|
|
开始
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</button>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<button class="btn" @click="prev" :disabled="step <= 1">上一步</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<button
|
2026-01-19 11:25:10 +08:00
|
|
|
|
class="btn primary"
|
|
|
|
|
|
@click="next"
|
|
|
|
|
|
:disabled="step === 0 || step >= maxStep"
|
2026-01-18 12:21:49 +08:00
|
|
|
|
>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
下一步
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</button>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<button class="btn" @click="reset">重置</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div v-if="step > 0" class="progress">
|
|
|
|
|
|
Step {{ step }} / {{ maxStep }} · {{ steps[step - 1]?.title }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">浏览器(客户端)</div>
|
|
|
|
|
|
<div class="box">
|
|
|
|
|
|
<div class="box-title">Cookie Jar</div>
|
|
|
|
|
|
<div v-if="cookie" class="kv">
|
|
|
|
|
|
<div class="k">session_id</div>
|
|
|
|
|
|
<div class="v mono">{{ cookie }}</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div v-else class="empty">暂无 Cookie</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="box">
|
|
|
|
|
|
<div class="box-title">本步请求</div>
|
|
|
|
|
|
<pre class="code"><code>{{ clientRequest }}</code></pre>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">服务器</div>
|
|
|
|
|
|
<div class="box">
|
|
|
|
|
|
<div class="box-title">Session Store(Redis/Memory)</div>
|
|
|
|
|
|
<div v-if="session" class="kv">
|
|
|
|
|
|
<div class="k mono">{{ cookie }}</div>
|
|
|
|
|
|
<div class="v">
|
|
|
|
|
|
<div class="row"><span class="muted">user_id</span> 123</div>
|
|
|
|
|
|
<div class="row"><span class="muted">username</span> alice</div>
|
|
|
|
|
|
<div class="row"><span class="muted">role</span> admin</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div v-else class="empty">暂无 Session</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="box">
|
|
|
|
|
|
<div class="box-title">本步响应</div>
|
|
|
|
|
|
<pre class="code"><code>{{ serverResponse }}</code></pre>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">{{ steps[step - 1]?.title || '流程说明' }}</div>
|
|
|
|
|
|
<div class="desc">{{ steps[step - 1]?.desc }}</div>
|
|
|
|
|
|
<div v-if="steps[step - 1]?.warn" class="warn">
|
|
|
|
|
|
<div class="warn-title">注意</div>
|
|
|
|
|
|
<div class="warn-text">{{ steps[step - 1]?.warn }}</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const maxStep = 5
|
|
|
|
|
|
const step = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
const cookie = ref('')
|
|
|
|
|
|
const session = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const steps = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '1) 登录请求(POST /login)',
|
|
|
|
|
|
desc: '用户提交用户名/密码,服务器验证成功后创建 Session。'
|
2026-01-18 12:21:49 +08:00
|
|
|
|
},
|
2026-01-19 11:25:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '2) 服务器 Set-Cookie',
|
|
|
|
|
|
desc: '服务器返回 Set-Cookie: session_id=...;浏览器保存 Cookie。',
|
|
|
|
|
|
warn: 'Cookie 建议加 HttpOnly + Secure + SameSite;同时要考虑 CSRF 防护。'
|
2026-01-18 12:21:49 +08:00
|
|
|
|
},
|
2026-01-19 11:25:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '3) 后续请求自动带 Cookie',
|
|
|
|
|
|
desc: '浏览器对同域请求会自动带上 Cookie,服务器用 session_id 查 Session。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '4) 授权判断(role/权限)',
|
|
|
|
|
|
desc: '认证(你是谁)之后,仍需要授权(你能做什么)。比如 admin 才能访问管理接口。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '5) 注销',
|
|
|
|
|
|
desc: '服务器删除 Session(或让其过期),并让浏览器清理 Cookie。'
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
2026-01-19 11:25:10 +08:00
|
|
|
|
]
|
2026-01-18 12:21:49 +08:00
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
const start = () => {
|
|
|
|
|
|
step.value = 1
|
|
|
|
|
|
cookie.value = ''
|
|
|
|
|
|
session.value = false
|
|
|
|
|
|
}
|
2026-01-18 12:21:49 +08:00
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
const next = () => {
|
|
|
|
|
|
step.value = Math.min(maxStep, step.value + 1)
|
|
|
|
|
|
applyState()
|
|
|
|
|
|
}
|
2026-01-18 12:21:49 +08:00
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
const prev = () => {
|
|
|
|
|
|
step.value = Math.max(1, step.value - 1)
|
|
|
|
|
|
applyState()
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
const reset = () => {
|
|
|
|
|
|
step.value = 0
|
|
|
|
|
|
cookie.value = ''
|
|
|
|
|
|
session.value = false
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
const applyState = () => {
|
|
|
|
|
|
if (step.value <= 1) {
|
|
|
|
|
|
cookie.value = ''
|
|
|
|
|
|
session.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (step.value >= 2) {
|
|
|
|
|
|
if (!cookie.value)
|
|
|
|
|
|
cookie.value = 'sess_' + Math.random().toString(36).slice(2, 10)
|
|
|
|
|
|
session.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (step.value >= 5) {
|
|
|
|
|
|
// logout (show as empty state by step title/response)
|
|
|
|
|
|
// We don't auto-clear state; keep it visible until reset to avoid “auto” confusion.
|
|
|
|
|
|
}
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
2026-01-19 11:25:10 +08:00
|
|
|
|
|
|
|
|
|
|
const clientRequest = computed(() => {
|
|
|
|
|
|
if (step.value === 0) return '(点击开始)'
|
|
|
|
|
|
if (step.value === 1) {
|
|
|
|
|
|
return `POST /login
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
|
|
|
|
{"username":"alice","password":"******"}`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (step.value === 2) return '(等待服务器响应并写入 Cookie)'
|
|
|
|
|
|
if (step.value === 3) {
|
|
|
|
|
|
return `GET /api/user/profile
|
|
|
|
|
|
Cookie: session_id=${cookie.value}`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (step.value === 4) {
|
|
|
|
|
|
return `GET /api/admin/users
|
|
|
|
|
|
Cookie: session_id=${cookie.value}`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `POST /logout
|
|
|
|
|
|
Cookie: session_id=${cookie.value}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const serverResponse = computed(() => {
|
|
|
|
|
|
if (step.value === 0) return ''
|
|
|
|
|
|
if (step.value === 1) return '200 OK (credentials valid)'
|
|
|
|
|
|
if (step.value === 2) {
|
|
|
|
|
|
return `200 OK
|
|
|
|
|
|
Set-Cookie: session_id=${cookie.value}; HttpOnly; Secure; SameSite=Lax`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (step.value === 3) return '200 OK (profile payload...)'
|
|
|
|
|
|
if (step.value === 4)
|
|
|
|
|
|
return '200 OK (admin data...) / 403 Forbidden (if not admin)'
|
|
|
|
|
|
return `200 OK
|
|
|
|
|
|
Set-Cookie: session_id=; Max-Age=0`
|
|
|
|
|
|
})
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.session-demo {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-01-19 11:25:10 +08:00
|
|
|
|
border-radius: 8px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
padding: 1.5rem;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
margin: 1rem 0;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
2026-01-19 11:25:10 +08:00
|
|
|
|
margin-bottom: 1rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title {
|
2026-01-19 11:25:10 +08:00
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
2026-01-19 11:25:10 +08:00
|
|
|
|
margin-top: 0.25rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
display: flex;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
gap: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
flex-wrap: wrap;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
margin-bottom: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
cursor: pointer;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 0.875rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.btn.primary {
|
|
|
|
|
|
background: var(--vp-c-brand);
|
|
|
|
|
|
border-color: var(--vp-c-brand);
|
|
|
|
|
|
color: var(--vp-c-bg);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.btn:disabled {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
cursor: not-allowed;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.progress {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.grid {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
display: grid;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
grid-template-columns: 1fr 1fr;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
gap: 1rem;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
margin-bottom: 1rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.card {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.card-title {
|
|
|
|
|
|
font-weight: 800;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
margin-bottom: 0.75rem;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.box {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-01-19 11:25:10 +08:00
|
|
|
|
background: var(--vp-c-bg-alt);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.box-title {
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
margin-bottom: 0.5rem;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
font-size: 0.9rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.empty {
|
|
|
|
|
|
color: var(--vp-c-text-3);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.kv {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 2fr;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
align-items: start;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.k {
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.v {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
line-height: 1.7;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.row {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
display: flex;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
gap: 0.5rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.muted {
|
|
|
|
|
|
color: var(--vp-c-text-3);
|
|
|
|
|
|
min-width: 72px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.mono {
|
|
|
|
|
|
font-family: var(--vp-font-family-mono);
|
|
|
|
|
|
word-break: break-all;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.code {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-01-19 11:25:10 +08:00
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.desc {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
line-height: 1.75;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.warn {
|
|
|
|
|
|
margin-top: 0.75rem;
|
|
|
|
|
|
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.18);
|
|
|
|
|
|
background: rgba(var(--vp-c-brand-rgb), 0.06);
|
2026-01-18 12:21:49 +08:00
|
|
|
|
border-radius: 8px;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.warn-title {
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
margin-bottom: 0.25rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
.warn-text {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
line-height: 1.7;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 11:25:10 +08:00
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.grid {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|