关于python:用-Python-撸一个-Web-服务器第8章用户管理

34次阅读

共计 16627 个字符,预计需要花费 42 分钟才能阅读完成。

用户登录原理

用户登录与注册性能简直已成为 Web 利用的标配。所以咱们有必要给 Todo List 程序减少一个用户治理模块,以此来学习用户登录原理。

HTTP 协定是无状态的,这意味着每个残缺的 HTTP 申请 —— 响应过程都是绝对独立的,Web 服务器无奈分辨前后两次间断申请是否为同一个用户(客户端)发送过去的。为了让服务器可能记住用户,就有了一种叫作 Cookie 的技术。我画了一张图来形容 Cookie 的工作过程:

首先浏览器向服务器 / 门路发送了一个 GET 申请,服务器返回响应给浏览器时,在响应头中设置了两个键为 Set-Cookie 的响应首部字段,接下来浏览器收到 Set-Cookie 响应首部字段后会 主动 将其保留下来,在之后的所有申请中浏览器都会 主动 加上 Cookie 申请首部字段。

这就是 Cookie 的大抵工作原理,总结起来 Cookie 有如下几个特点:

  • Cookie 须要服务器和浏览器独特配合应用,服务器通过 Set-Cookie 响应首部字段来设置 Cookie,并且能够同时设置多个 Set-Cookie 响应首部字段。浏览器收到 Set-Cookie 响应首部字段时,会 主动 保留 Cookie 内容,在之后的申请中就会通过 主动 加上 Cookie 申请首部字段来携带 Cookie 内容到服务器。
  • Cookie 总是以键值对的模式存在,如 a=123b=456,多个 Cookie 应用 ; 分隔。
  • Cookie 并不限度申请办法,任何 HTTP 申请办法都能够应用 Cookie
  • 出于平安思考,Cookie 有域名限度。任何厂商的浏览器在保留 Cookie 时都会记录这个 Cookie 是属于哪个域名的,在给服务器发送申请时,只有这个域名下的 Cookie 才会被携带。如拜访 127.0.0.1:8000 域名时浏览器就只会携带这个域名下的 Cookie,如果此时浏览器也保留了 www.jd.com 域名下的 Cookie,是不会被携带到 127.0.0.1:8000 服务器的。
  • Cookie 能够设置过期工夫,像上图这样只设置了 Cookie 的键值为 a=123 这种状况,浏览器默认的解决机制是当浏览器敞开时主动删除这个 Cookie。如果须要设置过期工夫则能够这样设置 Set-Cookie: a=123; Max-Age=60,此时 a=123 这条 Cookie 的过期工夫就是 60 秒,60 秒后浏览器会主动将保留的这条 Cookie 删除。

如何在 Chrome 浏览器中查看 Cookie 呢?关上 Chrome 浏览器开发者工具,抉择 Application 选项卡,点击 Cookies 就能看到以后域名下记录的所有 Cookie 信息。

如果利用 Cookie 实现用户登录,那么流程大抵如下:

  1. 浏览器拜访 Web 利用的登录页面。
  2. 用户在登录页面输出用户名、明码,点击登录按钮进行登录。
  3. 服务器端收到浏览器传输过去的用户名、明码,而后到数据库中查找是否存在这个用户,身份验证一旦通过,就在返回给浏览器的响应中退出 Set-Cookie: username=zhangsan 响应首部字段。
  4. 浏览器在收到带有 Set-Cookie: username=zhangsan 响应首部字段的响应后,会将 username=zhangsan 保留下来。
  5. 当浏览器再次申请 Web 利用的某个页面时,就会 主动 携带 Cookie: username=zhangsan 申请首部字段。
  6. 此时服务器端收到浏览器申请,通过解析 Cookie 申请首部字段,服务器就晓得这个申请是 zhangsan 发送过去的了。

