这节课我们就要做登陆了,关于登陆其实需要的知识点不少,第一要懂得登陆的原理(cookie,session),还要在我们的User模型添加一个验证方法。下面我们开始。
我们首先说明一下cookie,HTTP协议是一个无状态的协议,协议本身并没有包含身份认证的机制,无法确定这个请求是谁发过来的,这次发过来的和上次是不是一个人,为了解决这个问题,所以cookie就诞生了。
通过服务器在在HTTP报文里添加Set-Cookie:key=value头部,浏览器收到这个报文之后会自动将报文保存在本地,并且每次请求我们服务器会带着这个,我们下面来实际的演示一下。
先把我们简单的login路由函数写出来。
def login(request):
if request.method == 'GET':
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
body = template('login.html')
return header + '\r\n' + body
elif request.method == 'POST':
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nSet-Cookie: username=123\r\n'
body = template('login.html')
return header + '\r\n' + body
else:
return error(request)
我们可以看到再返回的时候给头部设置了让浏览器保存cookie的字段。
先让浏览器给login发送一个POST请求,报文如下。
请求的主机信息('192.168.1.105', 48148)
POST /login HTTP/1.1
Host: 192.168.1.101:2000
User-Agent: Mozilla/5.0 (X11; Linux armv7l; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.101:2000/login
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=admin&password=123
下面在以GET方式请求login路由,报文如下。
请求的主机信息('192.168.1.105', 48150)
GET /login HTTP/1.1
Host: 192.168.1.101:2000
User-Agent: Mozilla/5.0 (X11; Linux armv7l; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: username=123
Connection: keep-alive
Upgrade-Insecure-Requests: 1
我先发现了一个新字段,Cookie,它就是我们刚才设置的值。所以我们用它可以实现登陆了。
我们先在user模型添加验证方法。
class User(Model):
def __init__(self, username, password):
self.username = username
self.password = password
@classmethod
def validate(cls, username, password):
user = User.get_by(username=username, password=password)
if user:
return True
return False
然后再改写login的验证逻辑
def login(request):
if request.method == 'GET':
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
body = template('login.html')
return header + '\r\n' + body
elif request.method == 'POST':
data = request.form()
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
if User.validate(data['username'], data['password']):
header += 'Set-Cookie: username:{}'.format(data['username'])
body = template('login.html')
body = body.replace('{{ message }}', '登陆成功')
else:
body = template('login.html')
body = body.replace('{{ message }}', '登录失败')
return header + '\r\n' + body
else:
return error(request)
然后我们改一下首页逻辑,希望它能认识我们登陆用户。
def index(request):
if 'Cookie' in request.headers:
username = request.headers['Cookie'].split(':', 1)[1]
else:
username = 'nobody'
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
body = template('index.html')
body = body.replace('{{ username }}', username)
return header + '\r\n' + body
好了,现在它认识用户了。
但是我们静下来仔细想想,我们代码其实不是很严密。这里面有很大的安全漏洞。我们先看看请求报文,如下。
请求的主机信息('192.168.1.105', 48188)
GET /login HTTP/1.1
Host: 192.168.1.101:2000
User-Agent: Mozilla/5.0 (X11; Linux armv7l; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: username:admin
Connection: keep-alive
Upgrade-Insecure-Requests: 1
我们都知道HTTP协议是简单的文本协议,我们是可以随便改这个HTTP首部字段的,比如请求前我设置Cookie: username:root,那我们的服务器就会把我们认成root用户,我们只要知道这个人的用户名就可以伪装他,那简直太可怕了。
所以Session就诞生了,它只是一种思想,实现的技术还是cookie,换了个名字而已。
Session的基本原理是我们给浏览器设置的cookie并不是真实有效的用户名等信息,而是一个随机字符串,但是这样怎么知道谁是谁呢?我们可以在服务器存储一个对应关系,键是随机字符串,值是真实的用户名,这样子客户端就没办法通过修改自己的cookie来伪装成其他用户了。下面我们实现它。
我们增加了一个全局字典sessions,然后修改一下login路由函数
def login(request):
if request.method == 'GET':
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
body = template('login.html')
return header + '\r\n' + body
elif request.method == 'POST':
data = request.form()
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
if User.validate(data['username'], data['password']):
# 产生一个32位0-9字符串
session_id = ''.join(str(randint(0, 9)) for _ in range(32))
# 保存session值
sessions[session_id] = data['username']
header += 'Set-Cookie: session_id:{}'.format(session_id)
body = template('login.html')
body = body.replace('{{ message }}', '登陆成功')
else:
body = template('login.html')
body = body.replace('{{ message }}', '登录失败')
return header + '\r\n' + body
else:
return error(request)
首页的逻辑也需要修改。
def index(request):
if 'Cookie' in request.headers:
session_id = request.headers['Cookie'].split(':', 1)[1]
# 根据session_id取出用户
username = sessions[session_id]
else:
username = 'nobody'
header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n'
body = template('index.html')
body = body.replace('{{ username }}', username)
return header + '\r\n' + body
下面我们看一看报文。
请求的主机信息('192.168.1.105', 48230)
GET / HTTP/1.1
Host: 192.168.1.101:2000
User-Agent: Mozilla/5.0 (X11; Linux armv7l; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: session_id:69194797573045933789189252889286
Connection: keep-alive
Upgrade-Insecure-Requests: 1
我们再理一下这个流程,我们从cookie里取出session_id,然后去服务器的sessions里查询这个session_id里存的用户是哪个,然后取出来。客户端无法猜出别人的session_id是多少,就算乱猜正确的概率也几乎为零,所以我们基本实现了用户信息的保护而且实现的状态的记录。
我们目前已经实现了session这种机制,但是还有个问题值得我们去考虑,那就是session的持久化问题,我们不希望服务器重启之后session全部消失了。
一共有两种解决方案
第一种是存储在永久性存储介质上,在服务器启动时读入程序。但是这种方式有个弊端,就是如果客户端不是浏览器,那么也就没有cookie这一说了。还有session这种机制不能满足分布式系统的需求,需要将session信息存储在多台服务器上,多台服务器同步也是较大的开销。
第二种是采用对称加密的方式,在服务器生成一个密钥,将用户加密后的数据存入cookie,然后就可以在服务器端解密数据,提取用户信息。这种方式比较灵活,目前流行的JWT验证方式就是基于这种思想的,有兴趣的同学可以研究一下。
网友评论