美文网首页
用Socket搭建一个简易网站(四)

用Socket搭建一个简易网站(四)

作者: 我有一只碗 | 来源:发表于2018-02-16 22:39 被阅读0次

    这节课我们就要做登陆了,关于登陆其实需要的知识点不少,第一要懂得登陆的原理(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验证方式就是基于这种思想的,有兴趣的同学可以研究一下。

    项目完整代码:https://github.com/KEYYYYY/simple-server

    相关文章

      网友评论

          本文标题:用Socket搭建一个简易网站(四)

          本文链接:https://www.haomeiwen.com/subject/xegptftx.html