尽管应用 Cookie 可能实现用户登录,然而这种间接将用户信息 username=zhangsan 存储到 Cookie 的形式并不牢靠。因为用户名很容易裸露,也很容易被猜到。如果网站的攻击者晓得了咱们零碎中有 zhangsan 这个用户存在,那么他并不需要晓得 zhangsan 这个用户的明码,只须要在浏览器中将 username=zhangsan 这个 Cookie 增加到 127.0.0.1:8000 域名下(在 Chorme 开发者工具中能够手动更改 Cookie),浏览器在下次申请 127.0.0.1:8000 服务器时就会主动携带这个 Cookie,服务器收到申请后就会认为申请是 zhangsan 这个用户发送过去的,这样网站攻击者就骗过了服务器的登录机制。故此,为了解决这个问题,又有人提出了一个叫作 Session 的概念。

Session 并不是一个具体的技术实现,而是一种思维。在利用 Cookie 实现用户登录时,是间接将用户信息 username=zhangsan 以明文的模式存储到浏览器 Cookie 中的。而采纳 Session 机制后,则能够将用户信息保留到服务器端(能够是任何存储介质),例如能够保留成 JSON 对象放到文件中:

{
    "6d78289e237c48719201640736226e39": "zhangsan",
    "3cdb8c488abb4eccad2c7cc8c3c9d065": "lisi"
}

对象的键是一个随机字符串,叫作 session_id,值为用户名。这样在服务器返回响应时,不再间接将用户名以明文的形式发送给浏览器,而是将 session_id 放到响应首部字段中 Set-Cookie: session_id=6d78289e237c48719201640736226e39 发送给浏览器。这种扭转对于浏览器端来说并没有任何实质变动,浏览器仍旧将这个 Cookie 保留到本地,下次发送申请时 主动 携带。但 Session 机制的退出,使得用户登录机制变得更加平安。因为 session_id 是一个随机生成的字符串,歹意用户即便晓得用户名也无奈伪造这个 session_id

不过退出了 Session 机制当前,服务器验证用户 Cookie 的机制就要稍作批改。以前服务器只须要解析浏览器传过来的 Cookie,就能失去用户名 zhangsan,但当初解析 Cookie 后失去的是 session_id,服务器还须要再到存储了所有 SessionJSON 对象中查找这个 session_id 键所对应的值,这样就失去了以后登录的用户名。

退出 Session 机制当前,登录流程大抵如下:

  1. 浏览器拜访 Web 利用的登录页面。
  2. 用户在登录页面输出用户名、明码,点击登录按钮进行登录。
  3. 服务器端收到浏览器传输过去的用户名、明码,而后到数据库中查找是否存在这个用户,身份验证通过当前,为 zhangsan 这个用户生成一个随机的 session_id,而后将 session_id 和用户名以键值对的模式存储到 JSON 文件中,接着在返回给浏览器的响应中退出 Set-Cookie: session_id=6d78289e237c48719201640736226e39 响应首部字段。
  4. 浏览器在收到带有 Set-Cookie: session_id=6d78289e237c48719201640736226e39 响应首部字段的响应后,会将 session_id=6d78289e237c48719201640736226e39 保留下来。
  5. 当浏览器再次申请 Web 利用的某个页面时,就会 主动 携带 Cookie: session_id=6d78289e237c48719201640736226e39 申请首部字段。
  6. 此时服务器端收到浏览器申请,通过解析 Cookie 申请首部字段失去 session_id,而后再到存储了所有 SessionJSON 文件中查找 6d78289e237c48719201640736226e39 这个 session_id 所对应的用户名为 zhangsan,服务器就晓得这个申请是 zhangsan 发送过去的了。

以上就是最常见的采纳 Session + Cookie 的形式实现用户登录的机制,Session 只是一种思维,也能够不搭配 Cookie 来应用,而用户登录的实现形式也有多种,有趣味的读者能够依据本人的需要自行摸索。

用户治理功能设计

晓得了用户登录原理,咱们再来剖析下要为 Todo List 程序减少用户治理性能,应该如何实现:

  • 对于模型层,须要新增 UserSession 两个模型类别离解决用户和 Session
  • 对于视图层,须要新增 register.html(注册)、login.html(登录)两个 HTML 页面。
  • 对于控制器层,须要实现 register(注册)、login(登录)两个视图函数。

除了对 MVC 模式中每一层须要减少的性能局部外。咱们还须要减少 user.jsonsession.json两个 JSON 文件,来别离存储用户信息和 Session 信息。

