☰
GET与POST的本质区别及Session、Cookie、Token鉴权机制详解
2026/10/19 4:33:32 网站建设 项目流程

1. 从一个接口调试翻车现场说起

很多人第一次接触 Web 开发,都是从"调通一个接口"开始的。我也不例外。当年写第一个登录功能,前端表单提交完,后端返回 200,我兴冲冲地刷新页面,结果登录状态没了。当时我盯着浏览器控制台看了半天,以为是后端没存住数据,后来才发现——问题出在我用 GET 发了一个带密码的请求,而且压根没搞明白 Cookie 到底是怎么被浏览器自动带上的。

这个标题里的内容,说白了就是每个后端和前端都绕不过去的一道坎:GET 和 POST 到底差在哪,Session、Cookie、Token 又分别解决什么问题。它看起来像是面试八股,但真正落到项目里,任何一个理解偏差都会变成线上事故。比如把敏感参数塞进 URL、把 Token 存进 localStorage 被 XSS 偷走、Session 在分布式环境下对不上号,这些都是我亲手踩过的坑。

这篇内容适合三类人:刚入行的前后端新手,想把这几个概念一次性理清楚;有一定经验但总在鉴权环节翻车的开发者,想搞明白底层机制;还有准备面试但不想背死答案的人,想真正理解而不是硬记。我会从协议层讲到工程实践,把每个"为什么"都拆开讲透,最后给出一套可以直接抄作业的鉴权方案。

2. GET 与 POST 的本质区别,别只背那几条

2.1 从 HTTP 报文结构看两者的真实差异

网上流传的"GET 参数在 URL、POST 参数在 Body"其实只是表象。要真正理解区别,得看 HTTP 报文本身。一个 HTTP 请求由三部分组成:请求行、请求头、请求体。GET 和 POST 都是这个结构,区别在于语义约定和参数放置位置。

GET 的请求行长这样:GET /api/user?id=123 HTTP/1.1,参数直接拼在 URL 的查询字符串里。POST 的请求行是POST /api/user HTTP/1.1,参数放在请求体里,请求头会多一个Content-Type来声明格式,比如application/json或application/x-www-form-urlencoded。

关键点在于:HTTP 协议本身并没有禁止 GET 带请求体,也没有禁止 POST 带查询参数。这是很多人不知道的冷知识。协议只规定了语义,具体怎么用是工程约定。但为什么大家还是严格区分?因为中间环节——浏览器、代理、缓存、日志系统——都默认按这个约定来处理。

我实测过一个场景:用某些 HTTP 客户端给 GET 请求塞 Body,服务端框架能收到,但浏览器发起的 GET 请求,Body 会被直接丢弃。所以别想着钻空子,老老实实按约定来。

2.2 安全性、幂等性、缓存:三个被误解最深的点

先说安全性。很多人说"POST 比 GET 安全",这话只对了一半。GET 参数在 URL 里,会被浏览器历史、服务器访问日志、代理服务器、Referer 头记录,所以敏感信息绝对不能放 URL。但 POST 的 Body 在网络传输中如果不走 HTTPS,一样是明文,抓包工具看得一清二楚。所以真正的安全来自 HTTPS,不是 GET 或 POST 的选择。

再说幂等性。这是 RESTful 设计的核心概念。幂等的意思是:同一个请求执行一次和执行 N 次,对服务器状态的影响相同。GET 是幂等的,你查一次用户信息和查十次,数据库不会变。POST 通常不幂等,提交一次订单和提交十次,可能生成十个订单。这就是为什么刷新一个 POST 提交后的页面,浏览器会弹窗问"是否重新提交表单"——因为它知道这可能造成重复操作。

最后是缓存。GET 请求可以被浏览器、CDN、代理服务器缓存,因为它是幂等的、可预测的。POST 默认不缓存。这个特性在性能优化里非常关键。我曾经把一个商品列表接口从 POST 改成 GET,配合Cache-Control头,QPS 直接降了一半,因为大量请求被 CDN 挡掉了。

对比维度GETPOST
参数位置URL 查询字符串请求体
幂等性幂等通常不幂等
可缓存可缓存默认不缓存
长度限制受 URL 长度限制(浏览器约 2KB-8KB)理论上无限制,受服务端配置约束
安全性参数暴露在 URL 和日志中参数在 Body,但仍需 HTTPS
典型用途查询、筛选、分页创建、修改、提交

