认证授权相关概念与方法实现
本文最后更新于 2025年9月18日 晚上
网络安全也是如今讨论的很多的话题,总的来说,与网络安全相关技术的动机和方法都是比较易懂的,但是由于实质的算法加密方面通常会涉及到比较复杂的数学运算,比如链式运算等,所以在实现的角度来说通常会涉及到很多严谨的证明,所以在学术上是十分困难的一件事。
本章主要讲一下如今比较流行的几个认证授权的相关概念与技术。
认证与授权
很简单的概念:
认证Authentication:你是不是你?
授权Authorization:你能做什么?
对这两个词有混淆的可能是因为两个英文单词很像,那就用中文记就行了。
直觉告诉我们,授权肯定是发生在认证通过之后的,毕竟程序在没有确定你是谁的情况下,也无法确定给了你什么权限。
RBAC模型
这是一种认证与授权的数据设计模型,全称为Role-Based Access Control,翻译过来就是以角色为基础的访问控制。它描述的是一种用户-角色-权限之间的授权模型:
graph LR
1[用户] --1, *--> 2[角色]
2 --*, 1--> 1
2 --1, *--> 3[权限]
3 --*, 1--> 2
- 一个用户可以有多个角色,一个角色可以被多个用户持有
- 一个角色可以有多个权限,一个权限可以被多个角色持有
有了这个模型的概念,便能够很方便地在数据库中管理和创建想要的授权与身份数据。
基本概念讲完了,那么可以开始谈谈具体怎么实现认证与授权。
首先,认证与授权(接下来我就简化成AA了)是一个前端(客户端)向后端(服务器)发送请求时第一个要做的事,服务器要做到AA操作,前端至少需要通过某种途径把自己的身份信息发送到后端。而如今用得最多的方法就是围绕这个概念展开的。
Cookie - Session
相信很多人在面试的时候都会被问到与这个东西相关的问题,这就是一个AA问题。这里先贴一个使用这个机制来进行AA操作的流程:
sequenceDiagram
participant Client
participant Server
Client ->> Server: POST /user/login
Server ->> Client: Cookie, SessionId
Client ->> Server: Cookie, SessionId
Server ->> Client: Response
这里描述了一个最基本的流程,Client尝试登录时向Server发送登录消息,其中包含了自己的用户名和密码登信息。Server通过接收到的消息来认证Client的身份,身份通过后,生成一个Session对象,也会生成一个Cookie,其中包含了一个SessionId,发送给Client。同时Server也将这个SessionId与该用户对应的Session对象保存(先不管怎么存)。Client接受到这个包含了SessionId的Cookie后,将其保存在本地。往后在发送消息给服务器时,会将这个Cookie一并发送,而Server端在接受消息时会根据Cookie中的SessionId来获取该Client之前的状态,以返回需要的结果。
再此之上,有许多额外的内容可以讲:
- Session需要设置有效时长,服务器肯定不能让一个session存在的时间过长,这即不安全,又容易造成存储压力过大。
- 在分布式情况下,为了保证高可用,需要对总体流程做一些增强。以下是一些有效的方案:
- 存Redis里,Redis由集群自动部署,session也会被自动管理
- 存标准数据库里,由于对数据库的访问本就是分布式的,所以在处理数据库的高可用问题时,session的高可用问题也就一并处理了
- 每存一个session,服务器都会通知其他服务器做相同的事情,让这个session总是全局有效的。当然了,这个方法挺蠢。
- 做Gossip或Broadcast传播,当一个服务器发现自己有对应session时就返回给原服务器,随后再返回给客户端。(直接返回给客户端也可以)
问题:
不够安全,由于在后续发送消息时需要同步将cookie一并发送,因此其他人能够很轻易地截取你的请求并获取你的cookie,随后用你的身份做事。
不过后面又提出了一个Token的解决方案,那个由于不会直接随着请求一并发送,所以相对更加安全。
Token并不会一定比cookie方法更安全,这依然是取决于场景。他们两个的诞生主要是由于他们的特性不同带来的性能差异。
session通常可以是一个由服务器随机生成的一个数,其本身并不代表任何意思,不保存任何信息,因此服务器方可以随时将其伸出并重新生成一个新的,以规避更进一步的损失。
token通常是一个无状态的信息,它本身会携带用户的相关信息,因此服务器要做的知识解析这个token然后进行下一步操作,不需要进行数据库操作。
综上,明显可以看出,session比起token更加不灵活,但是在止损与控制权逻辑上,session是更有优势的。
JWT
JWT, Json Web Token,是一种基于token的跨域认证解决方案。
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:
- Header(头部) : 描述 JWT 的元数据,定义了生成签名的算法以及
Token的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。 - Payload(载荷) : 用来存放实际需要传递的数据,包含声明(Claims),如
sub(subject,主题)、jti(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。 - Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。
JWT生成后发送到客户端,客户端会将其存放在浏览器的localStorage中而不是cookie中,这样就可以有效避免CSRF问题。
Token与Session的区别
在学习过程中,我一直没有搞懂为什么token能够避免CSRF问题,因为不论是使用token还是session,在发送消息时都会将所需内容包括在消息中,不论是cookie还是token。因此,窃取方最终都会接受到cookie和token。那么,只要窃取方用同样的方式向服务器发送请求,不是会出现一样的问题吗?
问了一下deepseek后,让它帮我做了一下总体的回顾与解答。看到回答后,我发现是我自己对LocalStorage这个概念的理解有误(真是没有学过前端的知识…)。
DeepSeek回答的内容如下:
第一部分:正常请求流程
A. Cookie-Session 流程
- 登录认证
- 用户:提交用户名和密码。
- 服务器:验证凭据。通过后,在服务器端(如Redis)创建一个Session对象存储用户信息,并生成一个唯一的Session ID。
- 响应:服务器通过
Set-Cookie响应头,将Session ID放入Cookie(通常设置HttpOnly; Secure; SameSite=Lax)返回给浏览器。
- 存储
- 浏览器:接收到
Set-Cookie指令后,自动将Session ID保存在当前域的Cookie存储中。
- 浏览器:接收到
- 后续请求
- 浏览器:每当用户向同域的服务器发送请求时,浏览器会自动、无声地将Cookie(包含Session ID)附加到HTTP请求的
Cookie头中。 - 服务器:从
Cookie头中提取Session ID,去Session存储中查找对应的Session数据。如果找到且有效,则认为用户已认证。
- 浏览器:每当用户向同域的服务器发送请求时,浏览器会自动、无声地将Cookie(包含Session ID)附加到HTTP请求的
- 登出
- 用户:点击退出。
- 服务器:在Session存储中删除对应的Session记录。
- 响应:通知浏览器清除Cookie(通过设置一个过期的同名Cookie)。
B. Token (JWT) 流程
- 登录认证
- 用户:提交用户名和密码。
- 服务器:验证凭据。通过后,使用密钥生成一个JWT(其中包含用户ID等信息),但服务器端不保存这个JWT。
- 响应:服务器在JSON响应体中直接将JWT字符串返回给前端。
- 存储
- 前端 (JavaScript):从登录API的响应中获取JWT字符串,然后使用
localStorage.setItem('token', jwt)将其保存在浏览器的LocalStorage中。
- 前端 (JavaScript):从登录API的响应中获取JWT字符串,然后使用
- 后续请求
- 前端 (JavaScript):在调用需要认证的API前,必须手动编写代码:从LocalStorage中取出JWT,并将其添加到HTTP请求的
Authorization头中(例如:Authorization: Bearer <jwt>)。 - 服务器:从
Authorization头中提取JWT,验证其签名是否有效、是否过期。验证通过后,直接从JWT的Payload中读取用户信息。
- 前端 (JavaScript):在调用需要认证的API前,必须手动编写代码:从LocalStorage中取出JWT,并将其添加到HTTP请求的
- 登出
- 前端:直接从LocalStorage中移除JWT即可。但服务器无法主动让一个JWT失效,除非使用黑名单等额外手段。
第二部分:CSRF攻击下的表现
CSRF攻击原理:攻击者诱导用户访问一个恶意网站,该网站会自动向目标网站(用户已登录的)发起一个请求。浏览器会自动携带目标网站的Cookie。
A. Cookie-Session:❌ 极度脆弱
- 攻击发生:用户已登录银行网站(Session有效),然后访问了恶意网站。恶意网站中有一个隐藏表单:
<form action="https://bank.com/transfer" method="POST">...。 - 浏览器行为:用户触发这个表单提交后,浏览器会向
bank.com发起请求,并自动将银行的Cookie(包含Session ID)附加到请求中。 - 服务器验证:服务器收到请求,看到有一个合法的Session ID,便认为是用户本人的合法操作,执行转账。
- 结果:攻击成功。
B. Token (in LocalStorage):✅ 天然免疫
- 攻击发生:同样场景,恶意网站试图伪造一个向
bank.com的转账请求。 - 浏览器行为:浏览器会发送这个请求,但是! 恶意网站的脚本无法访问
bank.com的LocalStorage,因此无法将JWT放到请求的Authorization头中。请求头里不会有Token。 - 服务器验证:服务器收到请求,发现没有
Authorization头或者Token无效。 - 结果:攻击失败。请求被拒绝。
第三部分:XSS攻击下的表现
XSS攻击原理:攻击者向网站注入恶意脚本,当其他用户访问该网站时,脚本运行,可以窃取用户数据。
A. Cookie-Session (with HttpOnly):✅ 相对安全
- 攻击发生:网站存在XSS漏洞,攻击者注入的脚本在用户浏览器上执行。
- 窃取尝试:脚本尝试执行
document.cookie来窃取Cookie。 - 浏览器行为:如果Cookie设置了
HttpOnly属性(这是最佳实践),那么JavaScript将无法访问这个Cookie。攻击者什么也读不到。 - 结果:攻击受阻。Session ID没有被盗,账户安全。
B. Token (in LocalStorage):❌ 极度脆弱
- 攻击发生:同样,恶意脚本在用户浏览器上执行。
- 窃取尝试:脚本执行
const token = localStorage.getItem('token');。 - 浏览器行为:LocalStorage完全对JS可见,脚本能轻松读取到完整的JWT。
- 结果:攻击成功。攻击者拿到了用户的全部身份凭证(Token)。他可以用这个Token在任何地方冒充用户,直到Token过期。危害极大。
所以总的来说,token存在localStorage中,这个LocalStorage是存在目标域名(后面写成real.com)下的内容,因此,即便窃取者试图用一个伪造的域名(fake.com)让你发送消息,它也不是real.com,它不论如何都不可能得到real.com的LocalStorage。因此,CSRF攻击对token根本就是无效的。
我又追问了一次,很凑巧的,似乎我的担忧与正常的XSS攻击是同一个道理:如果窃取者在real.com中创建了一个误导,或者是窃取用的JavaScript代码(另言之,窃取用的假域名),然后你又点了进去。此时,由于LocalStorage可以被JS爬取,而同时你正在real.com下,因此窃取者完全可以窃取你的token。