另外,Todo List 程序在退出用户治理性能后,只有已登录用户才可查看治理本人的 todo。所以还要对 todo 治理局部现有的视图函数做些批改。

用户治理性能编码实现

依据以上对用户治理性能的剖析,设计以后的 Todo List 程序目录构造如下:

todo_list
├── server.py
├── tests
│   ├── test_controllers.py
└── todo
    ├── __init__.py
    ├── config.py
    ├── controllers
    │   ├── __init__.py
    │   ├── auth.py
    │   ├── static.py
    │   └── todo.py
    ├── db
    │   ├── session.json
    │   ├── todo.json
    │   └── user.json
    ├── logs
    │   └── todo.log
    ├── models
    │   ├── __init__.py
    │   ├── session.py
    │   ├── todo.py
    │   └── user.py
    ├── static
    │   ├── css
    │   │   └── style.css
    │   └── favicon.ico
    ├── templates
    │   ├── auth
    │   │   ├── login.html
    │   │   └── register.html
    │   └── todo
    │       ├── edit.html
    │       └── index.html
    └── utils
        ├── __init__.py
        ├── error.py
        ├── http.py
        ├── logging.py
        └── templating.py

Session 模型类编写在 models/session.py 文件中:

# todo_list/todo/models/session.py

import uuid
import datetime

from . import Model


class Session(Model):
    """Session 模型类"""

    def __init__(self, **kwargs):
        # 为了平安起见,Session id 不应用自增数字,而应用 uuid
        self.id = kwargs.get('id')
        if self.id is None:
            self.id = uuid.uuid4().hex

        self.user_id = kwargs.get('user_id', -1)
        self.expire_in = kwargs.get('expire_in')
        
        if self.expire_in is None:
            now = datetime.datetime.now()
            expire_in = now + datetime.timedelta(days=1)
            self.expire_in = expire_in.strftime('%Y-%m-%d %H:%M:%S')

    def is_expired(self):
        """判断 Session 是否过期"""
        now = datetime.datetime.now()
        return datetime.datetime.strptime(self.expire_in, '%Y-%m-%d %H:%M:%S') <= now

    def save(self):
        """覆写父类的 save 办法,保留时过滤掉曾经过期的 Session"""
        models = [model.__dict__ for model in self.all()
                  if model.id != self.id and not model.is_expired()]
        if not self.is_expired():
            models.append(self.__dict__)
        self._save_db(models)

Session 模型类继承自 Model。与 Todo 模型类只实现了 __init__ 办法不同,Session 模型类还实现了 is_expiredsave 两个办法。is_expired 办法用来判断以后 Session 是否过期,因为通常来说用户的登录工夫都是有期限的。save 办法在保留以后 Session 对象时过滤掉曾经过期的 Session

我设计了如下 JSON 对象用来存储用户登录的 Session 数据:

{
    "id": "6d78289e237c48719201640736226e39",
    "user_id": 2,
    "expire_in": "2020-05-31 22:27:55"
}

id 即为 session_iduser_id 对应以后登录用户的 id,这样就能通过这个 Session 对象查找到对应用户,expire_in 示意这条 Session 的过期工夫。

为了实现随机的 session_id,在 Session 模型的 __init__ 办法中,通过 uuid 来获取一个随机字符串。

User 模型类编写在 models/user.py 文件中:

# todo/models/user.py

import hashlib

from . import Model
from todo.config import SECRET


class User(Model):
    """User 模型类"""

    def __init__(self, **kwargs):
        self.id = kwargs.get('id')
        self.username = kwargs.get('username', '')
        self.password = kwargs.get('password', '')

    @classmethod
    def generate_password(cls, raw_password):
        """生成明码"""
        md5 = hashlib.md5()
        md5.update(SECRET.encode('utf-8'))
        md5.update(raw_password.encode('utf-8'))
        return md5.hexdigest()

    @classmethod
    def validate_password(cls, raw_password, password):
        """验证明码"""
        md5 = hashlib.md5()
        md5.update(SECRET.encode('utf-8'))
        md5.update(raw_password.encode('utf-8'))
        return md5.hexdigest() == password