2.3 什么时候必须用 POST,什么时候 GET 更合适

判断标准其实很简单:这个操作会不会改变服务器状态。查询类、筛选类、分页类,一律用 GET,因为可缓存、可分享、可收藏。创建、更新、删除类,用 POST(或者 PUT/DELETE,看你的 API 设计风格)。

但有个例外场景要注意:参数特别长的时候。比如一个复杂的报表查询,筛选条件有几十个字段,拼成 URL 可能超过浏览器限制。这时候用 POST 把查询条件放 Body 里是合理的,虽然它不改变状态。这种叫"POST 用于查询",在 Elasticsearch 的_search接口里就是这么设计的。

还有一个坑:GET 请求的 URL 长度限制不是 HTTP 协议规定的,而是浏览器和服务器各自设的。IE 时代限制约 2083 字符,现代浏览器宽松很多,但 Nginx 默认的large_client_header_buffers是 8KB。我遇到过一次线上问题,一个导出接口用 GET 传了大量 ID,结果在 Nginx 层被截断,返回 414。后来改成 POST 才解决。

提示:如果你的接口参数可能超过 2000 字符,优先考虑 POST,别赌浏览器和服务器的限制。

3. Session、Cookie、Token 到底谁管谁

3.1 Cookie:浏览器端的"身份证复印件"

Cookie 是服务器通过Set-Cookie响应头写给浏览器的一小段数据,浏览器存下来,之后每次请求同一个域名时自动通过Cookie请求头带上。它本质上解决的是HTTP 无状态的问题——服务器记不住你是谁,那就让浏览器每次主动出示凭证。

一个典型的Set-Cookie长这样:

Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600

这里每个属性都有讲究。HttpOnly让 JavaScript 读不到这个 Cookie,能防 XSS 窃取。Secure表示只在 HTTPS 下发送。SameSite=Lax是防 CSRF 的关键,它限制跨站请求携带 Cookie。Max-Age控制过期时间。

我见过太多项目把HttpOnly忘了,结果前端用document.cookie就能读到 sessionId,一旦有 XSS 漏洞,攻击者直接拿走。这个属性加上去几乎零成本,但能挡掉一大类攻击。

Cookie 的容量限制是每个域名约 4KB,总数约 50 个。所以别往里塞大对象,它只适合存一个标识符。

3.2 Session:服务器端的"档案柜"

Session 是服务器端的存储机制。用户登录成功后,服务器生成一个唯一的 sessionId,把用户信息存在服务端(内存、Redis、数据库都行),然后把 sessionId 通过 Cookie 发给浏览器。之后浏览器每次带着 sessionId 来,服务器拿它去"档案柜"里查对应的用户数据。

这个设计的好处是敏感数据不落客户端。用户角色、权限、登录时间这些都在服务器手里,客户端只有一个无意义的 ID。但代价是服务器要维护状态,这就引出了分布式环境下的经典难题。

单机时代,Session 存内存里没问题。一旦上了多台服务器,用户第一次请求打到 A 机器,Session 存在 A 的内存里,第二次请求被负载均衡分到 B 机器,B 找不到这个 Session,用户就被踢下线了。解决方案有三种:

  • Session 粘滞:负载均衡按 IP 或 Cookie 把同一用户固定到同一台机器。简单但容错差,机器挂了用户就掉线。
  • Session 复制:机器之间互相同步 Session。机器一多,同步开销爆炸。
  • 集中式存储:Session 统一存 Redis。这是目前最主流的方案,扩展性好,机器无状态。

我现在的项目一律用 Redis 存 Session,key 是session:{sessionId},value 是序列化的用户信息,过期时间设 30 分钟,每次访问续期。这样任何一台应用服务器都能处理任何用户的请求。

3.3 Token:自带信息的"通行证"

Token 的思路完全不同。它不依赖服务器存储,而是把用户信息编码进 Token 本身,服务器拿到后验证签名即可,不需要查库。最典型的就是 JWT(JSON Web Token)。

一个 JWT 由三部分组成,用点号分隔:Header.Payload.Signature。Header 声明算法,Payload 放用户 ID、过期时间等声明,Signature 是用密钥对前两部分做的签名,防止篡改。

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNzAwMDAwMDAwfQ.xxxxx

