做了几年Web开发,回头看你写的登录模块,会不会觉得“安全”这两个字特别虚?前阵子我给一个运营了两年的中型后台系统做例行巡检,第一轮自查就翻出十几个问题:登录接口没有频率限制,订单接口传个用户ID就能查到别人的数据,文件上传只校验了后缀名,还有个老接口直接用裸Cookie当身份凭证。这篇文章不打算讲太多形而上的理论,就想把“常见安全隐患”和“传统的基于Session与Token的安全校验”这两块透透地掰开揉碎——哪些漏洞最高发、Session和Token各自的原理与坑点、以及日常项目里到底该怎么选、怎么加固。不管你是刚接触后端的新人,还是被安全测试折腾过的老开发,照着这份思路排查一遍自己的项目,基本能覆盖大部分低级风险。
1. 常见安全隐患:攻击者最常下手的七个地方
安全排查做得多了,你会发现真正被利用的漏洞其实高度集中,翻来覆去就是那几类。与其花大力气追踪各种威胁情报,不如先把这几类高频问题在自己代码里堵死。
1.1 注入类攻击:SQL注入为什么至今仍是头号杀手
虽然ORM和预编译早就普及了,我依然见过大量项目里存在字符串拼SQL的老写法:
# 危险写法:直接拼接 sql = "SELECT * FROM users WHERE name = '" + username + "' AND password = '" + pwd + "'"如果攻击者在用户名处传入' OR '1'='1,拼接后的SQL就变成了SELECT * FROM users WHERE name = '' OR '1'='1' AND password = '...',恒等条件成立,登录验证直接被绕过。更严重的还能用 UNION 拖其他表的数据,或者通过堆叠注入执行更新、删除语句。要根治它说难也难,说简单也简单:所有数据库操作统一走参数化查询。
cursor.execute( "SELECT * FROM users WHERE name = ? AND password = ?", (username, pwd) )参数化查询的核心不是“过滤”输入,而是让数据库把传入内容严格当作数据而不是SQL代码来对待,注入自然无从发生。凡是能从客户端传进来、最终进入SQL语句的内容,一律禁止拼字符串,这是身份与数据安全的第一道底线。
1.2 跨站脚本攻击(XSS):把用户输入当代码执行
XSS的本质是让浏览器把用户输入的内容当成脚本执行。最常见的场景是留言板、昵称、个人简介这类需要展示用户内容的模块:用户提交<script>alert(document.cookie)</script>,如果原样存进数据库,其他访客打开页面时脚本就会执行,Cookie、登录态、页面里的敏感信息都可能被偷走。
XSS分为三类:反射型,通过URL参数带入页面;存储型,先入库再渲染给所有人,危害最大;DOM型,纯前端通过DOM操作执行了恶意内容。防御的两个核心方向分别是输入过滤和输出编码。输入端按白名单保留安全标签,输出端把<转成<,让浏览器只显示不执行。现在很多前端框架默认会转义,但像innerHTML、v-html这类危险输出通道还是得自己把好关。尽量不要试图用黑名单拦截脚本,攻击者绕过的花样太多了。
1.3 跨站请求伪造(CSRF):借用户之手发请求
CSRF攻击很多人听过,但未必想透过它的前提:浏览器在发起请求时会自动携带目标站点的Cookie,而服务端在验证Session时只看请求带没带有效的会话标识,并不关心这个请求是不是用户本人在目标页面里主动发起的。
于是攻击者构造一个恶意页面,诱导已登录用户访问,浏览器就会带着用户的Cookie向目标站点发起预先编排好的请求。修改邮箱、转账、发帖这类接口,如果只依赖Session校验,就可能被悄悄触发。经典防御手段是校验请求中的CSRF Token:服务端生成一次性的随机Token下发到页面,提交时必须带上,服务端比对一致才处理。同时给Cookie设置SameSite属性,限制跨站请求携带Cookie,能挡住一大半CSRF。对资金、密码这类敏感操作,再加一次二次确认。
1.4 失效的访问控制:越权比想象的更普遍
越权是这几年实际被利用频率很高的漏洞。它分两种:水平越权,普通用户能访问其他用户的数据;垂直越权,普通用户能调用管理员的接口。常见原因不是攻击手段多高级,而是接口只做了登录校验,没做归属校验。
举个例子,某后台系统的订单查询接口是/order/info?id=1001,后端只验证了“已登录”,没验证“这个订单属于当前登录用户”,攻击者遍历id就能看到所有人的订单。很多人觉得按钮隐藏、菜单不展示就算控制了权限,其实那只是前端控制,直接构造请求调用接口依然畅通无阻。正解是每个涉及资源访问的接口都做对象级授权校验:既校验身份,又校验对具体资源的操作权限。这活儿挺繁琐,但没有捷径。
1.5 另外三个不显眼但高频的隐患
除了上面四类,还有三个问题在实际巡检中出镜率也非常高:
- 敏感信息泄露:错误堆栈直接返回给前端、数据库密码写死在代码里、日志打印了完整的Token。这类漏洞不需要高明的攻击手段,翻一下响应体或源码就能发现。
- 文件上传漏洞:只校验扩展名不校验文件内容,攻击者把可执行脚本改名成
.jpg上传,再通过解析漏洞或路径穿越触发执行。上传点必须校验文件头、限制大小、重命名、存到独立存储,并按白名单放行后缀。 - 安全配置错误:默认账号密码不换、管理后台暴露在公网、CORS配置了
*通配且允许携带凭据。这类问题平时不显山不露水,一旦被利用,破坏力一点不比注入小。
这七类问题基本覆盖了一个Web项目最容易出事的攻击面。如果时间有限,优先把注入、XSS、CSRF、越权这四类处理掉,它们直接关系身份认证和数据安全,属于安全工作里性价比最高的部分。
2. 基于Session的安全校验:从Cookie到会话固定攻击
2.1 Session的工作机制:服务端才拥有真正的账本
Session方案可以理解成:服务端开一个保险柜,用户登录成功后,服务端把用户身份信息写进保险柜,分配一个凭证号也就是Session ID,通过Cookie发给浏览器。浏览器每次请求都把这个凭证号带回来,服务端拿凭证号开柜、确认身份、返回数据。
这里的关键点是:真正的用户身份数据始终存在服务端,客户端手里的Session ID只是一把钥匙。钥匙本身没有业务含义,被人捡到也不影响服务端账本的真实性,但拿到钥匙的人可以冒充用户开柜子,所以保护这把钥匙非常关键。
Session的典型生命周期是:登录时创建会话、写入用户信息、返回Session ID;请求时校验会话是否存在且未过期;退出或超时后销毁会话。现在的大型应用一般把Session放到集中式存储里,多实例部署时也能统一校验,避免每个实例各存一份导致登录状态漂移。
2.2 Cookie的四个安全属性:很多人只用了前两个
承载Session ID的Cookie可以配置很多属性,但我在代码评审里看到的情况是,大多数项目只设置了HttpOnly和Secure,另外两个关键属性经常被遗漏。四个方面全部到位才算及格:
HttpOnly:让JavaScript读不到Cookie。就算页面被注入了XSS脚本,攻击者也很难直接偷走Session ID,这是防会话劫持的第一道保险。Secure:只允许HTTPS下传输Cookie,防止明文网络被中间人截获。SameSite:控制跨站请求是否携带Cookie,取值Strict或Lax可以从浏览器层面阻断大部分CSRF。Domain和Path:限制Cookie发挥作用的域名和路径,避免Cookie在无关子域、无关路径里裸奔。
要说明的是,这四个属性并不是孤立生效的,它们之间是互相补位的关系。HttpOnly挡XSS偷钥匙,SameSite挡CSRF借钥匙,Secure挡链路窃听,Path和Domain限制钥匙的使用范围。四件套都到位,Session方案的安全底座才算真正立住。用响应头表达大概是下面这样:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=36002.3 Session固定攻击与会话劫持:两个绕不开的坑
基于Session的方案有两个经典攻击场景,一个是Session固定,一个是会话劫持。
Session固定攻击的本质是:用户登录前后Session ID没有变化。攻击者先通过某种手段给受害者种下一个已知的Session ID,受害者拿着它登录后,服务端没有重新生成ID,攻击者手里的旧ID依然有效,受害者身份就被接管了。解法很直接:用户登录成功、密码重置、权限等级变化时必须重新生成Session ID,让旧钥匙作废。
会话劫持则是攻击者拿到了合法用户的Session ID,可能通过XSS偷Cookie、网络嗅探或日志泄露等途径。拿到钥匙之后,攻击者就能冒充合法用户。除了前面说的HttpOnly、Secure属性,还应该关注会话绑定:把Session和IP、User-Agent、登录设备信息做关联,关键属性变化时要求重新认证。同时设置合理的过期时间,别让会话无限期存活,降低泄露后的可利用时长。
2.4 给Session方案打补丁的实操清单
如果项目还在用Session做身份校验,我建议按这份清单过一遍:
- 登录成功、密码重置、权限变更后,强制重新生成Session ID。
- Cookie开启四件套:HttpOnly、Secure、SameSite=Lax或Strict、限制Path。
- 会话设置空闲超时,例如30分钟无操作自动过期;绝对过期时间不要放在太久,按业务风险定。
- 登录时可选绑定User-Agent或设备指纹,变化时触发二次验证。
- 退出时必须同时销毁服务端会话和客户端Cookie。
- Session存储不要落在单机默认内存里,改用集中式存储,并做好读取超时和降级策略。
把这些补丁打上,Session方案的常见漏洞基本就堵住了。但如果你用的是多节点、前后端分离的架构,Session方案的存储同步问题很快会冒出来,这就引出下一部分的Token方案。
3. 基于Token的安全校验:无状态认证的一次演进
3.1 Token机制的核心思路:把校验信息交给客户端
Session方案最大的痛点是“有状态”:每个请求都要服务端查一次会话存储,分布式架构里还要处理存储同步和共享问题。Token方案换了思路——把身份信息和签名封装成一个凭证直接交给客户端保存,服务端不再保存会话,收到请求时只要验签通过、时间没过期,就认可这个凭证。
这个思路把存储压力从服务端转移到了客户端,服务端变得轻量、易于横向扩展。代价是服务端失去了主动吊销单个凭证的能力,因为凭证本身不在服务端手里。所以Token方案一般会设计成“短期的访问凭证加长期的刷新凭证”组合,访问凭证过期后由刷新凭证换取新的访问凭证,这样既维持了无状态特性,又控制了泄露风险窗口。
3.2 JWT的结构与验签过程
Token方案里最常用的载体是JWT,但要先说清楚,JWT不等于Token,它只是一种凭证的编码和签名格式。JWT由三部分组成:头部、载荷、签名,用点号连接。头部声明算法类型;载荷存放用户标识、过期时间等声明;签名由服务端用只有自己知道的密钥对前两部分计算生成。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJleHAiOjE3MDAwMDAwMDB9.签名部分验签过程可以类比核对防伪标识:服务端用密钥重新计算签名,和客户端带回来的签名比对,一致说明内容没被篡改,再检查过期时间等声明。额外强调一点,JWT里的载荷只是Base64编码,不是加密,任何人解码都能看到内容,所以手机号、身份证号这类敏感信息一律不要放进去。
把这个过程落到代码层面,用你熟悉语言的JWT标准实现就能组织出清晰的最小验签函数。注意验签函数里一定要显式指定允许的算法列表,防止攻击者把算法替换成不需要密钥的none或弱算法;过期时间的判断也要集中在函数内统一处理,别散落在业务代码各处。处理流程大概是下面这样:
# 伪代码:使用你所用语言的JWT标准实现 def verify_token(token): claims = jwt_decode(token, secret_key, algorithms=["HS256"]) if claims["exp"] < now(): raise TokenExpired user_id = claims["userId"] return user_id3.3 无状态的代价:Token泄密、吊销和刷新
Token方案最需要小心的是泄密后的处置。Session可以随时删掉服务端会话,让客户端钥匙失效;Token一旦签发,过期前一直有效,除非引入黑名单机制把它记下来,否则没有立刻踢下线的能力。
工程实践上通常做三层控制。第一,访问Token过期时间要短,15分钟到2小时比较常见,缩短泄露后的可利用窗口。第二,引入Refresh Token机制,刷新凭证有效期稍长,专门用来换取新的访问凭证。第三,Refresh Token要支持吊销,用户在密码修改、设备移除、账号异常时可以让刷新凭证失效。这个组合下,即使访问Token泄露,攻击者只有很短的利用窗口;即使刷新凭证泄露,也有吊销手段兜底。
另外,Token在前端的存放位置也值得认真对待。存在localStorage里容易被脚本偷走,存在Cookie里要配合HttpOnly和SameSite使用。没有绝对完美的方案,但至少要清楚每种选择背后的风险。
3.4 最小可运行的安全Token校验流程
用Token做接口认证,我建议至少走完下面五步:
- 客户端登录成功后拿到访问Token和Refresh Token。
- 客户端把Token放在请求头
Authorization: Bearer <token>里,不要放在URL参数里,避免被日志和浏览器历史记录泄露。 - 服务端收到请求先验签名,签名不过直接拒绝。
- 校验过期时间,过期后返回特定状态码,客户端携带Refresh Token去换新Token。
- 修改密码、解绑手机等敏感操作要求重新认证,并强制吊销相关Token。
这套流程跑通之后,接口的认证链路才算完整。但Token方案也不是银弹,真的到了选型环节,我一般按下一节的判断标准来定。
4. Session与Token并非二选一:选型边界与混合实践
4.1 两种机制的本质差异:存服务端还是信客户端
到底该用Session还是Token,很多团队能吵一下午。其实两者的差别用一张表就能看清:
| 维度 | Session | Token |
|---|---|---|
| 身份数据存放 | 服务端存储 | 客户端保存,服务端验签 |
| 服务端存储压力 | 有,大并发时需要共享存储 | 无,天然适合横向扩展 |
| 主动吊销能力 | 强,删会话即可踢下线 | 弱,需黑名单或短过期弥补 |
| 分布式友好度 | 需集中存储或粘性会话 | 友好,任何节点都可验签 |
| CSRF暴露面 | 高,Cookie自动携带 | 低,一般放在Header里 |
| 客户端读取难度 | 不可读,只有一串ID | 可解码,含部分业务信息 |
这张表背后其实是“存服务端”和“信客户端”的取舍。Session把控制权牢牢攥在服务端手里,换来的是存储与同步的成本;Token用密码学签名换取服务端无状态,代价是失去实时控制凭证的权力。没有绝对优劣,只有适不适合。
4.2 什么场景坚持Session,什么场景放心用Token
具体落到选型上,我自己的经验可以概括成三条:
- 传统服务端渲染项目、内部管理系统、对实时踢人有强要求的业务,优先用Session。这类场景里服务端控制力比横向扩展更值钱,Session的主动吊销能力几乎无可替代。
- 前后端分离、移动端App、第三方开放接口这类场景,Token是更顺手的方案。客户端和API天然解耦,多端共用一套认证体系时不必迁就浏览器Cookie机制。
- 如果团队对密码学不熟悉,也没有成熟的密钥管理和轮换机制,先别急着上Token。密钥保管、算法选择、历史Token兼容,每一件都是要命的细节,处理不好比用Session更容易翻车。
4.3 混合方案:不用反复争论,按场景拆
现实中很少有项目只用一种方案。我接手过一个项目,Web后台用Session,对外API用Token,中间加一层认证网关做统一校验。这个组合很务实:内部员工后台需要强管控、能踢人,用Session;外部第三方接入希望无状态、便于扩展,用Token;认证网关统一负责登录、签发、刷新和吊销,后续策略调整只改网关,不用动业务服务。
搭建这样的体系核心原则是“一个入口签发身份,各端口用自己的校验方式”。登录行为统一走认证服务,同时支持下发Session和Token两种凭证,业务服务按端口类型选择对应的校验中间件。这样既保持了鉴权灵活性,也没有牺牲安全性,团队内部也不用在一个方案上反复拉扯。
5. 安全校验之外的纵深防御:我给项目补的四道防线
5.1 输入与输出双端过滤
身份校验只解决“你是谁”的问题,真正要把项目做扎实,还得在输入输出两个方向上做过滤。输入方向包括参数类型校验、长度限制、白名单校验,凡是和数据库、系统命令打交道的字段,统一走参数化查询或安全API。输出方向则是所有动态渲染到HTML里的内容都要做编码转义。我一般在项目里设两层防线:框架默认转义兜底,危险接口显式编码覆盖。双端都守住,SQL注入和XSS的生存空间会小很多。
5.2 接口频率限制与账号保护
攻击者最讨厌的就是限速。登录接口、验证码接口、文件下载接口都建议做频率限制,防止暴力破解和资源耗尽。账号层面至少提供登录失败锁定、异地登录提醒、登录设备列表管理这类能力。这些机制看起来不起眼,但能在攻击真正得手前把风险挡在门外。做频率限制时注意别只限制单一IP,要结合账号维度一起限,否则攻击者换一批代理IP就绕过去了。
5.3 敏感操作的二次校验
对修改密码、解绑手机、批量导出数据、转账这类敏感操作,只靠Session或Token远远不够,必须叠加二次校验。做法不复杂:操作前让用户重新输入密码或验证码验证,验证通过后生成一个短时效、一次性、绑定该次操作的校验凭证,再放行后续操作。这一步能有效避免“用户终端被挟持后被借机操作”和“单纯依靠一次CSRF成功就完成敏感操作”这两类风险。
5.4 安全响应头与依赖治理
最后说点投入产出比很高的小事。给站点加上安全响应头,比如Content-Security-Policy、X-Content-Type-Options、Referrer-Policy,可以在浏览器层面进一步限制脚本和异常请求。依赖治理也别忘:定期升级有已知漏洞的三方库,很多漏洞的利用方式已经公开,被盯上是迟早的事。这两件事不需要太复杂的架构,定期做、认真做,能替项目挡掉不少低门槛攻击。
说到这儿,我想起当初刚开始做安全加固时,总是纠结“到底该用Session还是Token”,后来踩了好几个坑才明白:选型只是安全体系的一小环,真正决定安全水平的,是把常见隐患堵完、把校验链路补全,再养成做纵深防御的习惯。建议你也把上面这几条整理成一份自查清单,每新起一个项目就对照着过一遍,比临时抱佛脚强得多。