出于平安思考,明码通常不会以明文的模式存储到数据库中。这样倘若咱们的 Web 利用被拖库,非明文明码能减小用户被撞库的危险。

因为明码不应用明文存储到文件中,所以 User 模型中实现了生成明码和查看明码两个办法。调用 generate_password 办法传入原始明码,失去的是加密后的字符串,能够将其存储到文件中,这个字符串无奈解密。验证时,将原始明码(用户输出的明码)和加密后的字符串一起传入 validate_password 办法,即可验证原始明码是否正确。

这里采纳了 md5 算法对用户明码进行加密。md5 算法是一种被宽泛应用的散列算法,但其有被碰撞的危险,所以代码中对其进行了 加盐 解决,这样就可能大大降低碰撞概率。

在进行明码验证时,并不需要对文件中存储的加密明码进行解密,只有对原始明码应用同样的办法进行加密,而后比拟两个加密后的字符串是否相等即可。因为 md5 算法对于雷同的输出肯定会失去雷同的输入,也就是说对同样的数据每次加密后果统一。

严格来讲,md5 并不属于加密算法,而是散列算法。因为通过加密算法加密后的数据是能够解密的,而通过散列算法失去的是一个信息摘要,不能通过这个摘要反向失去原始数据。但很多人都习惯把 md5 算法称作加密算法,故此,我这里也采纳了加密算法来的叫法来介绍它。更多的对于 md5 算法的常识读者可自行搜寻相干材料进行学习。

用户信息将会存储到 db/user.json 文件中,格局如下:

{
    "id": 1,
    "username": "user",
    "password": "7fff062fcb96c6f041df7dbc3fa0dcaf"
}

创立好了 SessionUser 两个模型,咱们接下来实现用户注册性能。

注册页面的 HTML 如下:

<!-- todo_list/todo/templates/auth/register.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List | Register</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1 class="container">Register</h1>
<div class="container">
    <form class="register" action="/register" method="post">
        <input type="text" name="username" placeholder="Username">
        <input type="password" name="password" placeholder="Password">
        <button> 注册 </button>
    </form>
</div>
</body>
</html>

将注册页面的 CSS 代码追加到 style.css 文件中:

/* todo_list/todo/static/css/style.css */

.register {
    width: 100%;
    max-width: 600px;
    text-align: center;
}

.register input {
    width: 100%;
    height: 40px;
    padding: 0 4px;
}

.register button {margin-top: 20px;}

在控制器层,编写一个 register 视图函数用来解决用户注册的业务逻辑:

# todo_list/todo/controllers/auth.py

def register(request):
    """注册视图函数"""
    if request.method == 'POST':
        # 获取表单中的用户名和明码
        form = request.form
        logger(f'form: {form}')
        username = form.get('username')
        raw_password = form.get('password')

        # 验证用户名和明码是否非法
        if not username or not raw_password:
            return '有效的用户名或明码'.encode('utf-8')
        user = User.find_by(username=username, ensure_one=True)
        if user:
            return '用户名已存在'.encode('utf-8')

        # 对明码进行散列计算,创立并保留用户信息
        password = User.generate_password(raw_password)
        user = User(username=username, password=password)
        user.save()
        # 注册胜利后重定向到登录页面
        return redirect('/login')

    return render_template('auth/register.html')

注册视图函数能够接管两种申请,GETPOST。如果为 GET 申请,则阐明用户要拜访注册页面,间接返回注册页面对应的 HTML。如果为 POST 申请,则阐明用户点击了注册页面的注册按钮,须要解决注册逻辑。

用户注册胜利后,会被重定向到登录页面,所以接下来咱们要实现用户登录性能。

登录页面的 HTML 如下:

<!-- todo_list/todo/templates/auth/login.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List | Login</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1 class="container">Login</h1>
<div class="container">
    <form class="login" action="/login" method="post">
        <input type="text" name="username" placeholder="Username">
        <input type="password" name="password" placeholder="Password">
        <button> 登录 </button>
    </form>
</div>
</body>
</html>

将登录页面的 CSS 代码追加到 style.css 文件中:

/* todo_list/todo/static/css/style.css */

.login {
    width: 100%;
    max-width: 600px;
    text-align: center;
}

