Cookie
介绍
cookie是一个保存在客户机中的简单的文本文件,用来记录客户状态,作用相当于一个身份证。
假如用户A登录进入www.taobao.com
,看到一个喜欢的衣服,点击进入www.taobao.com/clothe
,请思考进入该页面需不需要登录?如果不需要登录服务器是怎么知道是A在浏览该衣服,而不是B?如果需要登录那么每次页面跳转都需要输入账号密码是不是太麻烦了。cookie就是为了解决这一问题而出现的。
客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认是哪一个用户。
结构
Cookie是一段不超过4KB的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成:
Name/Value:设置Cookie的名称及相对应的值。Value值包括Web服务器所提供的访问令牌
Expires属性:设置Cookie的生存期。
有两种存储类型的Cookie:会话性与持久性。Expires属性缺省时,为会话性Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效
Path属性:定义了Web站点上可以访问该Cookie的目录。
Domain属性:指定了可以访问该 Cookie 的 Web 站点或域。
Secure属性:指定是否使用HTTPS]安全协议发送Cookie。
HTTPOnly 属性 :用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。
查看chrome浏览器的cookie:
打开一个网页(以百度为例),在控制台输入alert(document.cookie)
:
实例
登录页面:
//login.php
<?php
//设置编码格式防止乱码
header('Content-type:text/html;charset=utf-8');
if (isset($_POST['submit'])){
if ($_POST['username']==='admin' && $_POST['password']==='123456'){
if (setcookie('username',$_POST['username'],time()+120)){
//跳转到成功页面
header('Location:index.php');
}else{
echo 'cookie设置失败';
}
}else{
echo '用户名或密码错误';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form method="post" action="login.php">
账号:<input type="text" name="username" ></br>
密码:<input type="password" name="password"></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
登录成功页面:
//index.php
<?php
//设置编码格式防止乱码
header('Content-type:text/html;charset=utf-8');
if (isset($_COOKIE['username']) && $_COOKIE['username']==='admin') {
echo '欢迎';
}else{
echo '<a href="login.php">请登录</a>';
}
?>
两个页面的逻辑是:必须在login.php页面输入正确的账号密码才能访问到index.php,否则会提醒请登录:
使用正确的账号密码登录,查看请求和返回数据包:
可以发现返回数据包中有Set-Cookie:
字段,这说明服务器已经给了客户端一个cookie,而且有效时间是120秒。
接下来使用浏览器不登陆直接访问index.php:
请求数据包中带有刚才得到的cookie,直接登录上了,服务器通过cookie验证了我们的身份
cookie安全性
- 客户端脚本可以通过document.cookie属性访问Cookie,所以攻击者可以利用攻击脚本来获取用户的cookie,并发送到自己的服务器,如果cookie有效,攻击者就能够直接使用cookie登录
- 存在csrf攻击
Session
介绍
session是基于cookie实现的,与cookie一样,session也是一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
每个用户访问第一次服务器时会自动为其分配一个特定的sessionid,保存在cookie中。服务器同时也把sessionid和对应的用户信息、用户操作记录在服务器上,这些记录就是session。用户再次访问时会将sessionid带入cookie发送给服务器,服务器从cookie里找到sessionid,再根据sessionid找到session,这样服务器就可以知道该用户之前操控过什么、访问过哪里。
实例
登录页面:
//login.php
<?php
//设置编码格式防止乱码
header('Content-type:text/html;charset=utf-8');
if (isset($_POST['submit'])){
if ($_POST['username']==='admin' && $_POST['password']==='123456'){
session_start();
$_SESSION["username"] = true;
header('Location:index.php');
}else{
echo '用户名或密码错误';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form method="post" action="login.php">
账号:<input type="text" name="username" ></br>
密码:<input type="password" name="password"></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
登录成功页面:
//index.php
<?php
//设置编码格式防止乱码
header('Content-type:text/html;charset=utf-8');
session_start();
if(isset($_SESSION["username"]) && $_SESSION["username"] === true) {
echo '欢迎';
}else{
echo '<a href="login.php">请登录</a>';
}
?>
两个页面的逻辑是:必须在login.php页面输入正确的账号密码才能访问到index.php,否则会提醒请登录。
使用正确的账号密码登录:
返回数据包里面有Set-Cookie: PHPSESSID=
,因为是第一次请求,所以获得了sessionid
我们使用这个sessionid来登录:
成功登录!
如果是一个无效的session,就不能通过验证:
session安全性
- session是存储在服务器端的,相对来说安全一些。但是sessionid存储客户端,有被盗取截获的可能
- 存在csrf攻击
token
介绍
token的意思是“令牌”,是用户身份的验证方式。听起来好像跟cookie和session功能一样,为什么要引入token呢?
cookie和session的不足:
- cookie和session认证需要在同一主域名下才可以进行认证
- session保存在服务器中,在分布式服务器系统中,不会所有的服务器都保存有同一个用户的session
- session机制服务器会频繁访问数据库来查询session,负载较大
token即为令牌,是服务器生成的一串字符串,作为客户端向服务器进行请求的“通行证”。在客户端进行初次登陆后由服务器返回,之后的每次请求只需要携带token进行请求即可,而无需携带密码等敏感信息
结构:(以JWT token为例,JWT是token机制的一种实现方式,我是这样理解的,如有不对请指点一下)
第一部分(头部/header),是用来存放声明信息的:
{ "typ": "JWT", // 这里声明了类型,即JWT "alg": "HS256" // 这里声明了加密算法,即HS256加密算法 }
将header进行base64编码就得到第一部分
第二部分(载荷/payload),用来存放有效信息的:
{ exp, // 过期时间,这个过期时间必须要大于签发时间 iat, // 签发时间 [data,] // 如果你想的话,可以塞一些非敏感信息 [iss,] // 签发者 [sub,] // 面向的用户 [aud,] // 接收的一方 [nbf,] // 定义一个时间,即在该时间之前,这个jwt是不可用状态 [jti] // 唯一身份标识,主要用来作为一次性token,从而回避重放攻击 }
将payload进行base64编码就得到第二部分
第三部分(签证/signatrue):
“第一部分的base64编码
.
第二部分的base64编码.
密钥”,但后对这个字符串按照第一部分声明的加密算法来加密,得到的字符串就作为签名。
最后将三部分以.
连接起来,就得到jwt了
用户想要访问受保护的路由或资源时,浏览器应向服务器发送JWT,通常放在header中。header的内容应该如下所示:
Authorization: Bearer <token>
服务器收到JWT后,结合密钥(密钥只有服务器自己知道)对其签名进行验证,如果验证通过即可确认用户身份,反之则不能。
验证JWT的网站:https://jwt.io/
实例
python比较容易实现JWT,所以我这里使用python:
import jwt
payload = {
"username": "admin"
}
secretCode = "misspower007"
token = jwt.encode(payload, secretCode, algorithm="HS256")
print(token)
结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.HMbOX1eXkn1K-8l9AFauqhP_wxRr-6AISae-Oo2jc4g
如果密钥不对,就不能通过验证:
安全性
token可以有效防止csrf攻击,我结合pikachu的csrf(token)来讲一下。
登录进去,修改资料,然后抓包:
数据包:
可以看到数据包中出现token,它的值是无规律的,攻击者很难猜测,所以就无法构成csrf攻击。
再来看一下相关源码:
<p class="per_name">姓名:{$name}</p>
<p class="per_sex">性别:<input type="text" name="sex" value="{$sex}"/></p>
<p class="per_phone">手机:<input class="phonenum" type="text" name="phonenum" value="{$phonenum}"/></p>
<p class="per_add">住址:<input class="add" type="text" name="add" value="{$add}"/></p>
<p class="per_email">邮箱:<input class="email" type="text" name="email" value="{$email}"/></p>
<input type="hidden" name="token" value="{$_SESSION['token']}" />
最后一行有一个隐藏表单,内容是token
,值是$_SESSION['token']
,不难看出该页面生成的时候token的值就已经确定了,然后提交表单的时候会自动发送这个token。
再来看一下后端是如何检测token的:
if(isset($_GET['submit'])){
if($_GET['sex']!=null && $_GET['phonenum']!=null && $_GET['add']!=null && $_GET['email']!=null && $_GET['token']==$_SESSION['token']){
//转义
$getdata=escape($link, $_GET);
$query="update member set sex='{$getdata['sex']}',phonenum='{$getdata['phonenum']}',address='{$getdata['add']}',email='{$getdata['email']}' where username='{$_SESSION['csrf']['username']}'";
$result=execute($link, $query);
//没有修改,点击提交,也算修改成功
if(mysqli_affected_rows($link)==1 || mysqli_affected_rows($link)==0){
header("location:token_get.php");
}else {
$html1.="<p>修改失败,请重新登录</p>";
}
}
}
关键点在于$_GET['token']==$_SESSION['token']
,这里判断了token的值和服务器存储的$_SESSION['token']
是否相等,如果不相等就不能修改用户的资料。
JWT攻击
信息泄露
由于JWT中payload是使用base64加密的,如果其中存在敏感信息就会发送信息泄露
算法修改攻击
JWT的header部分中,有签名算法标识alg
,alg
决定了用于签名的算法,算法修改攻击就是修改alg
来造成身份伪造
将签名算法改为none:
头部中的alg
字段可以改为none,即不使用签名算法。当alg
字段为空时,后端将不执行签名验证。
将alg字段改为none后,系统就会从JWT中删除相应的签名数据(这时,JWT就会只含有头部 + ‘.’ + 有效载荷 + ‘.’),然后将其提交给服务器。
我们只需将有效载荷修改为其他用户的信息,就能伪造成其他用户
将RS256算法改为HS256:
本质是将 非对称密码算法 转换为 对称密码算法
RS256算法使用私钥对消息进行签名并使用公钥进行身份验证。
如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。
由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。
这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。
参考文章
https://www.jianshu.com/p/6fc9cea6daa2
https://blog.csdn.net/weixin_45393094/article/details/104747360