cookie.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <script setup lang="ts">
  2. definePageMeta({
  3. auth: false,
  4. })
  5. type CookieItem = {
  6. name: string
  7. value: string
  8. decodedValue?: string
  9. valueLength: number
  10. }
  11. type CookieSsrSnapshot = {
  12. serverCollectedAt: string
  13. url: string
  14. method: string
  15. clientIp: string | null
  16. cookieHeader: string
  17. cookieHeaderLength: number
  18. cookies: CookieItem[]
  19. headers: Record<string, string>
  20. }
  21. function safeDecodeURIComponent(value: string): string | undefined {
  22. try {
  23. return decodeURIComponent(value)
  24. }
  25. catch {
  26. return undefined
  27. }
  28. }
  29. function parseCookieHeader(cookieHeader: string): CookieItem[] {
  30. if (!cookieHeader)
  31. return []
  32. return cookieHeader
  33. .split(';')
  34. .map(part => part.trim())
  35. .filter(Boolean)
  36. .map((part) => {
  37. const idx = part.indexOf('=')
  38. const name = (idx >= 0 ? part.slice(0, idx) : part).trim()
  39. const value = (idx >= 0 ? part.slice(idx + 1) : '').trim()
  40. const decodedValue = value ? safeDecodeURIComponent(value) : undefined
  41. return {
  42. name,
  43. value,
  44. decodedValue,
  45. valueLength: value.length,
  46. }
  47. })
  48. }
  49. function collectSsrSnapshot(): CookieSsrSnapshot {
  50. const url = useRequestURL()
  51. const event = useRequestEvent()
  52. const headersRaw = useRequestHeaders([
  53. 'cookie',
  54. 'user-agent',
  55. 'accept-language',
  56. 'referer',
  57. 'host',
  58. 'x-forwarded-for',
  59. 'x-real-ip',
  60. ])
  61. const headers: Record<string, string> = {}
  62. for (const [k, v] of Object.entries(headersRaw)) {
  63. if (typeof v === 'string')
  64. headers[k] = v
  65. }
  66. const cookieHeader = headersRaw.cookie || ''
  67. const cookies = parseCookieHeader(cookieHeader)
  68. const xff = headersRaw['x-forwarded-for']
  69. const clientIp = (xff ? xff.split(',')[0]?.trim() : null)
  70. || headersRaw['x-real-ip']
  71. || event?.node?.req?.socket?.remoteAddress
  72. || null
  73. return {
  74. serverCollectedAt: new Date().toISOString(),
  75. url: url.toString(),
  76. method: event?.node?.req?.method || 'UNKNOWN',
  77. clientIp,
  78. cookieHeader,
  79. cookieHeaderLength: cookieHeader.length,
  80. cookies,
  81. headers,
  82. }
  83. }
  84. const importMetaServer = import.meta.server
  85. const ssrSnapshot = useState<CookieSsrSnapshot | null>('test-cookie-ssr-snapshot', () => {
  86. if (!import.meta.server)
  87. return null
  88. return collectSsrSnapshot()
  89. })
  90. const hydrated = ref(false)
  91. const clientNow = ref(new Date().toISOString())
  92. onMounted(() => {
  93. hydrated.value = true
  94. clientNow.value = new Date().toISOString()
  95. })
  96. </script>
  97. <template>
  98. <div class="cookie-page">
  99. <h1 class="title">
  100. Cookie SSR 调试
  101. </h1>
  102. <div class="meta">
  103. <div class="meta__row">
  104. <span class="meta__label">当前环境</span>
  105. <span class="meta__value">{{ importMetaServer ? 'server' : 'client' }}</span>
  106. </div>
  107. <div class="meta__row">
  108. <span class="meta__label">客户端已挂载</span>
  109. <span class="meta__value">{{ hydrated ? 'yes' : 'no' }}</span>
  110. </div>
  111. <div class="meta__row">
  112. <span class="meta__label">客户端时间</span>
  113. <span class="meta__value">{{ clientNow }}</span>
  114. </div>
  115. </div>
  116. <van-divider />
  117. <template v-if="ssrSnapshot">
  118. <h2 class="section-title">
  119. 原始 Cookie Header
  120. </h2>
  121. <pre class="code">{{ ssrSnapshot.cookieHeader || '(empty)' }}</pre>
  122. <van-divider />
  123. <h2 class="section-title">
  124. 解析后的 Cookies
  125. </h2>
  126. <template v-if="ssrSnapshot.cookies.length">
  127. <div class="table-wrap">
  128. <table class="table">
  129. <thead>
  130. <tr>
  131. <th>name</th>
  132. <th>value</th>
  133. <th>decodedValue</th>
  134. <th>length</th>
  135. </tr>
  136. </thead>
  137. <tbody>
  138. <tr
  139. v-for="c in ssrSnapshot.cookies"
  140. :key="c.name"
  141. >
  142. <td class="mono">
  143. {{ c.name }}
  144. </td>
  145. <td class="mono">
  146. {{ c.value }}
  147. </td>
  148. <td class="mono">
  149. {{ c.decodedValue ?? '-' }}
  150. </td>
  151. <td class="mono">
  152. {{ c.valueLength }}
  153. </td>
  154. </tr>
  155. </tbody>
  156. </table>
  157. </div>
  158. </template>
  159. <template v-else>
  160. <div class="empty">
  161. 未收到 Cookie(请求头里没有 cookie 字段或为空)。
  162. </div>
  163. </template>
  164. <van-divider />
  165. <h2 class="section-title">
  166. Cookie 相关请求头(SSR 采样)
  167. </h2>
  168. <pre class="code">{{ JSON.stringify(ssrSnapshot.headers, null, 2) }}</pre>
  169. <van-divider />
  170. <h2 class="section-title">
  171. SSR 快照(服务端渲染时采集并注入到客户端)
  172. </h2>
  173. <van-cell-group inset>
  174. <van-cell
  175. title="SSR 采集时间"
  176. :value="ssrSnapshot.serverCollectedAt"
  177. />
  178. <van-cell
  179. title="请求 URL"
  180. :value="ssrSnapshot.url"
  181. />
  182. <van-cell
  183. title="请求方法"
  184. :value="ssrSnapshot.method"
  185. />
  186. <van-cell
  187. title="推测客户端 IP"
  188. :value="ssrSnapshot.clientIp || '-'"
  189. />
  190. <van-cell
  191. title="Cookie Header 长度"
  192. :value="String(ssrSnapshot.cookieHeaderLength)"
  193. />
  194. <van-cell
  195. title="Cookie 数量(解析后)"
  196. :value="String(ssrSnapshot.cookies.length)"
  197. />
  198. </van-cell-group>
  199. </template>
  200. <template v-else>
  201. <van-empty description="未获取到 SSR 快照(可能是纯 CSR 导航且未命中 SSR,或当前环境不可用)。" />
  202. </template>
  203. </div>
  204. </template>
  205. <style scoped>
  206. .cookie-page {
  207. padding: 16px;
  208. }
  209. .title {
  210. font-size: 18px;
  211. font-weight: 700;
  212. margin: 0 0 12px;
  213. }
  214. .section-title {
  215. font-size: 14px;
  216. font-weight: 700;
  217. margin: 12px 0 8px;
  218. }
  219. .meta {
  220. display: grid;
  221. grid-template-columns: 1fr;
  222. gap: 6px;
  223. padding: 12px;
  224. border-radius: 10px;
  225. background: rgba(0, 0, 0, 0.03);
  226. }
  227. .meta__row {
  228. display: flex;
  229. justify-content: space-between;
  230. gap: 12px;
  231. font-size: 12px;
  232. }
  233. .meta__label {
  234. opacity: 0.7;
  235. }
  236. .meta__value {
  237. font-weight: 600;
  238. word-break: break-all;
  239. text-align: right;
  240. }
  241. .code {
  242. padding: 12px;
  243. border-radius: 10px;
  244. background: rgba(0, 0, 0, 0.04);
  245. overflow: auto;
  246. font-size: 12px;
  247. line-height: 1.45;
  248. margin: 0;
  249. }
  250. .table-wrap {
  251. overflow: auto;
  252. border-radius: 10px;
  253. background: rgba(0, 0, 0, 0.02);
  254. padding: 8px;
  255. }
  256. .table {
  257. width: 100%;
  258. border-collapse: collapse;
  259. font-size: 12px;
  260. }
  261. .table th,
  262. .table td {
  263. padding: 8px;
  264. border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  265. text-align: left;
  266. vertical-align: top;
  267. white-space: nowrap;
  268. }
  269. .mono {
  270. font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  271. }
  272. .empty {
  273. padding: 12px;
  274. border-radius: 10px;
  275. background: rgba(0, 0, 0, 0.03);
  276. font-size: 12px;
  277. opacity: 0.8;
  278. }
  279. .notes {
  280. margin: 0;
  281. padding-left: 18px;
  282. font-size: 12px;
  283. line-height: 1.6;
  284. }
  285. </style>