.login input {
    width: 100%;
    height: 40px;
    padding: 0 4px;
}

.login button {margin-top: 20px;}

在控制器层,编写一个 login 视图函数用来解决用户登录的业务逻辑:

# todo_list/todo/controllers/auth.py

def login(request):
    """登录视图函数"""
    # 如果用户曾经登录,间接重定向到首页
    if current_user(request):
        return redirect('/index')

    if request.method == 'POST':
        message = '用户名或明码不正确'.encode('utf-8')

        # 获取表单中的用户名和明码
        form = request.form
        logger(f'form: {form}')
        username = form.get('username')
        raw_password = form.get('password')
        
        # 验证用户名和明码是否正确
        if not username or not raw_password:
            return message
        user = User.find_by(username=username, ensure_one=True)
        if not user:
            return message
        password = user.password
        if not User.validate_password(raw_password, password):
            return message

        # 创立 Session 并将 session_id 写入 Cookie 实现登录
        session = Session(user_id=user.id)
        session.save()
        cookies = {'session_id': session.id,}
        return redirect('/index', cookies=cookies)

    return render_template('auth/login.html')

登录视图函数同样能够接管 GETPOST 两种申请形式。如果为 GET 申请,则阐明用户要拜访登录页面,间接返回登录页面对应的 HTML。如果为 POST 申请,则阐明用户点击了登录页面的登录按钮,须要解决登录逻辑。

登录视图函数里调用了 current_user 函数来判断用户以后是否曾经登录,current_user 函数实现如下:

# todo_list/todo/utils/auth.py

def current_user(request):
    """获取以后登录用户"""
    # 从 Cookie 中获取 session_id
    cookies = request.cookies
    logger(f'cookies: {cookies}')
    session_id = cookies.get('session_id')

    # 查找 Session 并验证其是否过期
    session = Session.get(session_id)
    if not session:
        return None
    if session.is_expired():
        session.delete()
        return None

    # 查找以后登录用户
    user = User.get(session.user_id)
    if not user:
        return None
    return user

current_user 函数中通过 request 对象的 cookies 属性获取以后申请中携带的 Cookie 信息,对于 Request 类如何解析申请中携带的 Cookie 信息局部相干的代码能够到本章节的源码仓库进行查看。

为了实现用户登录,须要依据 user_id 创立一个 Session 对象,并将 Session 对象的 session_id 写入浏览器 Cookie。因而对 redirect 函数和 Response 类做如下批改:

# todo_list/todo/utils/http.py

def redirect(url, status=302, cookies=None):
    """重定向"""
    headers = {'Location': url,}
    body = ''
    return Response(body, headers=headers, status=status, cookies=cookies)


class Response(object):
    """响应类"""

    # 依据状态码获取起因短语
    reason_phrase = {
        200: 'OK',
        302: 'FOUND',
        405: 'METHOD NOT ALLOWED',
    }

    def __init__(self, body, headers=None, status=200, cookies=None):
        # 默认响应头,指定响应内容的类型为 HTML
        _headers = {'Content-Type': 'text/html; charset=utf-8',}

        if headers is not None:
            _headers.update(headers)
        self.headers = _headers  # 响应头
        self.body = body  # 响应体
        self.status = status  # 状态码
        self.cookies = cookies  # Cookie

    def __bytes__(self):
        """结构响应报文"""
        # 状态行 'HTTP/1.1 200 OK\r\n'
        header = f'HTTP/1.1 {self.status} {self.reason_phrase.get(self.status,"")}\r\n'
        # 响应首部
        header += ''.join(f'{k}: {v}\r\n' for k, v in self.headers.items())
        # Cookie
        if self.cookies:
            header += 'Set-Cookie:' + \
                      ';'.join(f'{k}={v}' for k, v in self.cookies.items())
        # 空行
        blank_line = '\r\n'
        # 响应体
        body = self.body

        # body 反对 str 或 bytes 类型
        if isinstance(body, str):
            body = body.encode('utf-8')
        response_message = (header + blank_line).encode('utf-8') + body
        return response_message

这样,当 login 视图函数解决完登录逻辑,执行到最初一行 return redirect('/index', cookies=cookies) 时,就可能实现重定向到首页并实现登录。

