为了对移动应用有更好的支持,同时弥补cookie不能完成跨域认证的缺陷,会话技术的第三种,token,被引入。这一节我们一起来看看token的工作原理,同时实现一个简单的token生成和认证流程。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
操作环境
先总结下我的操作环境:
- Centos 7
- Python 3.7
- Pycharm 2019.3
- Django 2.2
因为Django长期支持版本2.2LTS和前一个长期支持版本1.11LTS有许多地方不一样,需要小心区分。
session的缺陷
先来回顾一下session认证的流程:
- 用户发送用户名和密码给服务器
- 服务器通过验证后,在当前的session中保存用户的信息,例如用户名,登陆时间,用户角色等等
- 同时服务器发送一个叫做sessionid的cookie给用户
- 随后的每次登录,用户都会通过cookie将sessionid传给服务器
- 服务器查询自己的记录,根据sessionid获取到用户的信息
这样导致了两个问题:
- 首先服务器需要维护用户信息,在进行横向扩容的时候,需要将该信息持久化来共享,不太方便
- 然后是cookie不能实现跨域的认证,同时移动端也不支持cookie
token的改进
做为以上流程的改进,token的实现有两种方式:
- 用户的个人信息还是保存在服务端,不过不用cookie去保存这个用户的记录id,而是发给用户一串唯一的哈希码。下次用户可以通过url的查询参数,或者post内容,或者http头的Authorization字段中将这串码传递给服务端。服务端查询自己的记录或者用户信息。(这种方式和session类似,服务器还是有状态的,不过摆脱了cookie的局限)
- 服务端不保存任何用户信息,所有用户的信息通过服务端的一个密钥进行加密,同时加入哈希值防篡改,以一串码的形式发给用户。用户以后登录也可以通过url的查询参数,或者post内容,或者http头的Authorization字段中将这串码传递给服务端。服务端通过密钥解析后获得用户的信息。(这个方法完美解决了上面session的两个问题,使得服务器无状态,例如比较流行的JWT就是用的这种方案)
网上关于JWT是否取代session的讨论很多,我个人也觉得token跟session比起来有很大的优势。但是如果是网页的话还是session实现起来更简单一点,而且因为保存有用户的信息,服务端想做一些操作就非常容易,例如修改用户角色,修改session过期时间等等。这个问题见仁见智,我们这里不做深入讨论。
token的简单实现
下面就以上面的第一种改进方法来进行一个简单的实现,了解了背后的基本逻辑,以后遇到类似JWT的框架也就很容易理解了。还是做一个简单的登录交互:
- 用户在
register/
页面输入用户名和密码进行注册,服务端查询数据库,不存在该用户就写入数据库否则注册失败 - 用户在
login/
页面输入用户名和密码进行登录,服务端验证用户名和密码,都正确就记录并返回token,否则登录失败。这里简化模型,没有考虑客户端存储token,可以是cookie或者LocalStorage) - 用户在
homepage/
带入自己的token访问,验证token成功则显示个人主页,否则提示错误
创建数据模型
首先创建一个数据模型,存储用户姓名,密码,和token
这里只是演示,生产环境的token会存放在缓存中进行查询
class Employee(models.Model):
e_name = models.CharField(max_length=16, unique=True)
e_passwd = models.CharField(max_length=128)
e_token = models.CharField(max_length=256)
之后迁移到数据库中备用
注册页面
创建路由规则和view函数如下,提供用户注册的页面
path('register/', views.register, name='register'),
def register(request):
if request.method == 'GET':
return render(request, 'token_register.html')
elif request.method == 'POST':
name = request.POST.get('name')
password = request.POST.get('password')
try:
employee = Employee(e_name=name, e_passwd=password) # test only, password should be saved in hash
employee.save()
return HttpResponse('Register Successfully')
except Exception as e:
return HttpResponse('User already exists')
其中在GET
方法下返回的h5页面如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register</title>
</head>
<body>
<form action="{% url 'token:register' %}" method="post">
<label for="name">Name: </label><input type="text" name="name" id="name"><br>
<label for="password">Password: </label><input type="password" name="password" id="password"><br>
<input type="submit">
</form>
</body>
</html>
访问http://127.0.0.1:8000/token/register/
,进行用户注册
注册成功
查看数据库中的记录发现多了一条
登陆页面
创建路由规则和view函数如下,提供登录的页面
path('login/', views.login, name='login'),
def login(request):
if request.method == 'GET':
return render(request, 'token_login.html')
elif request.method == 'POST':
name = request.POST.get('name')
password = request.POST.get('password')
employee = Employee.objects.filter(e_name=name).filter(e_passwd=password)
if employee.exists():
ip = request.META.get('REMOTE_ADDR')
token = generate_token(ip, name)
result = employee.first()
result.e_token = token
result.save()
data = {
'status': 200,
'token': token,
}
return JsonResponse(data=data)
else:
data = {
'status': 800,
}
return JsonResponse(data=data)
在GET
方法是返回的h5页面和注册几乎一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="{% url 'token:login' %}" method="post">
<label for="name">Name: </label><input type="text" name="name" id="name"><br>
<label for="password">Password: </label><input type="password" name="password" id="password"><br>
<input type="submit">
</body>
</html>
然后是这里是重点,就是POST
方法的逻辑。如果用户名和密码都正确,就生成该用户的token,存到数据库中的同时下发给用户。成功了返回200,失败返回自定义的800
企业开发中使用6xx-9xx来进行自定义状态码
Token在生成的时候,要针对当前时间戳单个ip的单个用户必须唯一,否则就不具备身份识别性。所以其实就是对ip,用户名,和当前时间戳的信息集合做一个哈希。 这里的token生成函数如下
def generate_token(ip, name):
timestamp = int(time.time() * 1000)
str_bfmd5 = str(timestamp) + ip + name
token = hashlib.md5(str_bfmd5.encode('utf-8')).hexdigest()
return token
这里使用的是hashlib中的md5,当然也可以用sha128,sha256等等方式。注意编码对象必须是bytes类型才可以。
用刚才注册的账号访问http://127.0.0.1:8000/token/login/
登录
成功获取到返回的Json数据
数据库中也成功进行了记录
个人主页
有了token,用户在访问的时候就可以带上它,我这里采用的是url查询参数的方式进行。
创建路由和view函数如下
path('homepage/', views.homepage, name='homepage'),
def homepage(request):
token = request.GET.get('token')
employee = Employee.objects.filter(e_token=token)
if employee.exists():
return HttpResponse(employee.first().e_name)
else:
return HttpResponse('Token error')
然后带上上一步返回的token创建url
http://127.0.0.1:8000/token/homepage/?token=25ec0f4ad459c0a549fd85cf5ca879f4
成功访问个人主页
也可以直接用反向解析的方式从登录页面直接重定向到个人主页来
def login(request):
if request.method == 'GET':
return render(request, 'token_login.html')
elif request.method == 'POST':
name = request.POST.get('name')
password = request.POST.get('password')
employee = Employee.objects.filter(e_name=name).filter(e_passwd=password)
if employee.exists():
ip = request.META.get('REMOTE_ADDR')
token = generate_token(ip, name)
result = employee.first()
result.e_token = token
result.save()
# data = {
# 'status': 200,
# 'token': token,
# }
# return JsonResponse(data=data)
return HttpResponseRedirect(reverse('token:homepage')+'?token='+token)
else:
data = {
'status': 800,
}
return JsonResponse(data=data)
退出登录
因为对于下发的token没有办法控制,退出登陆的时候只需要删除数据库里面和token对应的记录即可。
总结
到了这里,MTV模型的基础就了解的差不多了。从下一节开始我们一起来看看一些进阶的内容
转载:https://blog.csdn.net/Victor2code/article/details/105101868