Token 的最大优势是服务端无状态,特别适合微服务架构和跨域场景。但它也有硬伤:签发后无法主动失效。用户登出、改密码、被封号,Token 在过期前依然有效。要解决这个,就得引入黑名单,而黑名单又变成了有状态存储,绕回去了。

所以实践中常见的做法是:Access Token 短期有效(15分钟到2小时),Refresh Token 长期有效(7天到30天)。Access Token 过期后用 Refresh Token 换新的。这样既保证了无状态的性能优势,又通过短过期时间限制了风险窗口。

机制存储位置状态主动失效适用场景
Cookie浏览器无不适用承载标识符
Session服务端有状态容易传统 Web 应用
Token客户端无状态困难微服务、跨域、移动端

3.4 三者不是替代关系,而是协作关系

新手最容易犯的错是把这三个当成"三选一"。实际上它们经常一起用。经典组合是:Session 存在服务端,sessionId 通过 Cookie 传输。Token 则是另一条路线,可以放在 Cookie 里,也可以放在Authorization头里。

我个人的选型经验是:如果是传统的服务端渲染应用,Session + Cookie 最省事,浏览器自动管理,代码量最少。如果是前后端分离、有移动端、或者微服务架构,Token 更合适,因为不依赖 Cookie 的自动携带机制,跨域和跨端都方便。

4. 手把手实现一套完整的鉴权流程

4.1 环境与依赖准备

我用 Node.js + Express 来演示,因为它的中间件机制能把这几个概念讲得最清楚。数据库用 Redis 存 Session,JWT 用jsonwebtoken库。这套组合在生产环境里非常常见。

npm init -y npm install express express-session connect-redis ioredis jsonwebtoken cookie-parser

这里解释下每个依赖的作用。express-session是 Session 中间件,connect-redis让它把 Session 存到 Redis 而不是内存,ioredis是 Redis 客户端,jsonwebtoken负责签发和验证 JWT,cookie-parser用来解析 Cookie。

Redis 的安装我就不展开了,本地起一个redis-server即可,默认端口 6379。

4.2 Session + Cookie 方案的完整实现

先看服务端配置:

const express = require('express'); const session = require('express-session'); const RedisStore = require('connect-redis').default; const Redis = require('ioredis'); const redisClient = new Redis({ host: '127.0.0.1', port: 6379 }); const app = express(); app.use(express.json()); app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret-key-change-in-production', resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: false, // 生产环境走 HTTPS 时设为 true sameSite: 'lax', maxAge: 30 * 60 * 1000 // 30 分钟 } }));

resave: false表示如果 Session 没被修改就不重新保存,减少 Redis 写入。saveUninitialized: false表示未登录的访客不创建 Session,避免爬虫刷爆存储。这两个参数是性能优化的关键,很多人默认配置不改,结果 Redis 里堆了一堆空 Session。

登录接口:

app.post('/login', async (req, res) => { const { username, password } = req.body; const user = await findUser(username); if (!user || !verifyPassword(password, user.passwordHash)) { return res.status(401).json({ error: '用户名或密码错误' }); } req.session.userId = user.id; req.session.role = user.role; res.json({ message: '登录成功' }); });

登录成功后,express-session会自动生成 sessionId,通过Set-Cookie写给浏览器。之后浏览器每次请求都会带上这个 Cookie,服务端从 Redis 里取出对应的 Session 数据。

鉴权中间件:

function requireLogin(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: '未登录' }); } next(); } app.get('/profile', requireLogin, async (req, res) => { const user = await findUserById(req.session.userId); res.json({ username: user.username, role: req.session.role }); });

登出就是销毁 Session:

app.post('/logout', (req, res) => { req.session.destroy(() => { res.clearCookie('connect.sid'); res.json({ message: '已登出' }); }); });

这套流程跑通后,你可以打开浏览器开发者工具,在 Application 面板里看到connect.sid这个 Cookie,值是加密后的 sessionId。每次请求的 Request Headers 里都会自动带上它。

4.3 Token 方案的完整实现

Token 方案不依赖 Session 存储,服务端只负责签发和验证。

