目錄
一、前言
在當今軟體開發的世界中,前後端分離的架構已經成為構建現代Web應用的黃金標準。這種設計哲學強調在用戶界面(UI)層(即前端)和數據處理與業務邏輯層(即後端)之間劃清界限。通過這種分離,前端開發者能夠專注於打造一流的用戶體驗,而後端開發者則致力於確保數據的穩定性和安全性。這兩個層面透過API進行高效的通訊,API不僅作為兩者之間的橋樑,也確保了代碼的模組化和可重用性,同時使前端和後端能夠獨立開發和部署,有效提升了開發效率、可擴展性和可維護性。
本文將引領您了解如何使用Django後端框架結合前後端分離的方法學,來實現一個高效的登入和註冊系統。雖然Django擁有強大的模板引擎,我們將限制它的使用,主要專注於後端API的構建。這些API將會部分符合RESTful風格——採用合適的HTTP請求方法和狀態碼。然而,在狀態管理方面,系統將依賴於Django提供的Authentication System,通過伺服器端的Session追蹤用戶狀態,與Stateless的RESTful原則略有不同。
專案程式碼:hsunAlfred/AlfredWiki at loginSignup (github.com)
二、準備工作
專案環境
- Windows 11 + VMWare + Ubuntu 20.04
- Python 3.8.10
- Django 4.1.2
環境建置可以參考這篇文章<Python Django開發環境設置 Win 11 + VMWare + Ubuntu 20.04>。
配置Django專案
如果你有依照環境設定文章設定,你應該會有一個Django專案AlfredWiki,同時有一個TestAPP,如果已經完成以上步驟,接下來我們要為了登入及註冊建立一個名為Member的APP。
首先,我們打開Terminal,並切換到專案資料夾下,
cd AlfredWiki
啟動虛擬環境,
source ~/VENV/bin/activate
安裝本次需要的Python模組,
pip3 install pycryptodome
並輸入以下指令建立一個新的APP,名稱為Member所有跟會有相關的驗證都在這邊處理。
python manage.py startapp Member
開啟跟專案同名的資料夾,並開啟settings.py,找到INSTALLED_APPS,並再List的最後加入Member
。
繼續在跟專案同名的資料夾中找到urls.py
,在檔案的一開始
加入以下程式碼,
from Member.urls import urlpatterns as memberUrl
並在檔案最後加入 ,
urlpatterns+=memberUrl
接著開啟Member資料夾,新增一個urls.py
,並貼上以下程式碼,
from django.urls import path
urlpatterns = []
這些設定的用途將在接下來的教學中進行說明。
三、URL端點資源定義
在採用前後端分離架構的Web應用中,前端通過API與後端進行溝通,這樣的設計模式下,精確定義URL端點資源就變得至關重要。URL端點不僅代表了後端提供的各種服務接口,也是前端與後端交流的關鍵節點。
首先,我們需要設計一個登入頁面的URL端點,這個端點將處理來自使用者登入請求的驗證和授權。其次,一個註冊頁面的URL端點也是必需的,以便使用者能夠創建新的帳戶。在註冊的過程中,後端將會接收到使用者提交的個人信息並處理這些數據,進行必要的驗證並在系統中創建新的用戶紀錄。
除了處理使用者身份的確認和註冊外,我們還需要定義一個專門用於登出的URL端點。該端點將用於清除用戶的Session資訊,確保使用者能夠安全地結束當前的Session。為了完善使用者的體驗,通常還需要一個簡單的首頁URL端點來展示一個成功登入後的歡迎界面或系統狀態的總覽。
結合起來,我們的URL設計將包含以下端點:
/member/login/
: 理用戶登入請求,提供登入表格且驗證使用者憑據- GET : 伺服器回傳登入頁面,Response的Content-Type為text/html
- POST : 接收使用者傳遞的登入資訊,Request的Content-Type為application/json
/member/signup/
: 用於使用者的註冊表單提交和新用戶建立- GET : 伺服器回傳註冊頁面,Response的Content-Type為text/html
- POST : 接收使用者傳遞的註冊資訊,Request的Content-Type為application/json
/member/logout/
: 負責使用者的安全登出功能,登出後重新導向回登入頁/
: 首頁,在用戶成功登入後顯示歡迎信息或其他內容。
我們遵循RESTful API的在一個端點下透過不同Verbs動詞定義資源,如GET返回登入頁面,POST接收登入資訊。
接著讓我們在Member資料夾下找到urls.py
,增加以下內容,注意這段程式碼不要取代掉了from django.urls import path
。
from Member.views import login, signup, logout, index
urlpatterns = [
path('member/login/', login), # login page
path('member/signup/', signup), # signup page
path('member/logout/', logout), # logout
path('', index),
]
一開始我們從Member資料夾下views.py匯入4個function(將在下一節定義),這些function會處理不同資源傳遞進來的請求。另外我們也將urlpatterns這個list擴充,加入前面規劃的端點。完成這個步驟後,記得要存檔,接下來我們將撰寫處理請求的function。
四、Views.py,視圖,資源請求處理
在前一節中我們在views.py中定義了四個function,login、signup、logout、index。請開啟Member資料夾下views.py,我們要來實作這些function了。
login
請在Member資料夾下的views.py貼上下方程式碼,
- 根據前一節的定義,這個function我們需要處理GET及POST請求,程式碼的一開始我們先判斷請求方法(Request Method)是否為GET或POST,如果不是就回傳Http Status Code 405,代表
M
ethod Not Allowed。 request.user.is_authenticated
: 根據Django文件,我們可以透過這個方式來判斷使用者是否在登入狀態,如果是就直接重新導向回首頁。request.method == 'GET'
: 在這個條件判斷式底下我們會回傳登入的介面,如果你是跟著我的環境設置文章建置Django專案,你的CsrfViewMiddleware
應該是註解掉的,為了防範csrf攻擊,每次回傳登入介面時,我都會埋設一個public key,前端要送出登入資料時也需要透過這個key將資料加密,後端會有一把對應的private key,只有當兩者符合時才能夠正確解析出使用者傳輸的資料。
由於這個key隨著頁面refresh就會更新,而且我們把一個session的存續時間設置為30分鐘,所以能夠跟傳統csrf token有相同效果。
並且這種對稱式加密,每次加密結果並不相同,代表每次傳輸的資料也不相同,增加資料傳輸時的安全性。
為了產生這個key,請在Member資料夾下建立utils資料夾,再在utils資料夾下建立secure資料夾,並請建立secureTools.py,並貼上下方secureTools.py的程式碼,同時也請建立easyRSA.py,並貼上相對應的程式碼。- 程式的最後一部分是對POST請求的處理,由於前面已經限制請求方法只能是GET或POST,同時處理完GET請求後這個function也
return
了,所以最後一塊我們並不需要再一個條件判斷,這種寫法可以減少縮排,增加可維護性。
在前一節我們定義了POST請求傳遞進來的body是JSON格式,所以這個部分程式一開始先將JSON字串轉換為Python物件,我們也定義了一個loginCheck
物件,這個物件有兩個方法分別為 :decryptBody
及process
,前者先將透過public key加密的資訊進行解密,後者再對相關資訊進行處理。兩者處理完成都透過判斷lc.lsr.ok
狀態是不是True
,並進行相對應處理。loginCheck
這個物件,將其包裝成一個class
,方便未來串接Google Oauth 2.0做為第三方登入,請在Member資料夾下建立utils資料夾,再在utils資料夾下建立loginSignup資料夾,並請建立loginSignup.py、login.py、fieldVaild.py三個檔案,並從底下找出程式碼並貼上。
# Member/Views.py login
from django.shortcuts import render
from django.contrib import auth
from django.http import HttpResponseRedirect, JsonResponse
from Member.utils.secure.secureTools import sessionKeyGenerate
from Member.utils.loginSignup.login import loginCheck
import json
def login(request):
# login page
if request.method not in ['GET', 'POST']:
return JsonResponse({'ok': False, 'message': 'Method Not Allowed.'}, status=405)
if request.user.is_authenticated:
return HttpResponseRedirect("/")
if request.method == 'GET':
session_public_key, session_private_key = sessionKeyGenerate()
request.session['login_public_key'] = session_public_key
request.session['login_private_key'] = session_private_key
context = {
"spk_client": session_public_key,
}
return render(request, 'member/login.html', context=context)
# call api to login
body = json.loads(request.body)
lc = loginCheck()
lc.decryptBody(body, request.session['login_private_key'])
if not lc.lsr.ok:
return JsonResponse({'ok': lc.lsr.ok, 'message': lc.lsr.message}, status=lc.lsr.code)
lc.process()
if lc.lsr.ok:
auth.login(request, lc.lsr.user_obj)
return JsonResponse({'ok': lc.lsr.ok, 'message': lc.lsr.message}, status=lc.lsr.code)
在secureTools.py
使用封裝好的easyRSA
模組建立這個專案需要的sessionKeyGenerate
、sessionDecrypted
及hmacsha
,有興趣的可以自行參閱程式碼。
# Member/utils/secure/secureTools.py
import hmac
from base64 import b64decode
from hashlib import sha1
from urllib import parse
from Member.utils.secure.easyRSA import easyRSA
def sessionKeyGenerate():
return easyRSA().session_key()
def sessionDecrypted(data_encrypted, session_private_key):
data_encrypted = parse.unquote(data_encrypted)
data_encrypted = b64decode(data_encrypted)
results = {
"status": "Success",
"info": ""
}
try:
results["info"] = b64decode(easyRSA().session_decrypted(
data_encrypted, session_private_key.encode())).decode()
except:
results["status"] = "Fail"
return results
def hmacsha(playerid, pass_ori):
return hmac.new(playerid.encode(), pass_ori.encode(), sha1).hexdigest()
# Member/utils/secure/easyRSA.py
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES, PKCS1_OAEP, PKCS1_v1_5
class easyRSA:
def session_key(self):
key = RSA.generate(1024)
private_key = key.export_key()
public_key = key.publickey().export_key()
return public_key.decode('utf-8'), private_key.decode('utf-8')
def session_encrypted(self, data, session_public_key):
public_key = RSA.import_key(session_public_key)
cipher_rsa = PKCS1_v1_5.new(public_key)
data_encrypted = cipher_rsa.encrypt(data)
return data_encrypted.hex()
def session_decrypted(self, data_encrypted, session_private_key):
private_key = RSA.import_key(session_private_key)
cipher_rsa = PKCS1_v1_5.new(private_key)
data_decrypted = cipher_rsa.decrypt(data_encrypted, None)
return data_decrypted.decode("utf-8")
def encrypt(self, secret_code, oriText):
key = RSA.generate(2048)
encrypted_key = key.export_key(passphrase=secret_code, pkcs=8,
protection="scryptAndAES128-CBC")
public_key = key.publickey().export_key()
recipient_key = RSA.import_key(public_key)
session_key = get_random_bytes(16)
cipher_rsa = PKCS1_OAEP.new(recipient_key)
enc_session_key = cipher_rsa.encrypt(session_key)
cipher_aes = AES.new(session_key, AES.MODE_EAX)
ciphertext, tag = cipher_aes.encrypt_and_digest(
oriText.encode("utf-8"))
res_dict = {
"public_key": public_key,
"private_encrypted": encrypted_key,
"enc_session_key": enc_session_key,
"secret_code": secret_code,
"nonce": cipher_aes.nonce,
"tag": tag,
"ciphertext_bin": ciphertext,
"ciphertext_hex": ciphertext.hex(),
}
return res_dict
def decrypt(self, encrypted_key, secret_code, nonce, enc_session_key, ciphertext, tag):
private_key = RSA.import_key(encrypted_key, passphrase=secret_code)
cipher_rsa = PKCS1_OAEP.new(private_key)
session_key = cipher_rsa.decrypt(enc_session_key)
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
res = cipher_aes.decrypt_and_verify(ciphertext, tag)
return res.decode('utf-8')
fieldVaild.py有三個function,使用正規表示式(regex)分別針對使用者名稱、電子郵件、密碼進行檢查:
- 使用者名稱檢查是否為大小寫英文字母及數字
- 電子郵件檢查是否為x@domain
- 密碼檢查是否長度大於等於7,並檢查是否為大小寫英文字母、數字及符號的組合
另外有一個自定義的錯誤類別,用以蒐集驗證欄位時的錯誤資訊。
# Member/utils/loginSignup/fieldVaild.py
import re
class ValidException(Exception):
def __init__(self, message):
super().__init__(message)
def usernameVaild(username):
regex = re.compile(r'([A-Za-z0-9])+')
if not re.fullmatch(regex, username):
return False
return True
def emailVaild(email):
regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
if not re.fullmatch(regex, email):
return False
return True
def passwordVaild(password):
if len(password) < 7:
return False
regex = re.compile(r'([A-Z]+[a-z]+[0-9]+[!@#$%^&*()_+*/=~])+')
if not re.fullmatch(regex, password):
return False
return True
loginSignup.py中使用物件導向的概念,共有兩個物件:
loginSignupResult
用以紀錄登入或註冊的結果loginSignupBase
則是抽象類別,作為登入及註冊的基底類別,該物件建構子中會直接建立loginSignupResult
物件,並儲存於類別變數lsr,並定義兩個抽象方法decryptBody
和process
,後續子類別必須實作該方法,確保interface標準化。
# Member/utils/loginSignup/loginSignup.py
from abc import ABCMeta, abstractmethod
class loginSignupResult:
ok = True
message = "Success."
user_obj = None
code = 200
def setFail(self, message, code):
self.ok = False
self.message = message
self.code = code
class loginSignupBase(metaclass=ABCMeta):
def __init__(self):
self.lsr = loginSignupResult()
@abstractmethod
def decryptBody(self):
pass
@abstractmethod
def process(self):
pass
login.py中的loginCheck
即繼承了loginSignupBase
這個抽象類別,並實作了decryptBody
和process
,我們可以注意到前者先對前端傳入資料進行解密,並在後者透過Django的auth.login方法前,另外將密碼透過hmacsha進行轉換。
這裡要注意一下,像是密碼這種機密資訊,絕對不會有直接寫入資料庫的可能發生,Django密碼保存使用的是PBKDF2演算法計算的一種256位元的hash值,這種hash值有一種特色,通常很難反向解密,這麼做最大的用途是,即使今天資料庫中的密碼被駭客竊取,他也沒辦法直接登入你的帳戶。
但既然Django已經有內建的處理方法了,為什麼我還要再做一次hash?django其實有一個內建的後台,位於/admin/
這個路徑,如果我們今天沒有進行密碼轉換,就代表這兩個後臺使用同一組密碼登入,透過這樣的轉換也可以避免有心人直接竊取密碼登入另一個後臺,但如果要完整杜絕這個狀況還需要其他做為,有這個需求時可以思考如何解決。
# Member/utils/loginSignup/login.py
from django.contrib import auth
from django.contrib.auth.models import User
import sys
from Member.utils.secure.secureTools import hmacsha, sessionDecrypted
from Member.utils.loginSignup.fieldVaild import emailVaild, ValidException
from Member.utils.loginSignup.loginSignup import loginSignupBase
class loginCheck(loginSignupBase):
def decryptBody(self, body, login_private_key):
try:
userEmail_encrypt = body['userEmail']
userEmail_decrypt = sessionDecrypted(
userEmail_encrypt, login_private_key)
pass_encrypt = body['pass']
pass_decrypt = sessionDecrypted(pass_encrypt, login_private_key)
if not userEmail_decrypt['status'] or not pass_decrypt['status']:
raise
self.userEmail_decrypt = userEmail_decrypt['info']
self.isMail = False
if emailVaild(self.userEmail_decrypt):
self.isMail = True
self.pass_decrypt = pass_decrypt['info']
except ValidException as e:
self.lsr.setFail(str(e), 400)
except Exception as e:
print(e)
self.lsr.setFail('Invalid Parameter.', 400)
def process(self):
try:
if self.isMail:
username_tmp = User.objects.get(
email=self.userEmail_decrypt
).username
else:
username_tmp = User.objects.get(
username=self.userEmail_decrypt
).username
pass_hash = hmacsha(username_tmp, self.pass_decrypt)
try:
user_obj = auth.authenticate(
username=username_tmp, password=pass_hash)
if user_obj is not None:
if not user_obj.is_active:
self.lsr.setFail("User not active.", 401)
else:
self.lsr.user_obj = user_obj
else:
self.lsr.setFail("Incorrect login info.", 401)
except Exception as e:
exception_type, exception, exc_tb = sys.exc_info()
print(exception_type, exception, exc_tb)
self.lsr.setFail(str(e), 401)
except Exception as e:
print(e)
self.lsr.setFail("User not exist.", 401)
signup
請在Member資料夾下的views.py貼上下方程式碼,程式碼結構大致跟login
相同,保持相同Coding Style,增加可讀性。
在signup中,也有一個signupCheck
物件,跟loginCheck
繼承相同的抽象類別,有興趣可以參考我的程式碼。請在這個路徑Member/utils/loginSignup/
下建立signup.py,程式碼在下方,貼上即可。
# Member/Views.py signup
from Member.utils.loginSignup.signup import signupCheck
def signup(request):
# signup page
if request.method not in ['GET', 'POST']:
return JsonResponse({'ok': False, 'message': 'Method Not Allowed.'}, status=405)
if request.user.is_authenticated:
return HttpResponseRedirect("/")
if request.method == 'GET':
session_public_key, session_private_key = sessionKeyGenerate()
request.session['signup_public_key'] = session_public_key
request.session['signup_private_key'] = session_private_key
context = {
"spk_client": session_public_key,
}
return render(request, 'member/signup.html', context=context)
body = json.loads(request.body)
sc = signupCheck()
sc.decryptBody(body, request.session['signup_private_key'])
if not sc.lsr.ok:
return JsonResponse({'ok': sc.lsr.ok, 'message': sc.lsr.message}, status=sc.lsr.code)
sc.process()
return JsonResponse({'ok': sc.lsr.ok, 'message': sc.lsr.message}, status=sc.lsr.code)
# Member/utils/loginSignup/signup.py
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from Member.utils.secure.secureTools import hmacsha, sessionDecrypted
from Member.utils.loginSignup.fieldVaild import usernameVaild, emailVaild, passwordVaild, ValidException
from Member.utils.loginSignup.loginSignup import loginSignupBase
class signupCheck(loginSignupBase):
def decryptBody(self, body, signup_private_key):
try:
username_encrypt = body['username']
username_decrypt = sessionDecrypted(
username_encrypt, signup_private_key)
email_encrypt = body['email']
email_decrypt = sessionDecrypted(email_encrypt, signup_private_key)
pass_encrypt = body['pass']
pass_decrypt = sessionDecrypted(pass_encrypt, signup_private_key)
if not username_decrypt['status'] or not email_decrypt['status'] or not pass_decrypt['status']:
raise
self.username_decrypt = username_decrypt['info']
if not usernameVaild(self.username_decrypt):
raise ValidException("Invalid username.")
self.email_decrypt = email_decrypt['info']
if not emailVaild(self.email_decrypt):
raise ValidException("Invalid email.")
self.pass_decrypt = pass_decrypt['info']
if not passwordVaild(self.pass_decrypt):
raise ValidException("Invalid password.")
self.pass_hash = hmacsha(self.username_decrypt, self.pass_decrypt)
except ValidException as e:
self.lsr.setFail(str(e), 400)
except Exception as e:
self.lsr.setFail('Invalid Parameter.', 400)
def process(self):
try:
User.objects.get(username=self.username_decrypt)
self.lsr.setFail('Username has been registered.', 400)
except ObjectDoesNotExist as e:
print('Username OK')
try:
User.objects.get(email=self.email_decrypt)
self.lsr.setFail('Email has been registered.', 400)
except ObjectDoesNotExist as e:
print('Email OK')
except Exception as e:
print('Email', e)
self.lsr.setFail('Invalid Parameter.', 400)
except Exception as e:
print('Username', e)
self.lsr.setFail('Invalid Parameter.', 400)
if not self.lsr.ok:
return self.lsr
try:
user = User.objects.create_user(
username=self.username_decrypt, email=self.email_decrypt, password=self.pass_hash)
user.save()
except Exception as e:
self.lsr.setFail('Sign up fail.', 400)
logout
這塊就相對單純,因為Django已經幫我們準備好logout()
方法,只要呼叫這個方法就可以直接清除當前Session中所有的資料。
# Member/Views.py logout
def logout(request):
# logout
auth.logout(request)
return HttpResponseRedirect('/member/login/')
index
這裡只有放上簡單的登出按鈕,方便測試。值得一提的是這個function使用了Django提供的裝飾器(decordator)login_required,這能在使用者訪問首頁,先檢查該使用者是否已經是登入狀態,如果不是就導向到login_url
完成登入手續。
# Member/Views.py index
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
@login_required(login_url='/member/login/')
def index(request):
return HttpResponse("<style>body{background:black} a{color:white}</style><a href='/member/logout/'>logout</a>")
到目前為止,後端開發我們基本上完成了,接下來要進行前端的開發。
四、templates,模板檔案
一般來說,除非你是全端工程師,不然應該會有前端工程師將前端程式碼打包給你。由於我們沒有把前後端完全分離,所以必須將前端程式碼整合到我們的專案中。
首先,我們在templates資料夾底下建立member資料夾,在member資料夾下我們需要再建立一個base資料夾,並在base資料夾底下建立base.html,除此之外,請在member資料夾下建立login.html及signup.html。
在這個架構下,我們先將模板檔案透過APP區分,並透過base資料夾存放共用的程式碼,與base資料夾同層級的login.html及signup.html則是我們主要使用的html檔案。
base.html
在base.html中,我們預留了三個block : custom_head、content、custom_body,後續login.html及signup.html會匯入這個檔案,並填寫這些block,就能夠避免相同程式碼重複出現在多個地方,增加可維護性。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet" />
<style>
body,
.navbar {
font-family: "Roboto", Arial, Helvetica, sans-serif;
background-color: #1f1d22;
color: aliceblue;
}
.navbar {
background-color: #0f09108a;
margin: 0 0 50px 0;
}
.navbar-brand {
padding: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.navbar-brand>img {
height: 75px;
margin-right: 10px;
}
.navbar-brand>span {
color: aliceblue;
font-size: 30px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: fit-content;
padding: 10px 50px 20px 50px;
box-shadow: 0px -5px 10px #e8edf9, 0px 5px 10px #e8edf9, 0px 3px 8px #e8edf9, 0px 5px 10px #e8edf9;
border-radius: 20px;
background: #1c1923;
margin-bottom: 5vh;
}
.container-ele {
width: 310px;
margin: 10px 0;
}
@media screen and (max-width:768px) {
.navbar-brand>img {
height: 60px;
margin-right: 10px;
}
.navbar-brand>span {
font-size: 20px;
}
}
@media screen and (max-width:431px) {
.container {
padding: 30px;
}
.container-ele {
width: 300px;
margin: 5px 0;
}
}
@media screen and (max-width:415px) {
.container-ele {
width: 280px;
}
}
@media screen and (max-width:376px) {
.container-ele {
width: 250px;
}
}
h1 {
font-weight: bolder;
text-align: center;
}
.input-div {
display: flex;
display: flex;
flex-direction: column;
}
span {
font-size: 20px;
}
.input-div>input {
display: block;
font-size: 1rem;
font-weight: 400;
inline-size: 100%;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #878787;
padding-inline: 14px;
padding-block-start: 8px;
padding-block-end: 8px;
background-color: #202124;
color: aliceblue;
}
.input-div>input:hover {
box-shadow: inset 0 0 0 1px #000000;
}
.action-btn {
background-color: #A19FE1;
color: #000000;
--bs-btn-hover-color: #000000;
--bs-btn-hover-bg: #645FCE;
font-size: 20px;
border-radius: 500px;
border: 1px solid #878787;
}
.third-login-method {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 500px;
border: 1px solid #878787;
}
.third-login-method>img {
width: 30px;
}
.third-login-method>span {
width: 250px;
}
.third-login>a.btn {
--bs-btn-hover-color: #f8f9fa;
--bs-btn-hover-bg: #202124;
}
hr {
color: #f8f9fa;
width: 100%;
}
.copyright {
text-align: center;
width: 100%;
font-weight: 100;
font-family: math;
color: #8b9399;
margin-bottom: 2.5vh;
}
</style>
{% block custom_head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<script src="/static/member/js/jsencrypt.min.js"></script>
<div>
<div hidden>
<input id="spk_client" value="{{spk_client}}" name="spk_client">
</div>
</div>
<div class="copyright">
<span>
Copyright © 2023 alfred.wiki. All rights reserved.
</span>
</div>
<script>
let cInfo = function (info, s) {
let jsEncrypt = new JSEncrypt()
jsEncrypt.setPublicKey(s)
let result = jsEncrypt.encrypt(btoa(info))
return result
}
</script>
{% block custom_body %}{% endblock %}
</body>
</html>
login.html
一開始的extends用途就是匯入base.html,接著我們就各自定義每個區塊所需自定義的內容。
{% extends 'member/base/base.html' %}
{% block content %}
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href=".">
<img src="/static/logo.png">
<span>Alfred's Python Wiki</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
</div>
</div>
</nav>
<div class="container">
<h1 class="container-ele">
Login
</h1>
<div class="container-ele input-div">
<span>Username/Email</span>
<input type="text" id="userEmail">
</div>
<div class="container-ele input-div">
<span>Password</span>
<input type="password" id="pass">
</div>
<button class="container-ele btn btn-outline-light action-btn" onclick="login()">Login</button>
<hr />
<a class="container-ele btn btn-outline-light third-login-method" href="/member/google/">
<img src="/static/member/image/googleg_standard_color_128dp.png" />
<span>Continue With Google</span>
</a>
<span>or</span>
<a class="container-ele btn btn-outline-light third-login-method" href="/member/signup/">
<span>Sign Up</span>
</a>
</div>
{% endblock %}
{% block custom_body %}
<script>
let login = function () {
let spk_client = document.getElementById("spk_client").value;
let userEmail = document.getElementById("userEmail").value;
let pass = document.getElementById("pass").value;
if (!userEmail | !pass) {
alert("All fields required!")
} else {
data = {
"userEmail": cInfo(userEmail, spk_client),
"pass": cInfo(pass, spk_client)
}
console.log(data)
const request = new Request(
'.',
{
body: JSON.stringify(data),
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
},
method: 'POST',
mode: 'cors',
redirect: 'follow',
}
);
fetch(request)
.then(response => {
console.log(response)
if (response.redirected === true) {
location.href = response.url;
return;
} else {
if (!response.ok) {
info = {
"status": response.status,
"body": response.json()
}
return Promise.reject(info);
}
return response.json();
}
}).then(response => {
console.log(response);
alert("Login success!")
next = new URL(location.href).searchParams.get('next');
//console.log("next", next);
if (!next) {
window.location.href = "/"
} else if (next == "/") {
window.location.href = "/"
} else {
window.location.href = next
}
}).catch(function (error) {
console.log(error);
error.body.then((err) => {
alert("Login fail! " + err.message);
})
}).catch(function (error) {
console.log(error);
alert("Login fail!");
});
}
}
</script>
{% endblock %}
signup.html
比較login.html可以發現,login.html並沒有填寫custom_head這個區塊,預留的區塊如果沒有需要填寫的東西,跳過他即可。
{% extends 'member/base/base.html' %}
{% block custom_head %}
<style>
.hint {
color: #c82525;
font-size: small;
}
</style>
{% endblock %}
{% block content %}
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href=".">
<img src="/static/logo.png">
<span>Alfred's Python Wiki</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
</div>
</div>
</nav>
<div class="container">
<h1 class="container-ele">
Sign Up
</h1>
<div class="container-ele input-div">
<span>User Name</span>
<input type="text" id="username" placeholder="Combination of uppercase and lowercase letters and numbers">
</div>
<div class="container-ele input-div">
<span>Email</span>
<input type="text" id="email" placeholder="[email protected]">
</div>
<div class="container-ele input-div">
<span>Password</span>
<span class="hint">
At least 7 character. Combination of uppercase and lowercase letters, symbols and numbers.
</span>
<input type="password" id="pass1" placeholder="Follow hint above.">
</div>
<div class="container-ele input-div">
<span>Password(Again)</span>
<input type="password" id="pass2" placeholder="Again">
</div>
<button class="container-ele btn btn-outline-light action-btn" onclick="signup()">Sign Up</button>
<hr />
<a class="container-ele btn btn-outline-light third-login-method" href="/member/google/">
<img src="/static/member/image/googleg_standard_color_128dp.png" />
<span>Continue With Google</span>
</a>
<span>or</span>
<a class="container-ele btn btn-outline-light third-login-method" href="/member/login/">
<span>Log In</span>
</a>
</div>
{% endblock %}
{% block custom_body %}
<script>
let signup = function () {
let spk_client = document.getElementById("spk_client").value;
let username = document.getElementById("username").value;
let email = document.getElementById("email").value;
let pass1 = document.getElementById("pass1").value;
let pass2 = document.getElementById("pass2").value;
if (!username | !email | !pass1 | !pass2) {
alert("All fields required!");
} else if (pass1 != pass2) {
alert("Password not correspond!");
} else {
data = {
"username": cInfo(username, spk_client),
"email": cInfo(email, spk_client),
"pass": cInfo(pass1, spk_client)
}
console.log(data)
const request = new Request(
'.',
{
body: JSON.stringify(data),
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
},
method: 'POST',
mode: 'cors',
redirect: 'follow',
}
);
fetch(request)
.then(response => {
console.log(response)
if (response.redirected === true) {
location.href = response.url;
return;
} else {
if (!response.ok) {
info = {
"status": response.status,
"body": response.json()
}
return Promise.reject(info);
}
return response.json();
}
}).then(response => {
console.log(response);
alert("Sign up success! Please login.")
window.location.href = "/member/login/"
}).catch(function (error) {
console.log(error);
error.body.then((err) => {
alert("Sign up fail! " + err.message);
})
}).catch(function (error) {
console.log(error);
alert("Sign up fail!");
});
}
}
</script>
{% endblock %}
五、static,靜態檔案
從這裡下載靜態檔案包,並解壓縮到你的static資料夾,最後的資料夾結構應該要如下圖所示,其中png可以自行替換,使用相同檔名直接複寫原有檔案即可。
六、讓網站運行
首先,我們打開Terminal,並啟動虛擬環境:
source ~/VENV/bin/activate
切換到專案資料夾:
cd AlfredWiki
如果是第一次執行,我們需要先透過指令在MySQL建立Table,未來每當你的資料庫有新的表需要建立時都需要執行這段程式碼:
python manage.py migrate
讓網站開始運行:
python manage.py runserver
接著,我們在ubuntu中開啟terminal上的url,http://127.0.0.1:8000
:
成功的話應該會直接跳轉到登入頁,跟前面提到未登入自動跳轉到登入頁效果相同,眼尖的各位應該會發現有一個Continue With Google的按鈕,可以參考<Python Django 深度教學:OAuth 2.0 手動串接Google登入認證>這篇文章。
由於目前還沒有註冊任何使用者,所以請點擊註冊按鈕前往註冊,跟登入頁一樣可以Continue With Google。在User Name上這邊限制只能有大小寫英文字母及數字,密碼也要求大小寫英文字母、數字及符號,增加安全性。
註冊完成後可嘗試登入,登入可使用Username及Email,如果登入成功會出現logout連結,點擊後可登出,並重新導向至登入頁。
七、小結
隨著本文的逐步解析,我們得以深入理解前後端分離架構的實用性及其在搭建現代 Web 應用中的重要性。透過 Python Django 框架,我們不僅能夠實現一個具有靈活性與擴展性的身份驗證系統,而且還能保持程式碼的清晰與整潔。從實作細節到最終的測試運行,我們逐步構築了一套可靠的登入註冊流程,且遵循了 RESTful API 設計標準,確保了資料的安全性和傳輸的有效性。未來,這個系統甚至可以輕鬆地整合第三方 OAuth 2.0 身份驗證,進一步豐富用戶的登錄選擇。整個開發過程體現了全端技術的力量,並為讀者提供了一個明確的開發藍圖,用以擴展和定制自己的 Web 應用程序。
文中雖然針對資料安全性額外進行了一些處理措施,這些措施只是要提醒正在閱讀你們及撰寫文章的我,在開發的同時必須時刻記得資安的重要性,詳細需要那些措施,這些措施要在哪個程面實行,必須依照當下情境進行選擇及設置,可以的話盡可能與資安專家合作,找出最合適的解決方案。
總而言之,本文不僅提供了一個後端驗證的具體執行指南,也展示了前後端分離可為開發帶來的優勢與便利。透過此系統的實作,開發者能夠鑽研安全性、效能、以及用戶體驗的最佳實踐,從而推動其應用向著更加現代化與專業化的方向邁進。
Great article, exactly what I wanted to find.
Very rapidly this website will be famous among all blog visitors, due to its fastidious articles
There is definately a great deal to find out about this issue. I like all of the points you made.
Now that you’ve recognized the emotional state you end up in, choose a reflection useful resource from our assortment of artwork, music, prose, poetry and meditations. You should utilize the main target that meditation offers to then channel your thoughts towards reflection as soon as you’re completed. Netlify, AWS Amplify, and Github Pages are some of the top hosting providers, but it is best to analysis and explore choices earlier than selecting. It includes deciding on a framework or web site generator you need to make use of, discovering a method to retailer content material, writing the code, and hosting the positioning. However, the second possibility may be very efficient and precious if you’re looking ahead to studying a framework. Data Science and AI are the longer term for individuals who explore them and use their insights to shape the world. Once you know what’s possible, you can extra easily talk with these who’re responsible for knowledge era and assortment. Efficiency and effectiveness are paramount in the realm of PHP software development companies, where timely project supply underscores success.
Quality content is the secret to invite the people to pay a quick visit the
site, that’s what this web site is providing.
Hi! I could have sworn I’ve visited this
web site before but after browsing through some of the articles I realized it’s new to me.
Regardless, I’m definitely happy I stumbled upon it
and I’ll be book-marking it and checking back often!