当初能够到浏览器中测试注册、登录性能:

用户登录胜利后,会被重定向到首页,展现以后用户所有 todo。

目前 todo 和用户还没有做关联,为了使两者分割起来,还须要更改 Todo 模型和存储 todo 的 JSON 对象格局。

Todo 模型的 __init__ 办法须要可能接管 user_id

# todo_list/todo/models/todo.py

from . import Model


class Todo(Model):
    """Todo 模型类"""

    def __init__(self, **kwargs):
        self.id = kwargs.get('id')
        self.user_id = kwargs.get('user_id', -1)
        self.content = kwargs.get('content', '')

存储 todo 的 JSON 对象中须要保留 user_id

{
    "id": 4,
    "user_id": 1,
    "content": "hello world"
}

这样就能通过以后用户的 user_id 查问出与之关联的所有 todo 了。

批改 Todo List 程序首页视图函数,获取以后登录用户,查问与之关联的所有 todo。

# todo_list/todo/controllers/todo.py

def index(request):
    """首页视图函数"""
    user = current_user(request)
    todo_list = Todo.find_by(user_id=user.id, sort=True, reverse=True)
    context = {'todo_list': todo_list,}
    return render_template('todo/index.html', **context)

登录胜利后再次拜访程序首页将显示以后用户的 todo:

你能够再注册一个账号,在另一个浏览器中关上 Todo List 程序,试着给另一个用户增加几条 todo,看看成果。

todo 关联了用户当前,对 todo 的新增操作,须要验证用户是否登录。而对于 todo 的删除、批改、查问操作,只有 todo 的创建者才有权限,所以不仅要验证用户是否登录,还要验证操作的 todo 是否属于以后登录用户。

咱们当然能够将验证操作都放到视图函数中,但仔细观察,你会发现对 todo 的所有操作有一个共同点,都须要验证用户是否登录。所以更优雅的做法是写一个验证登录的装璜器,这样所有须要验证用户登录的视图函数都只须要打上这个装璜器即可。

# todo_list/todo/utils/auth.py

def login_required(func):
    """验证登录装璜器"""

    @functools.wraps(func)
    def wrapper(request):
        user = current_user(request)
        if not user:
            return redirect('/login')
        result = func(request)
        return result

    return wrapper

以解决 Todo List 程序首页视图函数为例,验证登录装璜器的应用办法如下:

# todo_list/todo/controllers/auth.py

@login_required
def index(request):
    """首页视图函数"""
    user = current_user(request)
    todo_list = Todo.find_by(user_id=user.id, sort=True, reverse=True)
    context = {'todo_list': todo_list,}
    return render_template('todo/index.html', **context)

你不须要对 index 视图函数外部的代码做任何批改,只须要在函数定义处打上 @login_required 装璜即可。

在视图函数外部通过给 Todo 模型的 find_by 办法传入 user_id 关键字参数来查问只属于以后登录用户的 todo。

与 todo 相干的其余视图函数代码如下,这里不再一一解说。

# todo_list/todo/controllers/auth.py

@login_required
def new(request):
    """新建 todo 视图函数"""
    form = request.form
    logger(f'form: {form}')

    content = form.get('content')
    if content:
        user = current_user(request)
        if user:
            todo = Todo(content=content, user_id=user.id)
            todo.save()
    return redirect('/index')


@login_required
def edit(request):
    """编辑 todo 视图函数"""
    if request.method == 'POST':
        form = request.form
        logger(f'form: {form}')

        id = int(form.get('id', -1))
        content = form.get('content')

        if id != -1 and content:
            user = current_user(request)
            if user:
                todo = Todo.find_by(id=id, user_id=user.id, ensure_one=True)
                if todo:
                    todo.content = content
                    todo.save()
        return redirect('/index')

    args = request.args
    logger(f'args: {args}')

    id = int(args.get('id', -1))
    if id == -1:
        return redirect('/index')

    user = current_user(request)
    if not user:
        return redirect('/index')

    todo = Todo.find_by(id=id, user_id=user.id, ensure_one=True)
    if not todo:
        return redirect('/index')

    context = {'todo': todo,}
    return render_template('todo/edit.html', **context)


