Django 创建项目笔记
基本命令
mkdir mysite # 创建项目目录,常取名mysite
cd mysite
virtualenv env #
env\Scripts\activate.bat # Win
pip install django==2.1.3
python -m django --version # 查看django版本
where python
django-admin startproject mysite # 创建项目,也可以选择利用Pycharm创建一个Django项目
python manage.py startapp login # 创建app,也可以使用django-admin startapp login
python manage.py runserver [0.0.0.0:8000/0:8000/8000] # 如要能够被其他域的计算机访问,需要运行在0.0.0.0端口
设计数据模型
数据表的定义,在Django中就是model类的设计。
数据库模型设计
# login/models.py
from django.db import models
# Create your models here.
class User(models.Model):
gender = (
('male', "男"),
('female', "女"),
)
name = models.CharField(max_length=128, unique=True)
password = models.CharField(max_length=256)
email = models.EmailField(unique=True)
sex = models.CharField(max_length=32, choices=gender, default="男")
c_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
ordering = ["-c_time"]
verbose_name = "用户"
verbose_name_plural = "用户"
- name必填,最长不超过128个字符,并且唯一,也就是不能有相同姓名;
- password必填,最长不超过256个字符(实际可能不需要这么长);
- email使用Django内置的邮箱类型,并且唯一;
- 性别使用了一个choice,只能选择男或者女,默认为男;
- 使用__str__帮助人性化显示对象信息;
- 元数据里定义用户按创建时间的反序排列,也就是最近的最先显示;
注意:这里的用户名指的是网络上注册的用户名,不要等同于现实中的真实姓名,所以采用了唯一机制。如果是现实中可以重复的人名,那肯定是不能设置unique的。另外关于密码,建议至少128位长度,原因后面解释。
设置数据库后端
注册app
INSTALLED_APPS 字段加上新注册的app名字。在settings的下面部分添加‘login’,建议在最后添加个逗号。
创建记录和数据表
执行如下命令,Django自动为我们创建了login\migrations\0001_initial.py
文件,保存了我们的第一次数据迁移工作,也就是创建了User模型
(mysite_env) λ python manage.py makemigrations
Migrations for 'login':
login\migrations\0001_initial.py
- Create model User
执行如下命令才会真实创建表。Django将在数据库内创建真实的数据表。如果是第一次执行该命令,那么一些内置的框架,比如auth、session等的数据表也将被一同创建
(mysite_env) λ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, login, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying login.0001_initial... OK
Applying sessions.0001_initial... OK
admin后台
在admin中注册模型
进入login/admin.py文件,代码如下:
from django.contrib import admin
# Register your models here.
from . import models
admin.site.register(models.User)
创建超级管理员
python manage.py createsuperuser
# http://127.0.0.1:8000/admin/
url路由和视图
路由设计
URL | 视图 | 模板 | 说明 |
---|---|---|---|
/index/ | login.views.index() | index.html | 主页 |
/login/ | login.views.login() | login.html | 登录 |
/register/ | login.views.register() | register.html | 注册 |
/logout/ | login.views.logout() | 无需专门的页面 | 登出 |
架构初步视图
from django.shortcuts import render
from django.shortcuts import redirect
# Create your views here.
def index(request):
pass
return render(request, 'login/index.html')
def login(request):
pass
return render(request, 'login/login.html')
def register(request):
pass
return render(request, 'login/register.html')
def logout(request):
pass
return redirect("/index/")
- 四个视图都返回一个render()调用,render方法接收request作为第一个参数,要渲染的页面为第二个参数,以及需要传递给页面的数据字典作为第三个参数(可以为空),表示根据请求的部分,以渲染的HTML页面为主体,使用模板语言将数据字典填入,然后返回给用户的浏览器。
- 渲染的对象为login目录下的html文件,这是一种安全可靠的文件组织方式。
创建HTML页面
在项目根路径的login目录中创建一个templates目录,再在templates目录里创建一个login目录。这么做有助于app复用,防止命名冲突,能更有效地组织大型工程,具体说明请参考教程前面的相关章节。
在login/templates/login
目录中创建三个文件index.html
、login.html
以及register.html
前端页面设计
原生HTML页面
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div style="margin: 15% 40%;">
<h1>欢迎登录!</h1>
<form action="/login/" method="post">
<p>
<label for="id_username">用户名:</label>
<input type="text" id="id_username" name="username" placeholder="用户名" autofocus required />
</p>
<p>
<label for="id_password">密码:</label>
<input type="password" id="id_password" placeholder="密码" name="password" required >
</p>
<input type="submit" value="确定">
</form>
</div>
</body>
</html>
- form标签主要确定目的地url和发送方法;
- p标签将各个输入框分行;
- label标签为每个输入框提供一个前导提示,还有助于触屏使用;
- placeholder属性为输入框提供默认值;
- autofocus属性为用户名输入框自动聚焦
- required表示该输入框必须填写
- passowrd类型的input标签不会显示明文密码
引入Bootstrap
Bootstrap下载。
Django的根目录下新建一个static目录,并将解压后的bootstrap-3.3.7-dist目录,整体拷贝到static目录中
由于Bootstrap依赖JQuery,所以我们需要提前下载并引入JQuery,我这里使用的是jquery-3.2.1.js,当然别的版本也是可以的。
在static目录下,新建一个css和js目录,作为以后的样式文件和js文件的存放地,将我们的jquery文件拷贝到static/js目录下。
静态文件设置
打开项目的settings文件,在最下面添加这么一行配置,用于指定静态文件的搜索目录:
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
django的STATICFILES_FINDERS设置包含了一个会自己从一系列目录中找静态文件夹的配置,其中一个查找器会找每个INSTALLED_APPS的static目录
创建base.html模板
既然要将前端页面做得像个样子,那么就不能和前面一样,每个页面都各写各的,单打独斗。一个网站有自己的统一风格和公用部分,可以把这部分内容集中到一个基础模板base.html中。现在,在根目录下的templates中新建一个base.html文件用作站点的基础模板。
在Bootstrap文档中,为我们提供了一个非常简单而又实用的基本模板。
使用Boostrap静态文件
通过{% static '相对路径' %}
这个Django为我们提供的静态文件加载方法,可以将页面与静态文件链接起来。当然,还有一种办法是使用CDN,如果有可靠的源,也是可以使用的。
{% load staticfiles %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>{% block title %}base{% endblock %}</title>
<!-- Bootstrap -->
<link href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
{% block css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav" aria-expanded="false">
<span class="sr-only">切换导航条</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Mysite</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="my-nav">
<ul class="nav navbar-nav">
<li class="active"><a href="/index/">主页</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{% block content %}{% endblock %}
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{% static 'js/jquery-3.3.1.js' %}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
</body>
</html>
- 通过页面顶端的{% load staticfiles %}加载后,才可以使用static方法;
- 通过{% block title %}base{% endblock %},设置了一个动态的页面title块;
- 通过{% block css %}{% endblock %},设置了一个动态的css加载块;
- 通过{% block content %}{% endblock %},为具体页面的主体内容留下接口;
- 通过{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}将样式文件指向了我们的实际静态文件,下面的js脚本也是同样的道理。
设计登录页面
Bootstrap提供了一个基本的表单,我们结合Bootstrap和前面自己写的form表单,修改login/templates/login/login.html成符合项目要求的样子:
{% extends 'base.html' %}
{% load staticfiles %}
{% block title %}登录{% endblock %}
{% block css %}<link href="{% static 'css/login.css' %}" rel="stylesheet"/>{% endblock %}
{% block content %}
<div class="container">
<div class="col-md-4 col-md-offset-4">
<form class='form-login' action="/login/" method="post">
<h2 class="text-center">欢迎登录</h2>
<div class="form-group">
<label for="id_username">用户名:</label>
<input type="text" name='username' class="form-control" id="id_username" placeholder="Username" autofocus required>
</div>
<div class="form-group">
<label for="id_password">密码:</label>
<input type="password" name='password' class="form-control" id="id_password" placeholder="Password" required>
</div>
<button type="reset" class="btn btn-default pull-left">重置</button>
<button type="submit" class="btn btn-primary pull-right">提交</button>
</form>
</div>
</div> <!-- /container -->
{% endblock %}
- 通过{% extends 'base.html' %}继承了‘base.html’模板的内容;
- 通过{% block title %}登录{% endblock %}设置了专门的title;
- 通过block css引入了针对性的login.css样式文件;
- 主体内容定义在block content内部
- 添加了一个重置按钮。
在static/css目录中新建一个login.css样式文件,这里简单地写了点样式,不算复杂。
body {
background-color: #eee;
}
.form-login {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-login .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-login .form-control:focus {
z-index: 2;
}
.form-login input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-login input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
登录视图
登录视图
用户通过login.html中的表单填写用户名和密码,并以POST的方式发送到服务器的/login/地址。服务器通过login/views.py中的login()视图函数,接收并处理这一请求。
def login(request):
if request.method == "POST":
username = request.POST.get('username')
password = request.POST.get('password')
print(username, password)
return redirect('/index/')
return render(request, 'login/login.html')
- 每个视图函数都至少接收一个参数,并且是第一位置参数,该参数封装了当前请求的所有数据;
- 通常将第一参数命名为request,当然也可以是别的;
- request.method中封装了数据请求的方法,如果是“POST”(全大写),将执行if语句的内容,如果不是,直接返回最后的render()结果;
- request.POST封装了所有POST请求中的数据,这是一个字典类型,可以通过get方法获取具体的值。
- 类似get('username')中的键‘username’是HTML模板中表单的input元素里‘name’属性定义的值。所以在编写form表单的时候一定不能忘记添加name属性。
- 利用print函数在开发环境中验证数据;
- 利用redirect方法,将页面重定向到index页。
问题:"CSRF验证失败,请求被中断"
为了解决这个问题,我们需要在前端页面的form表单内添加一个{% csrf_token %}标签:
<form class='form-login' action="/login/" method="post">
{% csrf_token %}
<h2 class="text-center">欢迎登录</h2>
<div class="form-group">
......
</form>
- 这个标签必须放在form表单内部,但是内部的位置可以随意。
数据验证
数据验证分前端页面验证和后台服务器验证
通常,除了数据内容本身,我们至少需要保证各项内容都提供了且不为空,对于用户名、邮箱、地址等内容往往还需要剪去前后的空白,防止用户未注意到的空格。
验证用户名和密码
通过唯一的用户名,使用Django的ORM去数据库中查询用户数据,如果有匹配项,则进行密码对比,如果没有匹配项,说明用户名不存在。如果密码对比错误,说明密码不正确。
def login(request):
if request.method == "POST":
username = request.POST.get('username', None)
password = request.POST.get('password', None)
if username and password: # 确保用户名和密码都不为空
username = username.strip()
# 用户名字符合法性验证
# 密码长度验证
# 更多的其它验证.....
try:
user = models.User.objects.get(name=username)
except:
return render(request, 'login/login.html')
if user.password == password:
return redirect('/index/')
return render(request, 'login/login.html')
添加提示信息
无论是登录成功还是失败,用户都没有得到任何提示信息,这显然是不行的
Django表单
设想一下,如果我们的表单拥有几十上百个数据字段,有不同的数据特点,如果也使用手工的方式,其效率和正确性都将无法得到保障。有鉴于此,Django在内部集成了一个表单功能,以面向对象的方式,直接使用Python代码生成HTML表单代码,专门帮助我们快速处理表单相关的内容。
编写Django的form表单,非常类似我们在模型系统里编写一个模型。在模型中,一个字段代表数据表的一列,而form表单中的一个字段代表
图片验证
验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试。
图形验证码的历史比较悠久,到现在已经有点英雄末路的味道了。因为机器学习、图像识别的存在,机器人已经可以比较正确的识别图像内的字符了。但不管怎么说,作为一种防御手段,至少还是可以抵挡一些低级入门的攻击手段,抬高了攻击者的门槛。
在Django中实现图片验证码功能非常简单,有现成的第三方库可以使用,这个库叫做django-simple-captcha。
安装
pip install django-simple-captcha
注册captcha
在settings中,将‘captcha’注册到app列表里。
captcha需要在数据库中建立自己的数据表,所以需要执行migrate命令生成数据表:
(mysite_env) λ python manage.py makemigrations
No changes detected
D:\01-Code\Gitlab\z00422347\django-login\mysite (master -> origin)
(mysite_env) λ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, captcha, contenttypes, login, sessions
Running migrations:
Applying captcha.0001_initial... OK
添加路由
目录下的urls.py文件中增加captcha对应的网址
from django.contrib import admin
from django.urls import path
from django.urls import include
from login import views
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('login/', views.login),
path('register/', views.register),
path('logout/', views.logout),
path('captcha', include('captcha.urls')) # 增加这一行
]
使用了二级路由机制,需要在顶部from django.conf.urls import include
修改forms.py
上面都OK了,就可以直接在我们的forms.py文件中添加CaptchaField了。
from django import forms
from captcha.fields import CaptchaField
class UserForm(forms.Form):
username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))
password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))
captcha = CaptchaField(label='验证码')
需要提前导入from captcha.fields import CaptchaField
,然后就像写普通的form字段一样添加一个captcha字段就可以了!
修改login.html
额外增加了一条{{ login_form.captcha.errors }}
用于明确指示用户,你的验证码不正确
其中验证图形码是否正确的工作都是在后台自动完成的,只需要使用is_valid()这个forms内置的验证方法就一起进行了,完全不需要在视图函数中添加任何的验证代码,非常方便快捷!
session会话
为了实现连接状态的保持功能,网站会通过用户的浏览器在用户机器内被限定的硬盘位置中写入一些数据,也就是所谓的Cookie。通过Cookie可以保存一些诸如用户名、浏览记录、表单记录、登录和注销等各种数据。但是这种方式非常不安全,因为Cookie保存在用户的机器上,如果Cookie被伪造、篡改或删除,就会造成极大的安全威胁,因此,现代网站设计通常将Cookie用来保存一些不重要的内容,实际的用户数据和状态还是以Session会话的方式保存在服务器端。
Session依赖Cookie!但与Cookie不同的地方在于Session将所有的数据都放在服务器端,用户浏览器的Cookie中只会保存一个非明文的识别信息,比如哈希值。
Django提供了一个通用的Session框架,并且可以使用多种session数据的保存方式:
- 保存在数据库内
- 保存到缓存
- 保存到文件内
- 保存到cookie内
通常情况,没有特别需求的话,请使用保存在数据库内的方式,尽量不要保存到Cookie内。
Django的session框架默认启用,并已经注册在app设置内。
当session启用后,传递给视图request参数的HttpRequest对象将包含一个session属性,就像一个字典对象一样。你可以在Django的任何地方读写request.session属性
使用session
修改login/views.py中的login()视图函数
def login(request):
if request.session.get('is_login',None):
return redirect("/index/")
if request.method == "POST":
login_form = forms.UserForm(request.POST)
message = "请检查填写的内容!"
if login_form.is_valid():
username = login_form.cleaned_data['username']
password = login_form.cleaned_data['password']
try:
user = models.User.objects.get(name=username)
if user.password == password:
request.session['is_login'] = True
request.session['user_id'] = user.id
request.session['user_name'] = user.name
return redirect('/index/')
else:
message = "密码不正确!"
except:
message = "用户不存在!"
return render(request, 'login/login.html', locals())
既然有了session记录用户登录状态,那么就可以完善登出视图函数了:
def logout(request):
if not request.session.get('is_login', None):
# 如果本来就未登录,也就没有登出一说
return redirect("/index/")
request.session.flush()
# 或者使用下面的方法
# del request.session['is_login']
# del request.session['user_id']
# del request.session['user_name']
return redirect("/index/")
完善页面
有了用户状态,就可以根据用户登录与否,展示不同的页面,比如导航条内容
使用Github管理项目
创建requirements.txt
pip freeze > ./requirements.txt
一键安装:
pip install -r requirements.txt
创建.gitignore文件
使用ignore插件
特殊文件处理
对于settings.py文件有个问题,如果没有这个文件是无法运行Django项目的,但是settings中又可能包含很多关键的不可泄露的部分,比如SECRET_KEY。还有数据库的IP/port、用户名和密码,邮件发送端的用户名和密码,这些都是绝对不能泄露的。
那怎么办呢?简单!复制settings文件,并重命名为settings.example.py文件,放在同一目录里,把敏感信息、密码等修改或删除。使用者看到这个文件名,自然会明白它的作用。
添加说明文件和许可证
在项目根目录下创建一个README.md文件,这是markdown格式的。在文件里写入项目说明,使用方法,注意事项等等所有你认为需要说明的东西。
对于许可文件LICENSE,如果你暂时不想公开授权,或者不知道用哪种授权,可以暂时不提供。
使用Github源码
如果你不是从教程的开始一步步地实现整个项目,而是直接使用从Github上copy下来的整个源码,那么你可能需要做一些额外的工作,比如:
- 创建虚拟环境,建议虚拟环境目录为env或venv
- 使用pip安装第三方依赖
- 修改settings.example.py文件为settings.py
- 运行migrate命令,创建数据库和数据表
- 运行python manage.py runserver启动服务器
而在Pycharm中运行服务器的话,可能还需要做一些额外的工作,比如:
- 配置解释器:host和虚拟环境的解释器
- 配置启动参数:设置中的languages&frameworks>django,django project root和manage script配置
FAQ
Q1:报错'%s=%s' % (k, v) for k, v in params.items(),
File "D:\01-Code\Github-Other\liujiangblog_project_2\venv\lib\site-packages\django\contrib\admin\widgets.py", line 151
'%s=%s' % (k, v) for k, v in params.items(),
原因:Python3.7版本和老版本的Django不适配,Python3.6无此问题,或者Python3.7+Django2版本。
解决修复:python3.7报错 原
参考
- 实战一:用户登录与注册系统:本文主要参考文章
- 编写你的第一个 Django 应用:Django官宣入门教程