python

安全开发之 token 那些事

文章暂存

systemime
2021-03-24
8 min

摘要.

在开发网络应用时,不管是移动端的 APP 也好,还是 web 端 APP 也好,只要有用户群体存在,都绕不开身份认证这个话题,选择一种好的身份认证方法常常在应用安全中起到了至关重要的作用。目前用的最多的就是使用 “token” 认证用户的身份。
本文主要讲述了你不知道的关于 token 的那些事,以及在目前常见的应用中增加 token 认证的方案。
token 的作用
token 是什么?简单来说 token 就是在客户端与服务器之间传输的一段字符串。
说到 token 的作用,那这里不得不提一下 CSRF 攻击。CSRF 全称 “跨站请求伪造攻击”。假设一个用户登录一个银行网站,此时银行网站将用户的登录状态保存在了浏览器的 cookie 中,每当用户访问这个银行网站的不同页面时,浏览器会自动带上 cookie 中用户的登录状态,服务器以此来判断用户登录与否,并根据用户的登录状态响应不同的结果。
此时,攻击者写了一个恶意页面,内含一个指示银行网站从用户账号向攻击者账号转钱的请求,并诱使用户访问这个攻击者写的恶意页面。一旦用户访问了这个恶意页面,该恶意请求将自动带着
cookie
中用户的登录状态被发送到银行网站的服务器上,银行服务器认为这个请求是用户自己发出的,就执行了该请求,从用户的账号向攻击者的账号转了相应数额的钱,给用户造成了损失。
CSRF 攻击大体上就是这个样子,不过 CSRF 不是本文讲述的重点,有兴趣可以自行了解 CSRF 的细节部分。
token
就是用来区别请求是来自用户本身还是他人伪造的一个好办法。当用户在登录时,服务器生成一个 token 发送给客户端,客户端把这个 token
存在内存中或者本地,每次请求都带上这个 token,服务器接收到这个 token 并验证合法性,合法即继续执行请求,非法即拦截请求,不予执行。
由于浏览器的同源策略的限制,攻击者的页面无法跨域得到用户页面接收到的
token,所以攻击者的请求肯定是无法给出合法的 token 的(排除 token 被盗的可能,token
被盗不是本文讨论的范畴),由此服务器可以判断请求到底是用户自己发出的,还是以用户的名义被伪造发出的。从而防范 CSRF 攻击。
token 在开发中的实践
1、前后端混合开发
使用前后端混合开发模式是较为传统的开发模式。一般是后端写完功能让前端写样式,前后端共同维护着同一个页面。在这种应用中,session
会话就挑起了客户端与服务端通信的大旗。请求一般以 form 表单的形式发送给服务器。在这种应用中加上 token
进行身份验证常见的有两种方案。
方案一:服务端 token+ 表单页面 token
在用户输入正确的用户名和密码登录成功后,由服务器生成 token,一份存入 session 中,以 PHP 为例:
$_SESSION['token'] =generateToken();
一份存入页面中的表单,在页面上所有的表单中加入一个存放 token 的隐藏域:
<formaction=""method="POST">
...
<inputtype="hidden"name="token"value="tokenHere">
...

在表单提交上来时先检查接收到的 token 是否与 session 中的 token 相等,相等即可证明请求是来自用户自己,不相等则该请求很可能并非来自用户本身,很可能用户遭到了 CSRF 攻击。 **方案二:cookie 中 token+ 表单页面 token** 在用户登录成功后服务器生成 token,一份同上存入表单页面的隐藏域中,一份存入用户 cookie,以 PHP 为例如下: setcookie("token",generateToken(),time()+3600,'','','',true); 同样的,当服务端接收到请求时,比对 cookie 中的 token 和表单中的 token 是否相等,相等则合法,反之非法。这种方案的优势在于服务器端可以不存放用户的登录状态,节约了服务器的资源,也算是顺应了 http 的无状态。 2、前后端分离开发 使用前后端分离的开发模式是较为新颖的开发模式。这种开发模式一般是前端与后端先协商好一份 Restful API 文档,标明请求的地址、格式、类型等,然后各自对照着这份 api 文档同时进行开发,提升了效率。这种开发模式在目前流行的单页应用(SPA)中使用较多。在这种应用中可以不使用 session 会话来维持客户端与服务器的通信。转而只用 JWT(Json Web Token)来实现身份认证。 [https://jwt.io/introduction/](https://link.zhihu.com/?target=https%3A//jwt.io/introduction/) 单页应用为了维护其良好的用户体验,发送请求的方式由传统的 form 表单提交改为了使用 AJAX/Fetch 传输数据,实现页面无刷。用户在登录成功接收到 token 后可以将 token 存在内存中,也就是可以存在一个 JS 全局变量里,也可以存在 LocalStorage 中,唯一的区别是后者可以实现自动登录而前者不可以。每次发送请求时将 base64 编码后的 token 添加到 header 里的 Authorization 中发送给服务器: //ajax $.ajax({ type: 'POST', url: '/api/datapost', data: { title: '...', content: '...' }, headers: { Authorization: 'dG9rZW5IZXJl' }, success: function(res) { do(res); } })

//fetch
fetch('/api/datapost',{
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": 'dG9rZW5IZXJl'
},
body: JSON.stringify(
{
title: '...',
content: '...'
}
)
}).then(res=>do(res))
.catch(e=>handle(e))

服务器解密验证 headers 中的 JWT,得到身份数据。整个流程如下:

大家可以想想为什么前两种方案都需要验证两个 token 是否相等来判断 token 的合法性而这里不需要。
这是因为攻击者如果要利用
CSRF,构造一个包含恶意请求的页面,无论 GET 还是 POST 还是别的请求类型,由于同源策略的限制,请求只能由构造 form
表单发出,AJAX
是不支持跨域发送请求的(除非服务器开启跨域支持,如果服务器开启跨域,开发者需要严格限制请求的来源,对不信任的来源不予响应),而通过表单发送的请求是没法添加自定义的
header 头的,也就是说攻击者是发不出 header 中带有 token 的请求,所以我们可以以此来进行身份认证。
这种方案的优势在于服务器保持无状态,不需要维持用户的登录状态,给服务器节约了资源。而且在一些无法使用 cookie 的场景下也适用。
token 的生成方法
其实
csrf token 就是一段随机值而已,它的实现方法因人而异,不同的公司可能有不同的标准,可以使用标准的 JWT
格式,也可以是内部约定的实现方法,但总的来说要满足随机性,不能轻易被别人预测到;字符数不能太长也不能太短,太短容易被碰撞出来,太长给传输带来不便,耗费资源影响速度。下面分别以
PHP 和 JAVA 为例
PHP:使用 uniqid() 方法生成随机值,开启第二个参数增加一个熵,使生成的结果更具唯一性,应对高并发
functiongenerateToken() {
returnmd5(uniqid(microtime() . mt_rand(),true));
}
JAVA: 使用 java.util 包下面的 UUID 类中的 randomUUID() 方法
publicstaticStringgenerateToken()
{
returnUUID.randomUUID().toString().replace("-", "");
}
当然了,具体的实现方案还要按照各自的业务需求来,这里只是介绍了几种常见的 token 方案仅供参考。
转自公众号:信安之路 https://zhuanlan.zhihu.com/p/40588156 https://zhuanlan.zhihu.com/p/40588156

上次编辑于: 5/20/2021, 7:26:49 AM