const jwt = require('jsonwebtoken'); const ACCESS_SECRET = 'access-secret'; const REFRESH_SECRET = 'refresh-secret'; app.post('/token/login', async (req, res) => { const { username, password } = req.body; const user = await findUser(username); if (!user || !verifyPassword(password, user.passwordHash)) { return res.status(401).json({ error: '用户名或密码错误' }); } const accessToken = jwt.sign( { userId: user.id, role: user.role }, ACCESS_SECRET, { expiresIn: '15m' } ); const refreshToken = jwt.sign( { userId: user.id, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' } ); res.json({ accessToken, refreshToken }); });

验证中间件:

function verifyToken(req, res, next) { const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: '缺少 Token' }); } const token = authHeader.slice(7); try { const payload = jwt.verify(token, ACCESS_SECRET); req.user = payload; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token 已过期', code: 'TOKEN_EXPIRED' }); } return res.status(401).json({ error: 'Token 无效' }); } }

前端拿到 Token 后,存在内存变量里(不要存 localStorage),每次请求手动加到Authorization头。Access Token 过期后,用 Refresh Token 调/token/refresh换新的。

这里有个关键设计:Refresh Token 应该存在 HttpOnly Cookie 里,而不是前端 JS 能读到的地方。这样即使 XSS 攻击也偷不走 Refresh Token。Access Token 存内存,页面刷新就丢,用 Refresh Token 重新获取即可。

4.4 参数选择与安全配置的计算过程

过期时间怎么定?这不是拍脑袋。Access Token 的过期时间要平衡安全性和用户体验。太短,用户频繁刷新,请求量翻倍;太长,泄露后风险窗口大。我的经验值是15 分钟到 2 小时,具体看业务敏感度。金融类应用取 15 分钟,普通内容类可以到 2 小时。

Refresh Token 的过期时间通常设7 到 30 天。设太长,用户长期不登录也能被换新,风险大;设太短,用户频繁重新登录,体验差。7 天是个比较平衡的值。

Session 的过期时间类似,但要注意滑动过期和绝对过期的区别。滑动过期是每次访问都续期,用户一直活跃就不会掉线。绝对过期是从登录算起,到点必须重新登录。安全要求高的场景用绝对过期,普通场景用滑动过期。

密钥长度方面,HMAC-SHA256 的密钥至少 32 字节,太短容易被暴力破解。生产环境的密钥必须从环境变量读取,绝对不能硬编码在代码里。我见过把密钥提交到 Git 仓库的事故,等于把大门钥匙挂在门口。

注意:JWT 的 Payload 是 Base64 编码,不是加密。任何人都能解码看到内容。所以绝对不要在 Payload 里放密码、手机号等敏感信息,只放 userId 这类无意义的标识。

5. 踩坑实录与常见问题速查

5.1 跨域场景下 Cookie 带不上的排查

前后端分离项目最常见的坑:前端在localhost:3000,后端在localhost:8080,登录接口返回了Set-Cookie,但后续请求死活带不上 Cookie。

原因有三个,逐个排查。第一,CORS 配置没开凭证。后端要设置Access-Control-Allow-Credentials: true,且Access-Control-Allow-Origin不能是*,必须是具体域名。第二,前端请求没带凭证。用 fetch 要加credentials: 'include',用 axios 要加withCredentials: true。第三,Cookie 的 SameSite 属性。跨站请求默认不发送SameSite=Lax的 Cookie,需要设为SameSite=None; Secure,而Secure又要求 HTTPS。

这三个条件必须同时满足,缺一个都不行。我当年在这个问题上耗了一下午,就是因为只改了 CORS 没改前端配置。

5.2 Token 过期后的无感刷新怎么做

用户体验的关键在于:Access Token 过期时,用户不应该被弹回登录页,而应该自动刷新后重试原请求。

实现思路是在 axios 的响应拦截器里做文章:

let isRefreshing = false; let pendingQueue = []; axios.interceptors.response.use( response => response, async error => { const originalRequest = error.config; if (error.response?.data?.code === 'TOKEN_EXPIRED' && !originalRequest._retry) { if (isRefreshing) { return new Promise(resolve => { pendingQueue.push(token => { originalRequest.headers['Authorization'] = `Bearer ${token}`; resolve(axios(originalRequest)); }); }); } originalRequest._retry = true; isRefreshing = true; try { const { data } = await axios.post('/token/refresh', {}, { withCredentials: true }); const newToken = data.accessToken; setAccessToken(newToken); pendingQueue.forEach(cb => cb(newToken)); pendingQueue = []; originalRequest.headers['Authorization'] = `Bearer ${newToken}`; return axios(originalRequest); } finally { isRefreshing = false; } } return Promise.reject(error); } );

这段代码的精髓在于isRefreshing锁和pendingQueue。如果同时有五个请求都返回 401,不能刷新五次 Token,而是第一个请求负责刷新,其余四个挂起等待,刷新完成后一起重试。这个细节不做,会出现 Token 被刷新多次导致 Refresh Token 失效的问题。

5.3 常见问题速查表

问题现象可能原因排查方向
登录后刷新页面状态丢失Session 未持久化或 Cookie 未带上检查 Redis 连接、Cookie 的 Domain/Path
跨域请求 Cookie 丢失CORS 或 SameSite 配置问题检查 Allow-Credentials、withCredentials、SameSite
Token 验证总是失败密钥不一致或算法不匹配确认签发和验证用同一密钥和算法
用户登出后 Token 仍有效JWT 无状态特性引入黑名单或缩短过期时间
分布式环境频繁掉线Session 存在单机内存改用 Redis 集中存储
移动端 Cookie 不生效部分客户端不支持 Cookie改用 Token 放 Header

5.4 几个只有踩过才知道的细节

第一,Cookie 的 Domain 和 Path 要设对。默认 Domain 是当前域名,如果前后端不同子域,比如api.example.com和www.example.com,要把 Domain 设为.example.com才能共享。Path 默认是/,一般不用改,但如果你把 Cookie 限制在/admin路径下,其他路径就带不上。

第二,Session 的 key 名可以自定义。express-session默认用connect.sid,这个信息暴露了技术栈,建议改成无意义的名字,比如sid或随机字符串,减少被针对性攻击的可能。

第三,JWT 的exp是秒级时间戳,不是毫秒。我见过有人传毫秒,结果 Token 有效期变成几万年。签发时用Math.floor(Date.now() / 1000),别搞错单位。

第四,Redis 存 Session 要设过期时间。express-session的cookie.maxAge会同步到 Redis 的 TTL,但如果你手动操作 Redis,记得自己设EXPIRE,否则 Session 永远不清理,内存迟早爆。

第五,Token 不要放在 URL 参数里。有些教程图省事,把 Token 拼在 URL 上,比如/api/data?token=xxx。这样 Token 会进浏览器历史、服务器日志、Referer 头,泄露风险极高。永远放Authorization头。

6. 选型建议与我的实际体会

回到最初的问题:项目里到底该用 Session 还是 Token?我的判断逻辑是这样的。

如果你的应用是传统的服务端渲染,用户通过浏览器访问,没有移动端,没有跨域需求,那 Session + Cookie 是最省心的方案。浏览器自动管理 Cookie,服务端代码简单,还能主动踢人下线。中小型项目我优先推荐这个。

如果你的应用是前后端分离 + 多端(Web、App、小程序),或者微服务架构,那 Token 更合适。它不依赖 Cookie 的自动携带,跨域跨端都统一,服务端无状态易于水平扩展。代价是要自己处理刷新、黑名单这些额外逻辑。

还有一种混合方案我也用过:Session 存 Redis,但 sessionId 不通过 Cookie 传,而是放在Authorization头里,前端手动管理。这样既有服务端可控的优势,又避免了 Cookie 的跨域麻烦。适合对安全性要求高、又不想被 Cookie 限制的场景。

最后分享一个我踩过最深的坑:永远不要相信客户端传来的任何身份信息。曾经有个接口,前端把 userId 放在请求体里传过来,后端直接拿来查数据。结果攻击者改个 userId 就能看别人的订单。身份必须从服务端验证过的 Session 或 Token 里取,客户端传的一律不可信。这个原则看起来简单,但每年都有项目栽在上面。

这套东西说到底不复杂,难的是把每个细节都想清楚,把每个边界都处理到。GET 和 POST 的语义、Cookie 的属性、Session 的存储、Token 的刷新,每一环都有它的道理,理解了道理,用起来才不会慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询