HTTP协议本身是”无状态”的,在一次请求和下一次请求之间没有任何状态保持,服务器无法识别来自同一用户的连续请求。有了cookie和session、token,服务器就可以利用它们记录客户端的访问状态了,这样用户就不用在每次访问不同页面都需要登录了,也叫单点登录。
1.cookie
cookie的应用场景及缺点
cookie是一种数据存储技术, 它是将一段文本保存在客户端(浏览器或本地电脑)的一种技术,并且可以长时间的保存。当用户首次通过客户端访问服务器时,web服务器会发送给客户端的一小段信息。客户端浏览器会将这段信息以cookie形式保存在本地某个目录下的文件内。当客户端下次再发送请求时会自动将cookie也发送到服务器端,这样服务器端通过查验cookie内容就知道该客户端早访问过了。
cookie的常见应用场景包括:
- 判断用户是否已经登录
- 记录用户登录信息(比如用户名,上次登录时间)
- 记录用户搜索关键词
ookie的缺点在于其并不可靠
和不安全
,主要原因如下:
- 浏览器不一定会保存服务器发来的cookie,用户可以通过设置选择是否保存cookie。
- cookie是有生命周期的(通过Expire设置),如果超过周期,cookie就会被清除。
- HTTP数据通过明文发送,容易受到攻击,因此不能在cookie中存放敏感信息(比如信用卡号,密码等)。
- cookie以文件形式存储在客户端,用户可以随意修改的。
Django中使用cookies
设置cookies(保存数据到客户端)
response.set_cookie(key,value,expires)
key : cookie的名称
value : 保存的cookie的值
expires : 保存的时间,以秒为单位
例子: response.set_cookie('username','John',60*60*24)
一般在Django的视图中先生成不含cookie的response,然后set_cookie, 最后把response返回给客户端(浏览器)。
下面是3个设置cookie的例子:
例子1、不使用模板
response = HttpResponse("hello world")
response.set_cookie(key,value,expires)
return response
例子2、使用模板
response = render(request,'xxx.html', context)
response.set_cookie(key,value,expires)
return response
例子3、重定向
response = HttpResponseRedirect('/login/')
response.set_cookie(key,value,expires)
return response
获取cookies,获取用户发来请求中的cookies
request.COOKIES['username']
request.COOKIES.get('username')
检查cookies是否已经存在
request.COOKIES.has_key('<cookie_name>')
删除cookies
response.delete_cookie('username')
django中使用场景简介:
通常来说,有些功能的实现需要通过认证。
举个例子,如用户点击某一块链接时,如果需要先登录认证的,这个请求会以post方式请求后台资源,后台接收到post请求后,先get用户请求的cookies,看cookies是否有相应信息,如果有,后台经过处理后返回响应数据。
如果没有,这个时候应该做的逻辑就是跳转或重定向到login界面,通过用户再次post过来的数据validate后,后台先到数据库里查找有没这个用户,如果有,接下来应该为用户设置cookies后返回数据。
之后还有请求过来,重复上面操作,即有cookies处理响应,无cookies认证后再响应重定向。
2.session
session及session的工作原理
session又名会话,其功能与应用场景与cookie类似,用来存储少量的数据或信息。但由于数据存储在服务器上,而不是客户端上,所以比cookie更安全。
Session工作的流程如下:
- 客户端向服务器发送请求时,看本地是否有cookie文件。如果有,就在HTTP的请求头(Request Headers)中,包含一行cookie信息。
- 服务器接收到请求后,根据cookie信息,得到sessionId,根据sessionId找到对应的session,用这个session就能判断出用户是否登录等等。
使用Session的好处在于,即使用户关闭了浏览器,session仍将保持到会话过期。
django使用session
设置session的值
request.session['key'] = value
request.session.set_expiry(time):设置过期时间,0表示浏览器关闭则失效
获取session的值
request.session.get('key',None)
删除 session 的值
del request.session['key']
判断是否在session里
'fav_color' in request.session
获取所有session的key和value
request.session.keys()
request.session.values()
request.session.items()
settings.py 有关session的设置
1、SESSION_COOKIE_AGE = 60 * 30
2、SESSION_EXPIRE_AT_BROWSER_CLOSE = True
django中使用场景简介:
参照cookie认证,发送请求到后台先看请求get session信息,如果都没有则重定向login,然后经过validate后查看数据库有没该用户,没有就提示注册,有的话因为这次登陆没有携带session,后台通过request设置生成session后处理返回数据。
当然,也可以有其他的处理思路,后台收到请求时可以查看后台缓存有没有相应数据,有的话就继续下步操作,没有自然login,然后设置生成session,缓存也可以保存一份,下次ttl内访问直接处理下步请求。
另外,通过在session中设置其他字段也可以完成一些额外的功能,如多次评论限制问题的实现。
3.token
在实现登录功能的时候,正常的B/S应用都会使用cookie+session的方式来做身份验证,后台直接向cookie中写数据,但是由于移动端的存在,移动端是没有cookie机制的,所以使用token可以实现移动端和客户端的token通信。
验证流程
- 整个基于Token的验证流程如下:
- 客户端使用用户名跟密码请求登录
- 服务器收到请求,去验证用户名和密码
- 验证成功后,服务端会签发一个Token,再把这个Token发送到客户端
- 客户端收到的Token以后可以把它存储起来,比如放在Cookie或LocalStorage里
- 客户端每次向服务器发送其他请求的时候都要带着服务器签发的Token
- 服务器收到请求,去验证客户端请求里面带着的Token,如果验证成功,就像客户端返回请求的数据
JWT
构造Token的方法挺多的,可以说只要是客户端和服务器端约定好了格式,是想怎么写就怎么写的,然而还有一些标准写法,例如JWT表示:JSON Web Tokens。
JWT标准的Token有三个部分:
- header
- payload
- signature
三个部分会用点分割开,并且都会使用Base64编码, Header.Payload.Signature, 所以真正的Token看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
header部分主要是两部分内容,一个是Token的类型,另一个是使用的算法,比如下面的类型就是JWT,使用的算法是HS256:
{
"typ": "JWT",
"alg": "HS256"
}
上面的内容要用 Base64 的形式编码一下,所以就变成这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:
iss:Issuer,发行者
sub:Subject,主题
aud:Audience,观众
exp:Expiration time,过期时间
nbf:Not before
iat:Issued at,发行时间
jti:JWT ID
Signature
JWT的最后一部分是Signature,这部分相当于前两段的摘要,用来防止其他人来篡改Token中的信息,在处理时可以首先将前两段生成的内容使用Base64生成一下再加盐然后利用MD5等摘要算法在生成一遍。
服务端生成Token
在服务端生成Token的时候,需要解决两个问题:
- 使用什么加密算法
- Token如何存储
加密算法
这里的加密算法并不是MD5,SHA1这样的哈希算法,因为这种算法是无法解密的,只能用来生成摘要,在Django中内置了一个加密前面模块django.core.signing模块,可以用来加密和解密任何数据,使用签名模块的dumps和load函数来实现。
Token如何存储
用什么存储
在服务器中Token可以存储在内存中,因为本质是字符串,所以并不会占用很大的内存空间,如果是分布式的存储可以将所有的token信息分段存储在不同的服务器中,也可以存储在数据库中,在Django中提供了缓存类,可以用来存储Token,Django的缓存可以结合Redis来使用,可以借助django-redis来实现。
配置
为了使用django-redis,需要将django cache setting修改,修改settings.py,默认是没有Cache的配置信息的,在其中添加:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
如何存
由于redis是使用k-v模式来进行存储数据的,我们可以使用用户名作为key,而token信息作为value,相较于直接使用token作为key的方式,好处是我们可以使用更少的空间实现一些功能,例如当用户修改了密码或点击注销之后,它的token可以直接失效,直接将该用户名所对应的数据删除就好了,或者用户在一次登录成功后,又一次请求了登录接口,我们可以很简单的更新该用户的token信息,而这样存储所依赖于我们的token可以进行解密,如果你是直接生成了一串无法解密的数据作为token,不能使用用户名作为token了。
代码演示
import time
from django.core import signing
import hashlib
from django.core.cache import cache
HEADER = {'typ': 'JWP', 'alg': 'default'}
KEY = 'CHEN_FENG_YAO'
SALT = 'www.lanou3g.com'
TIME_OUT = 30 * 60 # 30min
def encrypt(obj):
"""加密"""
value = signing.dumps(obj, key=KEY, salt=SALT)
value = signing.b64_encode(value.encode()).decode()
return value
def decrypt(src):
"""解密"""
src = signing.b64_decode(src.encode()).decode()
raw = signing.loads(src, key=KEY, salt=SALT)
print(type(raw))
return raw
def create_token(username):
"""生成token信息"""
# 1. 加密头信息
header = encrypt(HEADER)
# 2. 构造Payload
payload = {"username": username, "iat": time.time()}
payload = encrypt(payload)
# 3. 生成签名
md5 = hashlib.md5()
md5.update(("%s.%s" % (header, payload)).encode())
signature = md5.hexdigest()
token = "%s.%s.%s" % (header, payload, signature)
# 存储到缓存中
cache.set(username, token, TIME_OUT)
return token
def get_payload(token):
payload = str(token).split('.')[1]
payload = decrypt(payload)
return payload
# 通过token获取用户名
def get_username(token):
payload = get_payload(token)
return payload['username']
pass
def check_token(token):
username = get_username(token)
last_token = cache.get(username)
if last_token:
return last_token == token
return False
pyjwt
当然python的库很强大,不用自己造轮子,直接用现成的模块即可,其中pyjwt可以考虑。
安装模块:
pip install pyjwt
利用 PyJWT 生成 Token:
>>> import jwt
>>> encoded_jwt = jwt.encode({'username':'运维咖啡吧','site':'https://ops-coffee.cn'},'secret_key',algorithm='HS256')
这里传了三部分内容给 JWT:
- 第一部分是一个 Json 对象,称为 Payload,主要用来存放有效的信息,例如用户名,过期时间等等所有你想要传递的信息
- 第二部分是一个秘钥字串,这个秘钥主要用在下文 Signature 签名中,服务端用来校验 Token 合法性,这个秘钥只有服务端知道,不能泄露
- 第三部分指定了 Signature 签名的算法
JWT 是不加密的,任何人都可以读的到其中的信息,其中第一部分 Header 和第二部分 Payload 只是对原始输入的信息转成了 base64 编码,第三部分 Signature 是用 header+payload+secret_key 进行加密的结果。
因为 JWT 不会对结果进行加密,所以不要保存敏感信息在 Header 或者 Payload 中,服务端也主要依靠最后的 Signature 来验证 Token 是否有效以及有无被篡改。
服务端在有秘钥的情况下可以直接对 JWT 生成的 Token 进行解密,解密成功说明 Token 正确,且数据没有被篡改。
当然我们前文说了 JWT 并没有对数据进行加密,如果没有 secret_key 也可以直接获取到 Payload 里边的数据,只是缺少了签名算法无法验证数据是否准确,pyjwt 也提供了直接获取 Payload 数据的方法,如下:
>>> jwt.decode(encoded_jwt, verify=False)
{'username': '运维咖啡吧', 'site': 'https://ops-coffee.cn'}
下面看看在具体的django项目中是可以怎样实现生成用户token的方法。
通过给 User model 添加一个 token 的静态方法来处理
class User(AbstractBaseUser, PermissionsMixin):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
username = models.EmailField(max_length=255, unique=True, verbose_name='用户名')
fullname = models.CharField(max_length=64, null=True, verbose_name='中文名')
phonenumber = models.CharField(max_length=16, null=True, unique=True, verbose_name='电话')
is_active = models.BooleanField(default=True, verbose_name='激活状态')
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
def __str__(self):
return self.username
@property
def token(self):
return self._generate_jwt_token()
def _generate_jwt_token(self):
token = jwt.encode({
'exp': datetime.utcnow() + timedelta(days=1),
'iat': datetime.utcnow(),
'data': {
'username': self.username
}
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
class Meta:
default_permissions = ()
permissions = (
("select_user", "查看用户"),
("change_user", "修改用户"),
("delete_user", "删除用户"),
)
另外,思考下这些能不能不直接影响视图层的处理,通过中间件处理是否可以呢。
思路:
- 自己写处理认证凭据的中间件,然后加到settings.py
参考:
https://mp.weixin.qq.com/s?__biz=MjM5OTMyODA4Nw==&mid=2247483911&idx=1&sn=cb66b1179996ca1107af279cf40eb5ff&chksm=a73c623f904beb29ebd1c4934ac1cc94e936b6615ed9dd9e258fa1d92d0c04b08c765c6b5d86&scene=21#wechat_redirect
http://www.bubuko.com/infodetail-3141395.html
https://www.v2ex.com/t/530103