共计 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=123
、b=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
实现用户登录,那么流程大抵如下:
- 浏览器拜访 Web 利用的登录页面。
- 用户在登录页面输出用户名、明码,点击登录按钮进行登录。
- 服务器端收到浏览器传输过去的用户名、明码,而后到数据库中查找是否存在这个用户,身份验证一旦通过,就在返回给浏览器的响应中退出
Set-Cookie: username=zhangsan
响应首部字段。 - 浏览器在收到带有
Set-Cookie: username=zhangsan
响应首部字段的响应后,会将username=zhangsan
保留下来。 - 当浏览器再次申请 Web 利用的某个页面时,就会 主动 携带
Cookie: username=zhangsan
申请首部字段。 - 此时服务器端收到浏览器申请,通过解析
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
,服务器还须要再到存储了所有 Session
的 JSON
对象中查找这个 session_id
键所对应的值,这样就失去了以后登录的用户名。
退出 Session
机制当前,登录流程大抵如下:
- 浏览器拜访 Web 利用的登录页面。
- 用户在登录页面输出用户名、明码,点击登录按钮进行登录。
- 服务器端收到浏览器传输过去的用户名、明码,而后到数据库中查找是否存在这个用户,身份验证通过当前,为
zhangsan
这个用户生成一个随机的session_id
,而后将session_id
和用户名以键值对的模式存储到JSON
文件中,接着在返回给浏览器的响应中退出Set-Cookie: session_id=6d78289e237c48719201640736226e39
响应首部字段。 - 浏览器在收到带有
Set-Cookie: session_id=6d78289e237c48719201640736226e39
响应首部字段的响应后,会将session_id=6d78289e237c48719201640736226e39
保留下来。 - 当浏览器再次申请 Web 利用的某个页面时,就会 主动 携带
Cookie: session_id=6d78289e237c48719201640736226e39
申请首部字段。 - 此时服务器端收到浏览器申请,通过解析
Cookie
申请首部字段失去session_id
,而后再到存储了所有Session
的JSON
文件中查找6d78289e237c48719201640736226e39
这个session_id
所对应的用户名为zhangsan
,服务器就晓得这个申请是zhangsan
发送过去的了。
以上就是最常见的采纳 Session
+ Cookie
的形式实现用户登录的机制,Session
只是一种思维,也能够不搭配 Cookie
来应用,而用户登录的实现形式也有多种,有趣味的读者能够依据本人的需要自行摸索。
用户治理功能设计
晓得了用户登录原理,咱们再来剖析下要为 Todo List 程序减少用户治理性能,应该如何实现:
- 对于模型层,须要新增
User
、Session
两个模型类别离解决用户和Session
。 - 对于视图层,须要新增
register.html
(注册)、login.html
(登录)两个 HTML 页面。 - 对于控制器层,须要实现
register
(注册)、login
(登录)两个视图函数。
除了对 MVC
模式中每一层须要减少的性能局部外。咱们还须要减少 user.json
、session.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_expired
、save
两个办法。is_expired
办法用来判断以后 Session
是否过期,因为通常来说用户的登录工夫都是有期限的。save
办法在保留以后 Session
对象时过滤掉曾经过期的 Session
。
我设计了如下 JSON
对象用来存储用户登录的 Session
数据:
{
"id": "6d78289e237c48719201640736226e39",
"user_id": 2,
"expire_in": "2020-05-31 22:27:55"
}
id
即为 session_id
,user_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"
}
创立好了 Session
、User
两个模型,咱们接下来实现用户注册性能。
注册页面的 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')
注册视图函数能够接管两种申请,GET
或 POST
。如果为 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')
登录视图函数同样能够接管 GET
和 POST
两种申请形式。如果为 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/
目录用来寄存 404
和 500
两个全局异样页面。
以下是 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/