每當(dāng)我們開(kāi)始一個(gè)新項(xiàng)目的時(shí)候,通常都會(huì)搭建一個(gè)全新、獨(dú)立、隔離的項(xiàng)目環(huán)境,這樣做的好處自然不必多說(shuō)。有很多種建立項(xiàng)目虛擬環(huán)境的工具,使用比較普遍的是Python中的virtualenv。安裝好virtualenv工具后,進(jìn)入想要放置的項(xiàng)目文件夾,建立一個(gè)虛擬環(huán)境,激活環(huán)境,安裝django。
virtualenv -p python3 venv #創(chuàng)建虛擬環(huán)境source venv/bin/activate #激活虛擬環(huán)境pip install django==1.11.7 #安裝django
在當(dāng)前虛擬環(huán)境,創(chuàng)建django項(xiàng)目,完成后會(huì)生成login_site項(xiàng)目文件夾,進(jìn)入,運(yùn)行django內(nèi)置服務(wù)器,在本機(jī)的瀏覽器中訪問(wèn)http://127.0.0.1:8000/
,這時(shí)我們的django服務(wù)已經(jīng)跑起來(lái)了。
django-admin startproject login_sitecd login_sitepython manage.py startapp login #創(chuàng)建apppython manage.py runserver
Django默認(rèn)使用美國(guó)時(shí)間和英語(yǔ),我們可以將時(shí)間和語(yǔ)言更改一下。配置文件主要在setting.py中,
#beforeLANGUAGE_CODE = 'en-us'TIME_ZONE = 'UTC'USE_I18N = TrueUSE_L10N = TrueUSE_TZ = True#afterLANGUAGE_CODE = 'zh-hans'TIME_ZONE = 'Asia/Shanghai'USE_I18N = TrueUSE_L10N = TrueUSE_TZ = True時(shí)間/語(yǔ)言設(shè)置
作為一個(gè)用戶登錄和注冊(cè)項(xiàng)目,需要保存的都是各種用戶的相關(guān)信息。很顯然,我們至少需要一張用戶表User,在用戶表里需要保存下面的信息:
進(jìn)入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 = "用戶"
各字段含義:
__str__
幫助人性化顯示對(duì)象信息;注意:這里的用戶名指的是網(wǎng)絡(luò)上注冊(cè)的用戶名,不要等同于現(xiàn)實(shí)中的真實(shí)姓名,所以采用了唯一機(jī)制。如果是現(xiàn)實(shí)中可以重復(fù)的人名,那肯定是不能設(shè)置unique的。
在settings.py修改,一定要加上前面的導(dǎo)入?;蚴窃趇nit.py里面導(dǎo)入pymysql模塊。
import pymysql # 一定要添加這兩行! pymysql.install_as_MySQLdb()DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'loginsys', #數(shù)據(jù)庫(kù)名字 'USER': 'root', #賬號(hào) 'PASSWORD': '123456', #密碼 'HOST': '127.0.0.1', #IP 'PORT': '3306', #端口 }}
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'login',]
數(shù)據(jù)庫(kù)遷移,每次models.py有更改,都要應(yīng)用以下命令。
python manage.py makemigrationspython manage.py migrate
# login/admin.pyfrom django.contrib import adminfrom . import modelsadmin.site.register(models.User)
python manage.py createsuperuser
創(chuàng)建好超級(jí)管理員后,就可以啟動(dòng)我們的開(kāi)發(fā)服務(wù)器了,然后在瀏覽器中訪問(wèn)http://127.0.0.1:8000/admin/
地址
注意,圖中下方的認(rèn)證和授權(quán)
是admin應(yīng)用自身的賬戶管理,上面的LOGIN欄目才是我們自己創(chuàng)建的login應(yīng)用所對(duì)應(yīng)的User模型。
增加測(cè)試用戶,
初步設(shè)想需要下面的四個(gè)URL:
URL | 視圖 | 模板 | 說(shuō)明 |
---|---|---|---|
/index/ | login.views.index() | index.html | 主頁(yè) |
/login/ | login.views.login() | login.html | 登錄 |
/register/ | login.views.register() | register.html | 注冊(cè) |
/logout/ | login.views.logout() | 無(wú)需專門的頁(yè)面 | 登出 |
考慮到登錄系統(tǒng)屬于站點(diǎn)的一級(jí)功能,為了直觀和更易于接受,這里沒(méi)有采用二級(jí)路由的方式,而是在根路由下直接編寫(xiě)路由條目,同樣也沒(méi)有使用反向解析名(name參數(shù))。
# login_sys/urls.pyfrom django.conf.urls import urlfrom django.contrib import adminfrom login import viewsurlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^index/', views.index), url(r'^login/', views.login), url(r'^register/', views.register), url(r'^logout/', views.logout),]
路由寫(xiě)好了,就進(jìn)入login/views.py
文件編寫(xiě)視圖的框架,代碼如下:
# login/views.pyfrom django.shortcuts import render,redirectdef 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/')
我們先不著急完成視圖內(nèi)部的具體細(xì)節(jié),而是把框架先搭建起來(lái)。
在項(xiàng)目根路徑的login目錄中創(chuàng)建一個(gè)templates目錄,再在templates目錄里創(chuàng)建一個(gè)login目錄
在login/templates/login
目錄中創(chuàng)建三個(gè)文件index.html
、login.html
以及register.html
,并寫(xiě)入如下的代碼:
{#login/templates/login/index.html#}<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>首頁(yè)</title></head><body><h1>首頁(yè)</h1></body></html>{#login/templates/login/login.html#}<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>登錄</title></head><body><h1>登錄頁(yè)面</h1></body></html>{#login/templates/login/register.html#}<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>注冊(cè)</title></head><body><h1>注冊(cè)頁(yè)面</h1></body></html>
保存后,運(yùn)行服務(wù),試試訪問(wèn)下以上路由頁(yè)面。
刪除原來(lái)的login.html
文件中的內(nèi)容,寫(xiě)入下面的代碼:
<!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>
保存,啟動(dòng)服務(wù)器,可以看到如下圖的頁(yè)面:
下載生產(chǎn)環(huán)境下的bootstrap,在項(xiàng)目根目錄(manage.py同級(jí))下新建一個(gè)static目錄,并將解壓后的bootstrap-3.3.7-dist
目錄,整體拷貝到static目錄中,如下圖所示:
由于Bootstrap依賴JQuery,所以我們需要提前下載并引入JQuery,在static目錄下,新建一個(gè)css和js目錄,作為以后的樣式文件和js文件的存放地,將我們的jquery文件拷貝到static/js
目錄下。
然后打開(kāi)項(xiàng)目的settings文件,在最下面添加配置,用于指定靜態(tài)文件的搜索目錄:
STATIC_URL = '/static/'STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"),]
既然要將前端頁(yè)面做得像個(gè)樣子,那么就不能和前面一樣,每個(gè)頁(yè)面都各寫(xiě)各的,單打獨(dú)斗。一個(gè)網(wǎng)站有自己的統(tǒng)一風(fēng)格和公用部分,可以把這部分內(nèi)容集中到一個(gè)基礎(chǔ)模板base.html
中?,F(xiàn)在,在根目錄下的templates中新建一個(gè)base.html
文件用作站點(diǎn)的基礎(chǔ)模板。
在Bootstrap文檔中,為我們提供了一個(gè)非常簡(jiǎn)單而又實(shí)用的基本模板,代碼如下:
<!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個(gè)meta標(biāo)簽*必須*放在最前面,任何其他內(nèi)容都*必須*跟隨其后! --> <title>Bootstrap 101 Template</title> <!-- Bootstrap --> <link href="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]--> </head> <body> <h1>你好,世界!</h1> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="js/bootstrap.min.js"></script> </body></html>
將它整體拷貝到base.html
文件中。
Bootstrap提供了現(xiàn)成的導(dǎo)航條組件
<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="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Brand</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> <li role="separator" class="divider"></li> <li><a href="#">One more separated link</a></li> </ul> </li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Search"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> <ul class="nav navbar-nav navbar-right"> <li><a href="#">Link</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> </ul> </li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --></nav>
其中有一些部分,比如搜索框是我們目前還不需要的,需要將多余的內(nèi)容裁剪掉。同時(shí),有一些名稱和url地址等需要按我們的實(shí)際內(nèi)容修改。最終導(dǎo)航條的代碼如下:
<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">切換導(dǎo)航條</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/">主頁(yè)</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/login/">登錄</a></li> <li><a href="/register/">注冊(cè)</a></li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav>
{% static '相對(duì)路徑' %}
這個(gè)Django為我們提供的靜態(tài)文件加載方法,可以將頁(yè)面與靜態(tài)文件鏈接起來(lái)
{% 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個(gè)meta標(biāo)簽*必須*放在最前面,任何其他內(nèi)容都*必須*跟隨其后! --> <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">切換導(dǎo)航條</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/">主頁(yè)</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/login/">登錄</a></li> <li><a href="/register/">注冊(cè)</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>
簡(jiǎn)要說(shuō)明:
{% load staticfiles %}
加載后,才可以使用static方法;{% block title %}base{% endblock %}
,設(shè)置了一個(gè)動(dòng)態(tài)的頁(yè)面title塊;{% block css %}{% endblock %}
,設(shè)置了一個(gè)動(dòng)態(tài)的css加載塊;{% block content %}{% endblock %}
,為具體頁(yè)面的主體內(nèi)容留下接口;{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}
將樣式文件指向了我們的實(shí)際靜態(tài)文件,下面的js腳本也是同樣的道理。看下效果
Bootstarp提供了一個(gè)基本的表單樣式,代碼如下:
<form> <div class="form-group"> <label for="exampleInputEmail1">Email address</label> <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Email"> </div> <div class="form-group"> <label for="exampleInputPassword1">Password</label> <input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password"> </div> <div class="form-group"> <label for="exampleInputFile">File input</label> <input type="file" id="exampleInputFile"> <p class="help-block">Example block-level help text here.</p> </div> <div class="checkbox"> <label> <input type="checkbox"> Check me out </label> </div> <button type="submit" class="btn btn-default">Submit</button></form>
我們結(jié)合Bootstrap和前面自己寫(xiě)的form表單,修改login/templates/login/login.html
成符合項(xiàng)目要求的樣子:
{% extends 'login/base.html' %}{% load staticfiles %}{% block title %}登錄{% endblock %}{% block css %} <link rel="stylesheet" href="{% static 'css/login.css' %}">{% 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 %}
說(shuō)明:
{% extends 'base.html' %}
繼承了‘base.html’模板的內(nèi)容;{% block title %}登錄{% endblock %}
設(shè)置了專門的title;block css
引入了針對(duì)性的login.css
樣式文件;block content
內(nèi)部在static/css
目錄中新建一個(gè)login.css
樣式文件,這里簡(jiǎn)單地寫(xiě)了點(diǎn)樣式,
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;}
最后效果:
根據(jù)我們?cè)诼酚芍械脑O(shè)計(jì),用戶通過(guò)login.html
中的表單填寫(xiě)用戶名和密碼,并以POST的方式發(fā)送到服務(wù)器的/login/
地址。服務(wù)器通過(guò)login/views.py
中的login()
視圖函數(shù),接收并處理這一請(qǐng)求。
我們可以通過(guò)下面的方法接收和處理請(qǐng)求:
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')
還需要在前端頁(yè)面的form表單內(nèi)添加一個(gè){% csrf_token %}
標(biāo)簽,CSRF(Cross-site request forgery)跨站請(qǐng)求偽造,是一種常見(jiàn)的網(wǎng)絡(luò)攻擊手段,具體原理和技術(shù)內(nèi)容請(qǐng)自行百科。Django自帶對(duì)許多常見(jiàn)攻擊手段的防御機(jī)制,CSRF就是其中一種,還有XSS、SQL注入等。
<form class='form-login' action="/login/" method="post"> {% csrf_token %} <h2 class="text-center">歡迎登錄</h2> <div class="form-group"> ......</form>
這個(gè)標(biāo)簽必須放在form表單內(nèi)部,但是內(nèi)部的位置可以隨意。
重新刷新login頁(yè)面,確保csrf的標(biāo)簽生效,然后再次輸入內(nèi)容并提交。瀏覽器頁(yè)面跳轉(zhuǎn)到了首頁(yè)。
通過(guò)唯一的用戶名,使用Django的ORM去數(shù)據(jù)庫(kù)中查詢用戶數(shù)據(jù),如果有匹配項(xiàng),則進(jìn)行密碼對(duì)比,如果沒(méi)有匹配項(xiàng),說(shuō)明用戶名不存在。如果密碼對(duì)比錯(cuò)誤,說(shuō)明密碼不正確。
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() # 用戶名字符合法性驗(yàn)證 # 密碼長(zhǎng)度驗(yàn)證 # 更多的其它驗(yàn)證..... 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')
上面的代碼還缺少很重要的一部分內(nèi)容,提示信息!無(wú)論是登錄成功還是失敗,用戶都沒(méi)有得到任何提示信息,這顯然是不行的。
修改一下login視圖:
def login(request): if request.method == "POST": username = request.POST.get('username', None) password = request.POST.get('password', None) message = "所有字段都必須填寫(xiě)!" if username and password: # 確保用戶名和密碼都不為空 username = username.strip() # 用戶名字符合法性驗(yàn)證 # 密碼長(zhǎng)度驗(yàn)證 # 更多的其它驗(yàn)證..... try: user = models.User.objects.get(name=username) if user.password == password: return redirect('/index/') else: message = "密碼不正確!" except: message = "用戶名不存在!" return render(request, 'login/login.html', {"message": message}) return render(request, 'login/login.html')
增加了message變量,用于保存提示信息。當(dāng)有錯(cuò)誤信息的時(shí)候,將錯(cuò)誤信息打包成一個(gè)字典,然后作為第三個(gè)參數(shù)提供給render()方法。這個(gè)數(shù)據(jù)字典在渲染模板的時(shí)候會(huì)傳遞到模板里供你調(diào)用。
為了在前端頁(yè)面顯示信息,還需要對(duì)login.html
進(jìn)行修改:
{% extends 'login/base.html' %}{% load staticfiles %}{% block title %}登錄{% endblock %}{% block css %} <link rel="stylesheet" href="{% static 'css/login.css' %}">{% endblock %}{% block content %} <div class="container"> <div class="col-md-4 col-md-offset-4"> <form class='form-login' action="/login/" method="post"> {% if message %} <div class="alert alert-warning">{{ message }}</div> {% endif %} {% csrf_token %} <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 %}
把index.html
主頁(yè)模板也修改一下,刪除原有內(nèi)容,添加下面的代碼:
{#login/templates/login/index.html#}{% extends 'login/base.html' %}{% block title %}主頁(yè){% endblock %}{% block content %} <h1>歡迎回來(lái)!</h1>{% endblock %}
Django的表單給我們提供了下面三個(gè)主要功能:
編寫(xiě)Django的form表單,非常類似我們?cè)谀P拖到y(tǒng)里編寫(xiě)一個(gè)模型。在模型中,一個(gè)字段代表數(shù)據(jù)表的一列,而form表單中的一個(gè)字段代表<form>
中的一個(gè)<input>
元素。
from django import formsclass UserForm(forms.Form): username = forms.CharField(label="用戶名", max_length=128) password = forms.CharField(label="密碼", max_length=256, widget=forms.PasswordInput)
說(shuō)明:
<form>
內(nèi)的一個(gè)input元素。這一點(diǎn)和Django模型系統(tǒng)的設(shè)計(jì)非常相似。<label>
標(biāo)簽max_length
限制字段輸入的最大長(zhǎng)度。它同時(shí)起到兩個(gè)作用,一是在瀏覽器頁(yè)面限制用戶輸入不可超過(guò)字符數(shù),二是在后端服務(wù)器驗(yàn)證用戶輸入的長(zhǎng)度也不可超過(guò)。widget=forms.PasswordInput
用于指定該字段在form表單里表現(xiàn)為<input type='password' />
,也就是密碼輸入框。使用了Django的表單后,就要在視圖中進(jìn)行相應(yīng)的修改:
# login/views.pyfrom django.shortcuts import render,redirectfrom . import modelsfrom .forms import UserFormdef index(request): pass return render(request,'login/index.html')def login(request): if request.method == "POST": login_form = UserForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" 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: return redirect('/index/') else: message = "密碼不正確!" except: message = "用戶不存在!" return render(request, 'login/login.html', locals()) login_form = UserForm() return render(request, 'login/login.html', locals())
說(shuō)明:
is_valid()
方法一步完成數(shù)據(jù)驗(yàn)證工作;cleaned_data
數(shù)據(jù)字典中獲取表單的具體值;另外,這里使用了一個(gè)小技巧,Python內(nèi)置了一個(gè)locals()函數(shù),它返回當(dāng)前所有的本地變量字典,我們可以偷懶的將這作為render函數(shù)的數(shù)據(jù)字典參數(shù)值,就不用費(fèi)勁去構(gòu)造一個(gè)形如{'message':message, 'login_form':login_form}
的字典了。這樣做的好處當(dāng)然是大大方便了我們,但是同時(shí)也可能往模板傳入了一些多余的變量數(shù)據(jù),造成數(shù)據(jù)冗余降低效率。
Django的表單很重要的一個(gè)功能就是自動(dòng)生成HTML的form表單內(nèi)容?,F(xiàn)在,我們需要修改一下原來(lái)的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"> {% if message %} <div class="alert alert-warning">{{ message }}</div> {% endif %} {% csrf_token %} <h2 class="text-center">歡迎登錄</h2> {{ login_form }} <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 %}
瀏覽器生成的HTML源碼
重新啟動(dòng)服務(wù)器,刷新頁(yè)面,如下圖所示:
<form class='form-login' action="/login/" method="post"> <div class="alert alert-warning">密碼不正確!</div> <input type='hidden' name='csrfmiddlewaretoken' value='t7MdqJzR7fbiDth5ZQSBpHb22F8sUkjTy32MlEuhXdW8EZPTwcTNuF0PPOHlxKPz' /> <h2 class="text-center">歡迎登錄</h2> <tr><th><label for="id_username">用戶名:</label></th><td><input type="text" name="username" value="jack" maxlength="128" required id="id_username" /></td></tr> <tr><th><label for="id_password">密碼:</label></th><td><input type="password" name="password" maxlength="256" required id="id_password" /></td></tr> <button type="reset" class="btn btn-default pull-left">重置</button> <button type="submit" class="btn btn-primary pull-right">提交</button></form>
直接{{ login_form }}
雖然好,啥都不用操心,但是界面真的很丑,往往并不是你想要的,如果你要使用CSS和JS,比如你要引入Bootstarps框架,這些都需要對(duì)表單內(nèi)的input元素進(jìn)行額外控制,那怎么辦呢?手動(dòng)渲染字段就可以了。
可以通過(guò){{ login_form.name_of_field }}
獲取每一個(gè)字段,然后分別渲染,如下例所示:
<div class="form-group"> {{ login_form.username.label_tag }} {{ login_form.username}}</div><div class="form-group"> {{ login_form.password.label_tag }} {{ login_form.password }}</div>
然后,在form類里添加attr屬性即可,如下所示修改login/forms.py
from django import formsclass 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'}))
再次刷新頁(yè)面,就顯示正常了!
為了防止機(jī)器人頻繁登錄網(wǎng)站或者破壞分子惡意登錄,很多用戶登錄和注冊(cè)系統(tǒng)都提供了圖形驗(yàn)證碼功能。
驗(yàn)證碼(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自動(dòng)區(qū)分計(jì)算機(jī)和人類的圖靈測(cè)試)的縮寫(xiě),是一種區(qū)分用戶是計(jì)算機(jī)還是人的公共全自動(dòng)程序??梢苑乐箰阂馄平饷艽a、刷票、論壇灌水,有效防止某個(gè)黑客對(duì)某一個(gè)特定注冊(cè)用戶用特定程序暴力破解方式進(jìn)行不斷的登陸嘗試。
圖形驗(yàn)證碼的歷史比較悠久,到現(xiàn)在已經(jīng)有點(diǎn)英雄末路的味道了。因?yàn)闄C(jī)器學(xué)習(xí)、圖像識(shí)別的存在,機(jī)器人已經(jīng)可以比較正確的識(shí)別圖像內(nèi)的字符了。但不管怎么說(shuō),作為一種防御手段,至少還是可以抵擋一些低級(jí)入門的攻擊手段,抬高了攻擊者的門檻。
在Django中實(shí)現(xiàn)圖片驗(yàn)證碼功能非常簡(jiǎn)單,有現(xiàn)成的第三方庫(kù)可以使用,我們不必自己開(kāi)發(fā)(也要能開(kāi)發(fā)得出來(lái),囧)。這個(gè)庫(kù)叫做django-simple-captcha
。
直接安裝:pip install django-simple-captcha
Django自動(dòng)幫我們安裝了相關(guān)的依賴庫(kù)six
、olefile
和Pillow
,其中的Pillow是大名鼎鼎的繪圖模塊。
注冊(cè)captcha
在settings中,將‘captcha’注冊(cè)到app列表里:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'login', 'captcha',]
captcha需要在數(shù)據(jù)庫(kù)中建立自己的數(shù)據(jù)表,所以需要執(zhí)行migrate命令生成數(shù)據(jù)表:
python manage.py makemigrationspython manage.py migrate
根目錄下的urls.py文件中增加captcha對(duì)應(yīng)的網(wǎng)址:
from django.conf.urls import urlfrom django.conf.urls import includefrom django.contrib import adminfrom login import viewsurlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^index/', views.index), url(r'^login/', views.login), url(r'^register/', views.register), url(r'^logout/', views.logout), url(r'^captcha', include('captcha.urls')) # 增加這一行]
如果上面都OK了,就可以直接在我們的forms.py文件中添加CaptchaField了。
from django import formsfrom captcha.fields import CaptchaFieldclass 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='驗(yàn)證碼')
需要提前導(dǎo)入from captcha.fields import CaptchaField
,然后就像寫(xiě)普通的form字段一樣添加一個(gè)captcha字段就可以了!
由于我們前面是手動(dòng)生成的form表單,所以還要修改一下,添加captcha的相關(guān)內(nèi)容,如下所示:
{% extends 'login/base.html' %}{% load staticfiles %}{% block title %}登錄{% endblock %}{% block css %} <link rel="stylesheet" href="{% static 'css/login.css' %}">{% endblock %}{% block content %} <div class="container"> <div class="col-md-4 col-md-offset-4"> <form class='form-login' action="/login/" method="post"> {% if message %} <div class="alert alert-warning">{{ message }}</div> {% endif %} {% csrf_token %} <h2 class="text-center">歡迎登錄</h2> <div class="form-group"> {{ login_form.username.label_tag }} {{ login_form.username}} </div> <div class="form-group"> {{ login_form.password.label_tag }} {{ login_form.password }} </div> <div class="form-group"> {{ login_form.captcha.errors }} {{ login_form.captcha.label_tag }} {{ login_form.captcha }} </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 %}
這里額外增加了一條{{ login_form.captcha.errors }}
用于明確指示用戶,你的驗(yàn)證碼不正確
查看效果:
其中驗(yàn)證圖形碼是否正確的工作都是在后臺(tái)自動(dòng)完成的,只需要使用is_valid()
這個(gè)forms內(nèi)置的驗(yàn)證方法就一起進(jìn)行了,完全不需要在視圖函數(shù)中添加任何的驗(yàn)證代碼,非常方便快捷!
因?yàn)橐蛱鼐W(wǎng)HTTP協(xié)議的特性,每一次來(lái)自于用戶瀏覽器的請(qǐng)求(request)都是無(wú)狀態(tài)的、獨(dú)立的。通俗地說(shuō),就是無(wú)法保存用戶狀態(tài),后臺(tái)服務(wù)器根本就不知道當(dāng)前請(qǐng)求和以前及以后請(qǐng)求是否來(lái)自同一用戶。對(duì)于靜態(tài)網(wǎng)站,這可能不是個(gè)問(wèn)題,而對(duì)于動(dòng)態(tài)網(wǎng)站,尤其是京東、天貓、銀行等購(gòu)物或金融網(wǎng)站,無(wú)法識(shí)別用戶并保持用戶狀態(tài)是致命的,根本就無(wú)法提供服務(wù)。你可以嘗試將瀏覽器的cookie功能關(guān)閉,你會(huì)發(fā)現(xiàn)將無(wú)法在京東登錄和購(gòu)物。
為了實(shí)現(xiàn)連接狀態(tài)的保持功能,網(wǎng)站會(huì)通過(guò)用戶的瀏覽器在用戶機(jī)器內(nèi)被限定的硬盤(pán)位置中寫(xiě)入一些數(shù)據(jù),也就是所謂的Cookie。通過(guò)Cookie可以保存一些諸如用戶名、瀏覽記錄、表單記錄、登錄和注銷等各種數(shù)據(jù)。但是這種方式非常不安全,因?yàn)镃ookie保存在用戶的機(jī)器上,如果Cookie被偽造、篡改或刪除,就會(huì)造成極大的安全威脅,因此,現(xiàn)代網(wǎng)站設(shè)計(jì)通常將Cookie用來(lái)保存一些不重要的內(nèi)容,實(shí)際的用戶數(shù)據(jù)和狀態(tài)還是以Session會(huì)話的方式保存在服務(wù)器端。
Session依賴Cookie!但與Cookie不同的地方在于Session將所有的數(shù)據(jù)都放在服務(wù)器端,用戶瀏覽器的Cookie中只會(huì)保存一個(gè)非明文的識(shí)別信息,比如哈希值。
Django提供了一個(gè)通用的Session框架,并且可以使用多種session數(shù)據(jù)的保存方式:
通常情況,沒(méi)有特別需求的話,請(qǐng)使用保存在數(shù)據(jù)庫(kù)內(nèi)的方式,盡量不要保存到Cookie內(nèi)。
Django的session框架默認(rèn)啟用,并已經(jīng)注冊(cè)在app設(shè)置內(nèi),如果真的沒(méi)有啟用,那么參考下面的內(nèi)容添加有說(shuō)明的那兩行,再執(zhí)行migrate命令創(chuàng)建數(shù)據(jù)表,就可以使用session了。
# Application definitionINSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', # 這一行 'django.contrib.messages', 'django.contrib.staticfiles',]MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # 這一行 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',]
當(dāng)session啟用后,傳遞給視圖request參數(shù)的HttpRequest對(duì)象將包含一個(gè)session屬性,就像一個(gè)字典對(duì)象一樣。你可以在Django的任何地方讀寫(xiě)request.session
屬性,或者多次編輯使用它。
下面是session使用參考:
class backends.base.SessionBase # 這是所有會(huì)話對(duì)象的基類,包含標(biāo)準(zhǔn)的字典方法: __getitem__(key) Example: fav_color = request.session['fav_color'] __setitem__(key, value) Example: request.session['fav_color'] = 'blue' __delitem__(key) Example: del request.session['fav_color'] # 如果不存在會(huì)拋出異常 __contains__(key) Example: 'fav_color' in request.session get(key, default=None) Example: fav_color = request.session.get('fav_color', 'red') pop(key, default=__not_given) Example: fav_color = request.session.pop('fav_color', 'blue')
# 類似字典數(shù)據(jù)類型的內(nèi)置方法 keys() items() setdefault() clear() # 它還有下面的方法: flush() # 刪除當(dāng)前的會(huì)話數(shù)據(jù)和會(huì)話cookie。經(jīng)常用在用戶退出后,刪除會(huì)話。 set_test_cookie() # 設(shè)置一個(gè)測(cè)試cookie,用于探測(cè)用戶瀏覽器是否支持cookies。由于cookie的工作機(jī)制,你只有在下次用戶請(qǐng)求的時(shí)候才可以測(cè)試。 test_cookie_worked() # 返回True或者False,取決于用戶的瀏覽器是否接受測(cè)試cookie。你必須在之前先調(diào)用set_test_cookie()方法。 delete_test_cookie() # 刪除測(cè)試cookie。 set_expiry(value) # 設(shè)置cookie的有效期??梢詡鬟f不同類型的參數(shù)值: · 如果值是一個(gè)整數(shù),session將在對(duì)應(yīng)的秒數(shù)后失效。例如request.session.set_expiry(300) 將在300秒后失效. · 如果值是一個(gè)datetime或者timedelta對(duì)象, 會(huì)話將在指定的日期失效 · 如果為0,在用戶關(guān)閉瀏覽器后失效 · 如果為None,則將使用全局會(huì)話失效策略 失效時(shí)間從上一次會(huì)話被修改的時(shí)刻開(kāi)始計(jì)時(shí)。 get_expiry_age() # 返回多少秒后失效的秒數(shù)。對(duì)于沒(méi)有自定義失效時(shí)間的會(huì)話,這等同于SESSION_COOKIE_AGE. # 這個(gè)方法接受2個(gè)可選的關(guān)鍵字參數(shù) · modification:會(huì)話的最后修改時(shí)間(datetime對(duì)象)。默認(rèn)是當(dāng)前時(shí)間。 ·expiry: 會(huì)話失效信息,可以是datetime對(duì)象,也可以是int或None get_expiry_date() # 和上面的方法類似,只是返回的是日期 get_expire_at_browser_close() # 返回True或False,根據(jù)用戶會(huì)話是否是瀏覽器關(guān)閉后就結(jié)束。 clear_expired() # 刪除已經(jīng)失效的會(huì)話數(shù)據(jù)。 cycle_key() # 創(chuàng)建一個(gè)新的會(huì)話秘鑰用于保持當(dāng)前的會(huì)話數(shù)據(jù)。django.contrib.auth.login() 會(huì)調(diào)用這個(gè)方法。
首先,修改login/views.py
中的login()視圖函數(shù):
def login(request): if request.session.get('is_login',None): return redirect('/index') if request.method == "POST": login_form = UserForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" 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()) login_form = UserForm() return render(request, 'login/login.html', locals())
通過(guò)下面的if語(yǔ)句,我們不允許重復(fù)登錄:
if request.session.get('is_login',None): return redirect("/index/")
通過(guò)下面的語(yǔ)句,我們往session字典內(nèi)寫(xiě)入用戶狀態(tài)和數(shù)據(jù):
request.session['is_login'] = Truerequest.session['user_id'] = user.idrequest.session['user_name'] = user.name
你完全可以往里面寫(xiě)任何數(shù)據(jù),不僅僅限于用戶相關(guān)!
既然有了session記錄用戶登錄狀態(tài),那么就可以完善我們的登出視圖函數(shù)了:
def logout(request): if not request.session.get('is_login', None): # 如果本來(lái)就未登錄,也就沒(méi)有登出一說(shuō) return redirect("/index/") request.session.flush() # 或者使用下面的方法 # del request.session['is_login'] # del request.session['user_id'] # del request.session['user_name'] return redirect("/index/")
flush()方法是比較安全的一種做法,而且一次性將session中的所有內(nèi)容全部清空,確保不留后患。但也有不好的地方,那就是如果你在session中夾帶了一點(diǎn)‘私貨’,會(huì)被一并刪除,這一點(diǎn)一定要注意。
有了用戶狀態(tài),就可以根據(jù)用戶登錄與否,展示不同的頁(yè)面,比如導(dǎo)航條內(nèi)容:
首先,修改base.html
文件:
<div class="collapse navbar-collapse" id="my-nav"> <ul class="nav navbar-nav"> <li class="active"><a href="/index/">主頁(yè)</a></li> </ul> <ul class="nav navbar-nav navbar-right"> {% if request.session.is_login %} <li><a href="#">當(dāng)前在線:{{ request.session.user_name }}</a></li> <li><a href="/logout/">登出</a></li> {% else %} <li><a href="/login/">登錄</a></li> <li><a href="/register/">注冊(cè)</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid -->
通過(guò)if判斷,當(dāng)?shù)卿洉r(shí),顯示當(dāng)前用戶名和登出按鈕。未登錄時(shí),顯示登錄和注冊(cè)按鈕。
注意其中的模板語(yǔ)言,{{ request }}
這個(gè)變量會(huì)被默認(rèn)傳入模板中,可以通過(guò)圓點(diǎn)的調(diào)用方式,獲取它內(nèi)部的{{ request.session }}
,再進(jìn)一步的獲取session中的內(nèi)容。其實(shí){{ request }}
中的數(shù)據(jù)遠(yuǎn)不止此,例如{{ request.path }}
就可以獲取先前的url地址。
再修改一下index.html
頁(yè)面,根據(jù)登錄與否的不同,顯示不同的內(nèi)容:
{% extends 'base.html' %}{% block title %}主頁(yè){% endblock %}{% block content %} {% if request.session.is_login %} <h1>你好,{{ request.session.user_name }}!歡迎回來(lái)!</h1> {% else %} <h1>你尚未登錄,只能訪問(wèn)公開(kāi)內(nèi)容!</h1> {% endif %}{% endblock %}
看下效果:
在/login/forms.py
中添加一個(gè)新的表單類:
class RegisterForm(forms.Form): gender = ( ('male', "男"), ('female', "女"), ) username = forms.CharField(label="用戶名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) password1 = forms.CharField(label="密碼", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) password2 = forms.CharField(label="確認(rèn)密碼", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) email = forms.EmailField(label="郵箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) sex = forms.ChoiceField(label='性別', choices=gender) captcha = CaptchaField(label='驗(yàn)證碼')
說(shuō)明:
同樣地,類似login.html文件,我們?cè)趓egister.html中編寫(xiě)forms相關(guān)條目:
{% extends 'login/base.html' %}{% block title %}注冊(cè){% endblock %}{% block content %} <div class="container"> <div class="col-md-4 col-md-offset-4"> <form class='form-register' action="/register/" method="post"> {% if message %} <div class="alert alert-warning">{{ message }}</div> {% endif %} {% csrf_token %} <h2 class="text-center">歡迎注冊(cè)</h2> <div class="form-group"> {{ register_form.username.label_tag }} {{ register_form.username}} </div> <div class="form-group"> {{ register_form.password1.label_tag }} {{ register_form.password1 }} </div> <div class="form-group"> {{ register_form.password2.label_tag }} {{ register_form.password2 }} </div> <div class="form-group"> {{ register_form.email.label_tag }} {{ register_form.email }} </div> <div class="form-group"> {{ register_form.sex.label_tag }} {{ register_form.sex }} </div> <div class="form-group"> {{ register_form.captcha.errors }} {{ register_form.captcha.label_tag }} {{ register_form.captcha }} </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 %}
進(jìn)入/login/views.py
文件,現(xiàn)在來(lái)完善我們的register()
視圖:
def register(request): if request.session.get('is_login', None): # 登錄狀態(tài)不允許注冊(cè)。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = RegisterForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" if register_form.is_valid(): # 獲取數(shù)據(jù) username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經(jīng)存在,請(qǐng)重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊(cè),請(qǐng)使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當(dāng)一切都OK的情況下,創(chuàng)建新用戶 new_user = models.User.objects.create() new_user.name = username new_user.password = password1 new_user.email = email new_user.sex = sex new_user.save() return redirect('/login/') # 自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面 register_form = RegisterForm() return render(request, 'login/register.html', locals())
從大體邏輯上,也是先實(shí)例化一個(gè)RegisterForm的對(duì)象,然后使用is_valide()
驗(yàn)證數(shù)據(jù),再?gòu)?code>cleaned_data中獲取數(shù)據(jù)。
重點(diǎn)在于注冊(cè)邏輯,首先兩次輸入的密碼必須相同,其次不能存在相同用戶名和郵箱,最后如果條件都滿足,利用ORM的API,創(chuàng)建一個(gè)用戶實(shí)例,然后保存到數(shù)據(jù)庫(kù)內(nèi)。
看一下注冊(cè)的頁(yè)面:
注冊(cè)成功在admin后臺(tái)可以看到注冊(cè)的用戶
用戶注冊(cè)的密碼應(yīng)該加密才對(duì)
對(duì)于如何加密密碼,有很多不同的途徑,其安全程度也高低不等。這里我們使用Python內(nèi)置的hashlib庫(kù),使用哈希值的方式加密密碼,可能安全等級(jí)不夠高,但足夠簡(jiǎn)單,方便使用,不是么?
首先在login/views.py
中編寫(xiě)一個(gè)hash函數(shù):
import hashlibdef hash_code(s, salt='mysite'):# 加點(diǎn)鹽 h = hashlib.sha256() s += salt h.update(s.encode()) # update方法只接收bytes類型 return h.hexdigest()
然后,我們還要對(duì)login()和register()視圖進(jìn)行一下修改:
#loginif user.password == hash_code(password): # 哈希值和數(shù)據(jù)庫(kù)內(nèi)的值進(jìn)行比對(duì)#registernew_user.password = hash_code(password1) # 使用加密密碼
views.py全部代碼
# login/views.pyfrom django.shortcuts import render,redirectfrom . import modelsfrom .forms import UserForm,RegisterFormimport hashlibdef index(request): pass return render(request,'login/index.html')def login(request): if request.session.get('is_login', None): return redirect("/index/") if request.method == "POST": login_form = UserForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" 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 == hash_code(password): # 哈希值和數(shù)據(jù)庫(kù)內(nèi)的值進(jìn)行比對(duì) 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()) login_form = UserForm() return render(request, 'login/login.html', locals())def register(request): if request.session.get('is_login', None): # 登錄狀態(tài)不允許注冊(cè)。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = RegisterForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" if register_form.is_valid(): # 獲取數(shù)據(jù) username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經(jīng)存在,請(qǐng)重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊(cè),請(qǐng)使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當(dāng)一切都OK的情況下,創(chuàng)建新用戶 new_user = models.User.objects.create() new_user.name = username new_user.password = hash_code(password1) # 使用加密密碼 new_user.email = email new_user.sex = sex new_user.save() return redirect('/login/') # 自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面 register_form = RegisterForm() return render(request, 'login/register.html', locals())def logout(request): if not request.session.get('is_login',None): return redirect('/index/') request.session.flush() return redirect('/index/')def hash_code(s, salt='mysite_login'): h = hashlib.sha256() s += salt h.update(s.encode()) # update方法只接收bytes類型 return h.hexdigest()views.py全部代碼
重啟服務(wù)器,進(jìn)入注冊(cè)頁(yè)面,新建一個(gè)用戶,然后進(jìn)入admin后臺(tái),查看用戶的密碼情況:
再使用該用戶登錄一下,大功告成!
可以看到密碼長(zhǎng)度根據(jù)你哈希算法的不同,已經(jīng)變得很長(zhǎng)了,所以前面model中設(shè)置password字段時(shí),不要想當(dāng)然的將max_length
設(shè)置為16這么小的數(shù)字。
通常而言,我們?cè)谟脩糇?cè)成功,實(shí)際登陸之前,會(huì)發(fā)送一封電子郵件到對(duì)方的注冊(cè)郵箱中,表示歡迎。進(jìn)一步的還可能要求用戶點(diǎn)擊郵件中的鏈接,進(jìn)行注冊(cè)確認(rèn)。
下面就讓我們先看看如何在Django中發(fā)送郵件吧。
其實(shí)在Python中已經(jīng)內(nèi)置了一個(gè)smtp郵件發(fā)送模塊,Django在此基礎(chǔ)上進(jìn)行了簡(jiǎn)單地封裝。
首先,我們需要在項(xiàng)目的settings文件中配置郵件發(fā)送參數(shù),分別如下:
EMAIL_USE_SSL = TrueEMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'EMAIL_HOST = 'smtp.qq.com'EMAIL_PORT = 465EMAIL_HOST_USER = 'xxxxxxxx@qq.com'EMAIL_HOST_PASSWORD = 'x x x x x x x'DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
特別說(shuō)明:
配置好了參數(shù),就可以先測(cè)試一下郵件功能了。
在項(xiàng)目根目錄下新建一個(gè)send_mail.py
文件,然后寫(xiě)入下面的內(nèi)容:
import osfrom django.core.mail import send_mailos.environ['DJANGO_SETTINGS_MODULE'] = 'projectname.settings'if __name__ == '__main__': send_mail( '來(lái)自www.xxxxx.com的測(cè)試郵件', '歡迎訪問(wèn)www.xxxxx.com,這里是xx站點(diǎn),本站專注于xx內(nèi)容的分享!', 'xxx@qq.com', ['xxx@qq.com'], )#HTML格式郵件import osfrom django.core.mail import EmailMultiAlternativesos.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'if __name__ == '__main__': subject, from_email, to = '來(lái)自www.xxxxx.com的測(cè)試郵件', 'xxx@qq.com', 'xxx@qq.com' text_content = '歡迎訪問(wèn)www.xxxxx.com,這里是xx站點(diǎn),專注于xx技術(shù)的分享!' html_content = '<p>歡迎訪問(wèn)<a target=blank>www.xxx.com</a>,這里是xx的站點(diǎn),專注于xx技術(shù)的分享!</p>' msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg.attach_alternative(html_content, "text/html") msg.send()
對(duì)于send_mail方法,第一個(gè)參數(shù)是郵件主題subject;第二個(gè)參數(shù)是郵件具體內(nèi)容;第三個(gè)參數(shù)是郵件發(fā)送方,需要和你settings中的一致;第四個(gè)參數(shù)是接受方的郵件地址列表。請(qǐng)按你自己實(shí)際情況修改發(fā)送方和接收方的郵箱地址。
運(yùn)行send_mail.py
文件,注意不是運(yùn)行Django服務(wù)器。然后到你的目的地郵箱查看郵件是否收到。
既然要區(qū)分通過(guò)和未通過(guò)郵件確認(rèn)的用戶,那么必須給用戶添加一個(gè)是否進(jìn)行過(guò)郵件確認(rèn)的屬性。
另外,我們要?jiǎng)?chuàng)建一張新表,用于保存用戶的確認(rèn)碼以及注冊(cè)提交的時(shí)間。
全新、完整的/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) has_confirmed = models.BooleanField(default=False) def __str__(self): return self.name class Meta: ordering = ["-c_time"] verbose_name = "用戶" verbose_name_plural = "用戶"class ConfirmString(models.Model): code = models.CharField(max_length=256) user = models.OneToOneField('User') c_time = models.DateTimeField(auto_now_add=True) def __str__(self): return self.user.name + ": " + self.code class Meta: ordering = ["-c_time"] verbose_name = "確認(rèn)碼" verbose_name_plural = "確認(rèn)碼"
說(shuō)明:
has_confirmed
字段,這是個(gè)布爾值,默認(rèn)為False,也就是未進(jìn)行郵件注冊(cè);c_time
是注冊(cè)的提交時(shí)間,"-c_time"表示降序排列.這里有個(gè)問(wèn)題可以討論一下:是否需要?jiǎng)?chuàng)建ConfirmString新表,可否都放在User表里?我認(rèn)為如果全都放在User中,不利于管理,查詢速度慢,創(chuàng)建新表有利于區(qū)分已確認(rèn)和未確認(rèn)的用戶。最終的選擇可以根據(jù)你的實(shí)際情況具體分析。
模型修改和創(chuàng)建完畢,需要執(zhí)行migrate命令,一定不要忘了。
順便修改一下admin.py文件,方便我們?cè)诤笈_(tái)修改和觀察數(shù)據(jù)。
# login/admin.pyfrom django.contrib import admin# Register your models here.from . import modelsadmin.site.register(models.User)admin.site.register(models.ConfirmString)
首先,要修改我們的register()
視圖的邏輯:
def register(request): if request.session.get('is_login', None): # 登錄狀態(tài)不允許注冊(cè)。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = forms.RegisterForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" if register_form.is_valid(): # 獲取數(shù)據(jù) username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經(jīng)存在,請(qǐng)重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊(cè),請(qǐng)使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當(dāng)一切都OK的情況下,創(chuàng)建新用戶 new_user = models.User() new_user.name = username new_user.password = hash_code(password1) # 使用加密密碼 new_user.email = email new_user.sex = sex new_user.save() code = make_confirm_string(new_user) send_email(email, code) message = '請(qǐng)前往注冊(cè)郵箱,進(jìn)行郵件確認(rèn)!' return render(request, 'login/confirm.html', locals()) # 跳轉(zhuǎn)到等待郵件確認(rèn)頁(yè)面。 register_form = forms.RegisterForm() return render(request, 'login/register.html', locals())
make_confirm_string()
是創(chuàng)建確認(rèn)碼對(duì)象的方法,代碼如下:
def make_confirm_string(user): now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") code = hash_code(user.name, now) models.ConfirmString.objects.create(code=code, user=user,) return code
在文件頂部要先導(dǎo)入datetime
模塊。
make_confirm_string()
方法接收一個(gè)用戶對(duì)象作為參數(shù)。首先利用datetime模塊生成一個(gè)當(dāng)前時(shí)間的字符串now,再調(diào)用我們前面編寫(xiě)的hash_code()
方法以用戶名為基礎(chǔ),now為‘鹽’,生成一個(gè)獨(dú)一無(wú)二的哈希值,再調(diào)用ConfirmString模型的create()方法,生成并保存一個(gè)確認(rèn)碼對(duì)象。最后返回這個(gè)哈希值。
send_email(email, code)
方法接收兩個(gè)參數(shù),分別是注冊(cè)的郵箱和前面生成的哈希值,代碼如下:
def send_email(email, code): from django.core.mail import EmailMultiAlternatives subject = '來(lái)自www.xxxxx.com的測(cè)試郵件' text_content = '歡迎訪問(wèn)www.xxxxx.com,這里是xx站點(diǎn),專注于xx技術(shù)的分享!' html_content = '<p>歡迎注冊(cè)<a href="http://{}/confirm/?code={}" target="blank>www.xxx.com</a>,這里是xx的站點(diǎn),專注于xx技術(shù)的分享!</p>'.format('127.0.0.1',code,settings.CONFIRM_DAYS) msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg.attach_alternative(html_content, "text/html") msg.send()
首先我們需要導(dǎo)入settings配置文件from django.conf import settings
。
郵件內(nèi)容中的所有字符串都可以根據(jù)你的實(shí)際情況進(jìn)行修改。其中關(guān)鍵在于<a href=''>
中鏈接地址的格式,我這里使用了硬編碼的'127.0.0.1:8000',請(qǐng)酌情修改,url里的參數(shù)名為code
,它保存了關(guān)鍵的注冊(cè)確認(rèn)碼,最后的有效期天數(shù)為設(shè)置在settings中的CONFIRM_DAYS
。所有的這些都是可以定制的!
下面是郵件相關(guān)的settings配置:
EMAIL_USE_SSL = TrueEMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'EMAIL_HOST = 'smtp.qq.com'EMAIL_PORT = 465EMAIL_HOST_USER = 'xxxxxxxx@qq.com'EMAIL_HOST_PASSWORD = 'x x x x x x x'DEFAULT_FROM_EMAIL = EMAIL_HOST_USERCONFIRM_DAYS = 7
首先,在根目錄的urls.py
中添加一條url:
url(r'^confirm/$', views.user_confirm),
其次,在login/views.py
中添加一個(gè)user_confirm
視圖。
def user_confirm(request): code = request.GET.get('code', None) message = '' try: confirm = models.ConfirmString.objects.get(code=code) except: message = '無(wú)效的確認(rèn)請(qǐng)求!' return render(request, 'login/confirm.html', locals()) c_time = confirm.c_time now = datetime.datetime.now() if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS): confirm.user.delete() message = '您的郵件已經(jīng)過(guò)期!請(qǐng)重新注冊(cè)!' return render(request, 'login/confirm.html', locals()) else: confirm.user.has_confirmed = True confirm.user.save() confirm.delete() message = '感謝確認(rèn),請(qǐng)使用賬戶登錄!' return render(request, 'login/confirm.html', locals())
說(shuō)明:
request.GET.get('code', None)
從請(qǐng)求的url地址中獲取確認(rèn)碼;confirm.html
頁(yè)面,并提示;c_time
,加上設(shè)置的過(guò)期天數(shù),這里是7天,然后與現(xiàn)在時(shí)間點(diǎn)進(jìn)行對(duì)比;confirm.html
頁(yè)面,并提示;has_confirmed
字段為True,并保存,表示通過(guò)確認(rèn)了。然后刪除注冊(cè)碼,但不刪除用戶本身。最后返回confirm.html
頁(yè)面,并提示。這里需要一個(gè)confirm.html
頁(yè)面,我們將它創(chuàng)建在/login/templates/login/
下面:
{% extends 'base.html' %}{% block title %}注冊(cè)確認(rèn){% endblock %}{% block content %} <div class="row"> <h1 class="text-center">{{ message }}</h1> </div> <script> window.setTimeout("window.location='/login/'",2000); </script>{% endblock %}
頁(yè)面中通過(guò)JS代碼,設(shè)置2秒后自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面。
既然未進(jìn)行郵件確認(rèn)的用戶不能登錄,那么我們就必須修改登錄規(guī)則,如下所示:
def login(request): if request.session.get('is_login', None): return redirect("/index/") if request.method == "POST": login_form = forms.UserForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" 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 not user.has_confirmed: message = "該用戶還未通過(guò)郵件確認(rèn)!" return render(request, 'login/login.html', locals()) if user.password == hash_code(password): # 哈希值和數(shù)據(jù)庫(kù)內(nèi)的值進(jìn)行比對(duì) 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()) login_form = forms.UserForm() return render(request, 'login/login.html', locals())
關(guān)鍵是下面的部分:
if not user.has_confirmed: message = "該用戶還未通過(guò)郵件確認(rèn)!" return render(request, 'login/login.html', locals())
最后,貼出view.py的整體代碼,供大家參考:
from django.shortcuts import render,redirectfrom . import modelsfrom . import formsimport hashlibfrom django.conf import settingsdef make_confirm_string(user): now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") code = hash_code(user.name, now) models.ConfirmString.objects.create(code=code, user=user,) return codedef send_email(email,code): from django.core.mail import EmailMultiAlternatives subject, from_email, to = '來(lái)自www.robindongblog.com的測(cè)試郵件','1361623944@qq.com','1505955202@qq.com' text_content = '歡迎注冊(cè)訪問(wèn)www.robindongblog.com,這里是wo的博客和教程站點(diǎn),本站專注于Python和Django技術(shù)的分享!' html_content = ''' <p>感謝注冊(cè)<a href="http://{}/confirm/?code={}" target=blank>robin-dong.github.io</a>, 這里是wo的博客和教程站點(diǎn),專注于Python和Django技術(shù)的分享!</p> <p>請(qǐng)點(diǎn)擊站點(diǎn)鏈接完成注冊(cè)確認(rèn)!</p> <p>此鏈接有效期為{}天!</p> '''.format('127.0.0.1:8000', code, settings.CONFIRM_DAYS) msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg.attach_alternative(html_content, "text/html") msg.send() print('done')def hash_code(s, salt='mysite'):# 加點(diǎn)鹽 h = hashlib.sha256() s += salt h.update(s.encode()) # update方法只接收bytes類型 return h.hexdigest()# Create your views here.def index(request): pass return render(request,'login/index.html')def login(request): if request.session.get('is_login', None): return redirect("/index/") if request.method == "POST": login_form = forms.UserForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" 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 not user.has_confirmed: message = "該用戶還未通過(guò)郵件確認(rèn)!" return render(request, 'login/login.html', locals()) if user.password == hash_code(password): # 哈希值和數(shù)據(jù)庫(kù)內(nèi)的值進(jìn)行比對(duì) 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()) login_form = forms.UserForm() return render(request, 'login/login.html', locals())def register(request): if request.session.get('is_login', None): # 登錄狀態(tài)不允許注冊(cè)。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = forms.RegisterForm(request.POST) message = "請(qǐng)檢查填寫(xiě)的內(nèi)容!" if register_form.is_valid(): # 獲取數(shù)據(jù) username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經(jīng)存在,請(qǐng)重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊(cè),請(qǐng)使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當(dāng)一切都OK的情況下,創(chuàng)建新用戶 new_user = models.User() new_user.name = username new_user.password = hash_code(password1) # 使用加密密碼 new_user.email = email new_user.sex = sex new_user.save() code = make_confirm_string(new_user) send_email(email, code) message = '請(qǐng)前往注冊(cè)郵箱,進(jìn)行郵件確認(rèn)!' return render(request, 'login/confirm.html', locals()) # 跳轉(zhuǎn)到等待郵件確認(rèn)頁(yè)面。 register_form = forms.RegisterForm() return render(request, 'login/register.html', locals()) def logout(request): if not request.session.get('is_login', None): # 如果本來(lái)就未登錄,也就沒(méi)有登出一說(shuō) return redirect("/index/") request.session.flush() # 或者使用下面的方法 # del request.session['is_login'] # del request.session['user_id'] # del request.session['user_name'] return redirect("/index/")def user_confirm(request): code = request.GET.get('code', None) message = '' try: confirm = models.ConfirmString.objects.get(code=code) except: message = '無(wú)效的確認(rèn)請(qǐng)求!' return render(request, 'login/confirm.html', locals()) c_time = confirm.c_time now = datetime.datetime.now() if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS): confirm.user.delete() message = '您的郵件已經(jīng)過(guò)期!請(qǐng)重新注冊(cè)!' return render(request, 'login/confirm.html', locals()) else: confirm.user.has_confirmed = True confirm.user.save() confirm.delete() message = '感謝確認(rèn),請(qǐng)使用賬戶登錄!' return render(request, 'login/confirm.html', locals())
首先,通過(guò)admin后臺(tái)刪除原來(lái)所有的用戶。
進(jìn)入注冊(cè)頁(yè)面,如下圖所示:
點(diǎn)擊提交,此時(shí)激活郵件已發(fā)送,但還是not confirmed狀態(tài),還不能登入,進(jìn)入你的測(cè)試郵箱,查看注冊(cè)郵件:
點(diǎn)擊鏈接,自動(dòng)跳轉(zhuǎn)到確認(rèn)成功提示頁(yè)面,2秒后再跳轉(zhuǎn)到登錄頁(yè)面。這個(gè)時(shí)候再次查看admin后臺(tái),可以看到用戶已經(jīng)處于登錄確認(rèn)狀態(tài),并且確認(rèn)碼也被自動(dòng)刪除了,不會(huì)第二次被使用:
使用該用戶正常登錄吧!Very Good!
聯(lián)系客服