@login_required
def delete(request):
    """删除 todo 视图函数"""
    form = request.form
    logger(f'form: {form}')

    id = int(form.get('id', -1))
    if id != -1:
        user = current_user(request)
        if user:
            todo = Todo.find_by(id=id, user_id=user.id, ensure_one=True)
            if todo:
                todo.delete()
    return redirect('/index')

欠缺我的项目

一个比拟残缺的 Web 我的项目应该退出全局异样解决的机制,因为你无奈在程序的每个函数中枚举全副可能呈现的异样。没有被捕捉的异样一旦间接裸露给用户,很可能会透露程序的重要信息。

这里我设计了两个异样页面,404 页面用来通知用户所拜访的页面不存在,500 页面用来通知用户服务器呈现了未知谬误。

404 页面的 HTML 代码如下:

<!-- todo_list/todo/templates/error/404.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List | Page Not Found</title>
</head>
<body>
<p>Page Not Found</p>
</body>
</html>

500 页面的 HTML 代码如下:

<!-- todo_list/todo/templates/error/500.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List | Internal Server Error</title>
</head>
<body>
<p>Internal Server Error</p>
</body>
</html>

templates 模板目录下新建 error/ 目录用来寄存 404500 两个全局异样页面。

以下是 404 页面和 500 页面的处理函数:

# todo_list/todo/utils/error.py

from todo.utils.templating import render_template
from utils.http import Response


def page_not_found():
    """解决 400 异样"""
    body = render_template('error/404.html')
    return Response(body, status=400)


def internal_server_error():
    """解决 500 异样"""
    body = render_template('error/500.html')
    return Response(body, status=500)


errors = {
    404: page_not_found,
    500: internal_server_error,
}

因为 server.py 是服务器程序的入口和进口,所以全局捕捉异样的代码适宜写在此文件。

# todo_list/server.py

def process_connection(client):
    """解决客户端申请"""
    request_bytes = b''
    while True:
        chunk = client.recv(BUFFER_SIZE)
        request_bytes += chunk
        if len(chunk) < BUFFER_SIZE:
            break

    # 申请报文
    request_message = request_bytes.decode('utf-8')
    logger(f'request_message: {request_message}')

    # 解析申请报文
    request = Request(request_message)
    try:
        # 依据申请报文结构响应报文
        response_bytes = make_response(request)
    except Exception as e:
        logger(e)
        # 返回给用户 500 页面
        response_bytes = bytes(errors[500]())
    # 返回响应
    client.sendall(response_bytes)

    # 敞开连贯
    client.close()


def make_response(request, headers=None):
    """结构响应报文"""
    # 默认状态码为 200
    status = 200
    # 解决动态资源申请
    if request.path.startswith('/static'):
        route, methods = routes.get('/static')
    else:
        try:
            route, methods = routes.get(request.path)
        except TypeError:
            # 返回给用户 404 页面
            return bytes(errors[404]())

    # 如果申请办法不被容许返回 405 状态码
    if request.method not in methods:
        status = 405
        data = 'Method Not Allowed'
    else:
        # 申请首页时 route 实际上就是咱们在 controllers.py 中定义的 index 视图函数
        data = route(request)

    # 如果返回后果为 Response 对象,间接获取响应报文
    if isinstance(data, Response):
        response_bytes = bytes(data)
    else:
        # 返回后果为字符串,须要先结构 Response 对象,而后再获取响应报文
        response = Response(data, headers=headers, status=status)
        response_bytes = bytes(response)

    logger(f'response_bytes: {response_bytes}')
    return response_bytes

当用户拜访的 URL 门路没有匹配的视图函数时,能够返回给用户 404 页面。当返回响应之前呈现未捕捉的异样时,会被 process_connection 函数中的全局异样解决所捕捉,能够返回给用户 500 页面。记得将真正的异样信息写入日志,不便排查。

以下是遇到 404 异样或 500 异样时的页面截图:

至此,Todo List 程序的性能就全副开发实现了。最初给读者留一个作业,能够试着实现用户登出性能。

本章源码:chapter8

分割我:

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:https://jianghushinian.cn/

正文完
 0