项目视频地址 6.01-云笔记项目-1_哔哩哔哩_bilibili P31-P36
需求是这样的
我们简单分析一下,首先任意网站得有一个主页,主页的内容在后期修改会很频繁,所以主页要单独做一个应用,其余就是用户做一个应用,笔记做一个应用,用户与笔记各自给一个数据表,然后使用外键将两个数据表联系起来
目录
1 准备工作
创建项目
创建应用,一共创建了三个,图就不都截出来了
注册应用
修改时区配置
修改数据库配置
修改模板配置
屏蔽一个中间件
添加静态文件配置
创建数据库
我们的项目结构是这样的
之后我们配置根路由
2 创建数据表
无论是什么表,创建时间与更新时间这两个字段是必须要有的(业务上使用的比较频繁,比如创建时间可以统计每日的新增用户量,更新时间可以统计当日用户活跃状态,因为用户信息还有关注,收藏,点赞这种。如果不给的话也不会报错),密码最大长度一般设置为32位,后面用到哈希算法加密后会再提到
这里注意created_time与update_time在数据库中会存储UTC时间,如果你用模板层读的话会自动给你转成你设置时区的时间(如果前后端分离你把接口给前端的时候要让他做一些处理,或者你自己做一些处理)
外键形式为 一对多 ,一个用户有多个笔记,外键挂到多表上
搞清楚这些之后再想怎么存储,没有外键的正常存,有外键的需要多加一个字段,字段名称我们直接看一下mysql
多出来的字段名称为username_id
3 主页部分
在这个项目的主页上我只放了两个链接,实际开发中主页要呈现的东西还是很多的
4 用户部分
templates和static都各自放到了应用文件夹中
对user中的urls设置两个路由
4.1 注册账户
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册页</title>
<link rel="stylesheet" href="/static/css/initialization.css">
<link rel="stylesheet" href="/static/css/register.css">
</head>
<body>
<img src="/static/pic/login_background.png" alt="">
<div id="login_div">
<form action="/user/register/" method="post">
<ul>
<li>用户注册</li>
<li><span></span><input type="text" name="username" class="content_input" maxlength="8"></li>
<li><span></span><input type="password" name="password" class="content_input" maxlength="8"><span></span></li>
<li><input type="submit" value="注   册"></li>
</ul>
</form>
</div>
</body>
<script src="/static/js/register.js"></script>
</html>
css
img {
position:fixed;
width:100%;
height:100%;
z-index:-100;
}
.content_input {
width:200px;
height:30px;
text-indent:30px;
outline:none;
}
span {
display:inline-block;
width:20px;
height:20px;
}
.content_input:focus {
border:3px rgb(107,157,188) solid;
}
#login_div {
position:absolute;
right:350px;
top:50%;
width:225px;
height:285px;
// background-color:white;
margin-top:-150px;
}
#login_div li {
position:relative;
margin-top:20px;
text-align:center;
}
#login_div li:nth-child(1) {
color:rgb(82,152,197);
font-size:30px;
}
#login_div li:nth-of-type(2) span:before {
content:'\e971';
font-family:'icomoon';
font-size:20px;
}
#login_div li:nth-of-type(3) span:nth-of-type(1):before {
content:'\e98d';
font-family:'icomoon';
font-size:20px;
}
#login_div li:nth-of-type(3) span:nth-of-type(2):before {
content:attr(data-content-after);
font-family:'icomoon';
font-size:20px;
}
#login_div span:nth-of-type(1) {
position:absolute;
left:20px;
}
#login_div span:nth-of-type(2) {
position:absolute;
right:20px;
}
input[type='submit'] {
width:200px;
height:50px;
background-color:rgb(61,155,237);
color:white;
border:0px;
font-size:24px;
}
#login_div li:nth-child(5) input[type='checkbox'] {
position:absolute;
left:25px;
top:-10px;
width:20px;
height:20px;
}
#login_div li:nth-child(5) em {
position:absolute;
left:50px;
top:-12px;
font-size:16px;
}
#login_div li:nth-child(6) input[type='checkbox'] {
position:absolute;
left:25px;
top:30px;
width:20px;
height:20px;
}
#login_div li:nth-child(6) em {
position:absolute;
left:50px;
top:17px;
font-size:16px;
text-align:left;
}
js
function change_password() {
if (password_input.type == 'password') {
// 第二个参数是闭上眼睛的icomoon
eye_span.setAttribute('data-content-after','')
password_input.type = 'text'
}
else {
password_input.type = 'password'
// 第二个参数是睁开眼睛的icomoon
eye_span.setAttribute('data-content-after','')
}
}
password_input = document.querySelector("input[name='password']")
eye_span = document.querySelector('#login_div span:nth-of-type(2)')
// 第二个参数是睁开眼睛的icomoon
eye_span.setAttribute('data-content-after','')
eye_span.onclick =change_password
视图是这样的,user_data是之前我们创建的数据表,已在上方引用
如果是get就直接给页面,如果是post会先检查一遍用户名是否已被占用,如果被占用就弹出already_register.html页面,如果没有被占用就在数据表中加上数据,然后返回注册成功页面
4.2 用户名已被注册
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册异常</title>
</head>
<body>
<p>这个用户名已经被注册了,5秒后返回注册页</p>
</body>
<script>
p = document.querySelector('p')
i = 5
setInterval(function() {
i = i - 1
p.innerHTML = '这个用户名已经被注册了,' + i +'秒后返回注册页'
},1000)
setTimeout(function() {location.href = '/user/register/'},5000)
</script>
</html>
4.3 注册成功
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册成功</title>
</head>
<body>
<p>注册成功,5秒后返回至登录页</p>
</body>
<script>
p = document.querySelector('p')
i = 5
setInterval(function() {
i = i - 1
p.innerHTML = '注册成功,' + i +'秒后返回至登录页'
},1000)
setTimeout(function() {location.href = '/user/login/'},5000)
</script>
</html>
4.4 登录账户
- checkbox如果勾选了,post的值是on,如果没勾选post的值是None
登录账户与注册账户的前端是相似的
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页</title>
<link rel="stylesheet" href="/static/css/initialization.css">
<link rel="stylesheet" href="/static/css/login.css">
</head>
<body>
<img src="/static/pic/login_background.png" alt="">
<div id="login_div">
<form action="/user/login/" method="post">
<ul>
<li>用户登录</li>
<li><span></span><input type="text" name="username" class="content_input" maxlength="8"></li>
<li><span></span><input type="password" name="password" class="content_input" maxlength="8"><span></span></li>
<li><input type="submit" value="登   录"></li>
<li><input type="checkbox" name="remember_me"><em>记住我</em></li>
</ul>
</form>
</div>
</body>
<script src="/static/js/login.js"></script>
</html>
css
img {
position:fixed;
width:100%;
height:100%;
z-index:-100;
}
.content_input {
width:200px;
height:30px;
text-indent:30px;
outline:none;
}
span {
display:inline-block;
width:20px;
height:20px;
}
.content_input:focus {
border:3px rgb(107,157,188) solid;
}
#login_div {
position:absolute;
right:350px;
top:50%;
width:225px;
height:285px;
// background-color:white;
margin-top:-150px;
}
#login_div li {
position:relative;
margin-top:20px;
text-align:center;
}
#login_div li:nth-child(1) {
color:rgb(82,152,197);
font-size:30px;
}
#login_div li:nth-of-type(2) span:before {
content:'\e971';
font-family:'icomoon';
font-size:20px;
}
#login_div li:nth-of-type(3) span:nth-of-type(1):before {
content:'\e98d';
font-family:'icomoon';
font-size:20px;
}
#login_div li:nth-of-type(3) span:nth-of-type(2):before {
content:attr(data-content-after);
font-family:'icomoon';
font-size:20px;
}
#login_div span:nth-of-type(1) {
position:absolute;
left:20px;
}
#login_div span:nth-of-type(2) {
position:absolute;
right:20px;
}
input[type='submit'] {
width:200px;
height:50px;
background-color:rgb(61,155,237);
color:white;
border:0px;
font-size:24px;
}
#login_div li:nth-child(5) input[type='checkbox'] {
position:absolute;
left:25px;
top:-10px;
width:20px;
height:20px;
}
#login_div li:nth-child(5) em {
position:absolute;
left:50px;
top:-12px;
font-size:16px;
}
#login_div li:nth-child(6) input[type='checkbox'] {
position:absolute;
left:25px;
top:30px;
width:20px;
height:20px;
}
#login_div li:nth-child(6) em {
position:absolute;
left:50px;
top:17px;
font-size:16px;
text-align:left;
}
js
function change_password() {
if (password_input.type == 'password') {
// 第二个参数是闭上眼睛的icomoon
eye_span.setAttribute('data-content-after','')
password_input.type = 'text'
}
else {
password_input.type = 'password'
// 第二个参数是睁开眼睛的icomoon
eye_span.setAttribute('data-content-after','')
}
}
password_input = document.querySelector("input[name='password']")
eye_span = document.querySelector('#login_div span:nth-of-type(2)')
// 第二个参数是睁开眼睛的icomoon
eye_span.setAttribute('data-content-after','')
eye_span.onclick =change_password
视图
如果用户名与密码都正确,那么就进入笔记列表路由,如果其中有一个错误则会返回login_error页面
4.5 登录异常
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录异常</title>
</head>
<body>
<p>你输入的账户名或密码错误,5秒后返回登录页</p>
</body>
<script>
p = document.querySelector('p')
i = 5
setInterval(function() {
i = i - 1
p.innerHTML = '你输入的账户名或密码错误,' + i +'秒后返回登录页'
},1000)
setTimeout(function() {location.href = '/user/login/'},5000)
</script>
</html>
5 笔记部分
笔记部分一共有五个路由
依次是 笔记列表,增加笔记,删除笔记,编辑笔记,退出登录
5.1 笔记列表
登录后会自动转到笔记列表
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
table {
width:1400px;
text-align:center;
}
td {
float:left;
width:250px;
height:35px;
line-height:35px;
border:1px solid black;
margin-left:-1px;
margin-top:-1px;
}
table thead tr:nth-child(1) td{
background-color:rgb(220,220,220);
}
</style>
<body>
{{current_user}}的笔记 <a href="/note/create_note/">添加新笔记</a> <a href="/note/logout/">退出登录</a>
<table>
<thead>
<tr>
<td>标题</td>
<td>创建时间</td>
<td>修改时间</td>
<td>操作</td>
</tr>
</thead>
<tbody>
{% for note in user_note_data %}
<tr>
<td>{{note.title}}</td>
<td>{{note.created_time}}</td>
<td>{{note.update_time}}</td>
<td>
<a href="/note/edit_note/?id={{note.id}}">修改</a>
<a href="/note/delete_note/?id={{note.id}}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
视图
通过cookie确认用户身份,拿到该用户的所有笔记,拿到该用户的用户名,把以上变量传给note_list.html
5.2 添加笔记
点击这里进入添加笔记路由
点进去两个文本框都是没有东西的
随便输入一些内容后点击提交后,会在note_list中显示
- 由于创建note数据表的时候对创建时间与修改时间设置了自动添加,所以在这里我们不需要手动去动它
如果你什么也没写直接提交也能存进去
视图
如果是get就直接给页面,如果是post就拿到标题,内容与用户id,之后在数据表中创建内容,如果外键那个你不确定字段是什么,你直接用desc查一下mysql就可以了
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/css/initialization.css">
</head>
<body>
<form action="/note/create_note/" method="post">
笔记标题<input type="text" name="title"><br>
笔记内容<textarea name="content" cols="30" rows="10"></textarea><br>
<input type="submit" value="提交">
</form>
</body>
</html>
5.3 更新笔记
点击修改可以对笔记进行更新
点击后会获取到之前的内容
随便改一改点击提交后,note_list会得到更新后的笔记内容
视图
如果是get就读取,如果是post就存储
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/css/initialization.css">
</head>
<body>
<form action="/note/edit_note/" method="post">
笔记标题<input type="text" name="title" value="{{title}}"><br>
笔记内容<textarea name="content" cols="30" rows="10">{{content}}</textarea><br>
<input type="submit" value="提交">
</form>
</body>
</html>
5.4 删除笔记
点击这里可以删除笔记
点击后删除的笔记就没了
视图
通过查询字符串拿到id,笔记的id是唯一的,与其他用户无关,比如一个用户的笔记id有1,7,8,另一个用户的笔记就不可能再有1,7,8
拿到id后删了,之后302回笔记列表
5.5 退出登录
点击这里可以退出登录
退出登录后返回登录页面
视图
6 提升
上面基本已经完成了云笔记的基本功能,下面我们对该项目进行一些提升
6.1 密码密文存储
6.1.1 哈希算法
密码密文存储用到了哈希算法,哈希算法多用于密码加密与文件的完整性校验
哈希算法有三个特点
- 无论多长的字符串通过哈希算法都会产生一个定长的字符串
- 很难破译
- 输入内容不同时,输出内容一定不同。输入内容相同时,输入内容一定相同
python中有一个自带的库hashlib可以做哈希算法,我们下面做一个例子
- 我们下面都以md5为例,如果想了解具体哈希算法的情况可以查一下相关的资料
m.update()的参数必须为字节字符串,我们可以使用encode()将普通字符串转换为字节字符串
通过哈希函数md5处理后,得出来的结果是32位
如果想算一个新的的哈希值需要重新搞一个md5对象
如果直接对m使用update(b'123'),相当于求123456123的哈希值,而不是123的
任何设备使用hashlib中的md5()只要输入相同,得到的结果一定相同
6.1.2 项目中应用
项目中涉及密码的地方都在用户应用中,我们首先来改注册
- 这里虽然名称每次都相同,但是每一次都会实例化一个对象,所以无所谓
这样注册后就会显示为密文了
之后修改登录页
修改之后可以正常登录
6.2 双重cookie验证
我们上面只设置了一个username_id的cookie,用户只需要改几个数就可以成功登录他人的笔记,这个安全性太低了,在我们增加完密码密文后,我们可以把密码的密文形式也加入到cookie中,这样两两配合,安全性更高
首先在登录的时候,要把密码存储进去
用户id与用户密码的密文形式,一般情况下用户自己也是不知道的,所以相对安全
之后修改列表页视图
如果你只有user_id或密码错误的时候,都会返回请不要修改cookie
6.3 同时注册同一个用户名的冲突
由于我们的表中的username设置的是unique,所以如果两台机子在同一时间起了同一个名字的时候会出现问题(并发写入问题)。
解决方式就是在添加到数据库之前做一个try,如果没try成功,就告诉用户该名字已被注册就行了
或者你告诉他出现了未知的错误,请重新操作
6.4 校验登录状态装饰器
我们有很多地方需要校验登录状态,比如登录页和主页,如果用户已经登录了,就不要再让他看见登录页了,这个时候我们可以搞一个校验登录状态的装饰器
装饰器的基本使用方法可以看一下这个 其余python操作_Suyuoa的博客-CSDN博客
我们在utils中创建一个py文件,名为check_login
逻辑是这样的,传入参数request,获取user_id和password,获取COOKIES的get如果获取不到也不会报错。之后去数据库拿到用户名与密码,在数据库中使用get如果拿不到是会报错的,所以用try搞一个容错。之后将COOKIES中的密码与数据库中的密码进行比对,如果比对成功就302到笔记列表页。如果比对不成功就会把这个请求放过去,该怎么着就怎么着,比如我访问了首页,比对之后发现不对,那么还是访问首页,没有变化
这里如果是第一次看装饰器可能看起来比较乱,我们理一下装饰器的执行顺序。首先要被校验的视图作为func传入外层check_login(),check_login()中有一个内层的check_login,def只定义,不执行,所以就走到了return check_login,现在return到了内层check_login,然后正式校验。此时request以及其余所有参数传进来(其余参数你可以传进来,怎么传进来怎么传出去,我不在这里进行任何处理),如果密码对了就302走了(这里注意302的路由上面不要加装饰器,不然会一直循环这个装饰器)。如果密码不对我一样要走,走的时候返回被校验是函数func,带着拿来的request以及其他参数一起走,也就是正常执行
之后我们在首页视图加入装饰器
- check_login中的func可以理解为要被检验的函数
再到登录页面加入装饰器
经测试如果cookie正常则访问首页与登录页均会跳转到笔记列表页,如果不正常就当没有这个校验器
当然像是添加笔记,删除笔记,编辑笔记,这些视图都有可能被直接用路由访问到,我们都可以加一个登录状态装饰器,装饰器这种东西复用起来也比较方便