🌈据说,看我文章时 关注、点赞、收藏 的 帅哥美女们 心情都会不自觉的好起来。
前言:
🧡作者简介:大家好我是 user_from_future ,意思是 “ 来自未来的用户 ” ,寓意着未来的自己一定很棒~
✨个人主页:点我直达,在这里肯定能找到你想要的~
👍专栏介绍:个人记账分析系统 ,专门记录制作过程,每天进步一点点~
想看往期历史文章,可以浏览此博文: 历史文章目录
,后续所有文章发布都会同步更新此博文~
前后端分离——实现登录注册功能
- 后端部分
- 登录视图
- 注册视图
- 图形验证码视图
- 邮件验证码视图
- 服务器时间视图
- 路由部分
- 前端部分
- 定义的路由
- 文件 App.vue
- 文件 Home.vue
- 文件 Login.vue
- 文件 Register.vue
- 效果
- 总结
后端部分
下文出现的 payment
是我创建的 app
,使用 django-admin startapp payment
创建,此项目设计为前后端完全分离,通过 {'code': 0, 'msg': 'str', 'data': object}
传递信息,data
可以省略。
登录视图
文件 payment\views.py
。
def login_(request):
if request.method == 'POST':
username = loads(request.body)['username']
password = loads(request.body)['password']
user = User.objects.filter(username=username) or User.objects.filter(email=username)
if user:
user = authenticate(username=user[0].username, password=password)
if user:
if user.is_active:
login(request, user)
Setting.objects.create(last_ip=request.META['REMOTE_ADDR'], owner=user)
request.session['uname'] = user.username
request.session['admin'] = user.is_superuser
response = JsonResponse({'code': 0, 'msg': '登录成功!'})
response.set_cookie('uname', user.username)
return response
return JsonResponse({'code': -1, 'msg': '用户状态不可用!'})
return JsonResponse({'code': -2, 'msg': '用户名密码错误!'})
return JsonResponse({'code': -3, 'msg': '用户名不存在!'})
return JsonResponse({'code': 1, 'msg': '请求方法只能是POST方法!'})
注册视图
文件 payment\views.py
。
def register(request):
if request.method == 'POST':
username = loads(request.body)['username']
password = loads(request.body)['password']
email = loads(request.body)['email']
yzm = loads(request.body)['yzm']
if User.objects.filter(username=username):
return JsonResponse({'code': -1, 'msg': '用户名已存在!'})
if request.session.get('email') != email or request.session.get('yzm') != yzm:
return JsonResponse({'code': -2, 'msg': '邮箱验证码错误!'})
user = User.objects.create_user(username=username, password=password, email=email)
if user.username:
return JsonResponse({'code': 0, 'msg': '注册成功!'})
return JsonResponse({'code': -3, 'msg': '注册失败!请重新尝试注册!'})
return JsonResponse({'code': 1, 'msg': '请求方法只能是POST方法!'})
图形验证码视图
文件 PersonalAccountWeb\views.py
。
图形验证码就由前端校验,简单通过 base64
加密验证码字符串。
def verification_code(request):
image = ImageCaptcha()
# 获得随机生成的验证码
captcha = ''.join(sample(list('abcdefghijklmnopqrstuvwxyz'), 4))
print("生成的验证码为:", captcha)
response = HttpResponse(image.generate(captcha), content_type='image/png')
# response['Access-Control-Allow-Origin'] = '*'
# response['Access-Control-Allow-Credentials'] = 'true'
response.set_cookie('yzm', b64encode(captcha.encode()).decode(), max_age=60 * 5)
return response
邮件验证码视图
文件 PersonalAccountWeb\views.py
。
邮件验证码就由后端校验,通过 session
保存,后续会用作异地登录验证(现在还没设计)。
def send_email_(to_address, header, html):
smtp_server = settings.EMAIL_HOST
msg = MIMEText(html, 'html', 'utf-8')
msg['From'] = Header(settings.EMAIL_HOST_USER)
msg['To'] = Header(to_address)
msg['Subject'] = Header(header)
server = smtplib.SMTP_SSL(smtp_server)
server.connect(smtp_server, settings.EMAIL_PORT)
server.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD) # python_gjj
server.sendmail(settings.EMAIL_HOST_USER, to_address, msg.as_string())
server.quit()
def send_email(request):
if request.method == 'POST':
to_address = loads(request.body)['email']
if User.objects.filter(email=to_address):
response = JsonResponse({'code': -1, 'msg': '邮箱已被注册!'})
else:
captcha = ''.join(sample(list('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 6))
print("生成的邮件验证码为:", captcha)
send_email_(to_address=to_address, header='邮箱验证码', html=f"""
<h1>【个人记账分析系统】</h1>
<p>您好,您正在注册我们的网站:<span style='color: red'><a href="http://{settings.CSRF_TRUSTED_ORIGINS[0]}">个人记账分析系统</a></span></p>
<p>您本次注册的验证码是:<span style='color: blue'>{captcha}</span></p>
<p>如果您<span style='color: red'>没有注册</span>,请<span style='color: red'>忽略并删除</span>本邮件!</p>
<p>感谢您对本系统的信赖!</p>
""")
request.session['yzm'] = captcha
request.session['email'] = to_address
response = JsonResponse({'code': 0, 'msg': '邮箱验证码发送成功!请注意查收!'})
else:
response = JsonResponse({'code': 1, 'msg': '请求方法只能是POST方法!'})
response.set_cookie('last', str(time()), max_age=60)
return response
服务器时间视图
文件 PersonalAccountWeb\api.py
。
安装了 ninja
都没用过,只能在这里用了。
from time import time
from ninja import NinjaAPI
api = NinjaAPI()
@api.get('/server_timestamp')
def hello(request):
return time()
路由部分
文件 PersonalAccountWeb\urls.py
。
from django.contrib import admin
from django.urls import path, include
from .api import api
from .views import verification_code, send_email
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls),
path('yzm/', verification_code),
path('email/', send_email),
path('', include('payment.urls'))
]
文件 payment\urls.py
。
from django.urls import path
from .views import info, login_, logout_, register, check_online
urlpatterns = [
path('info/', info),
path('login/', login_),
path('logout/',logout_),
path('register/', register),
path('check_online/', check_online)
]
前端部分
登录注册的UI参考了一个前端登录模板,略做修改就用在了我的项目上,前端登录模板 (点击即可跳转到下载页面)。
定义的路由
暂时这么多,后续可能会添加。
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHistory } from 'vue-router'
import C from './export_components.js'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: '跳转至首页',
redirect: '/Home',
},
{
path: '/Home',
name: '首页',
component: C.Home,
children: [
{
path: 'Login',
name: '登录',
component: C.Login
},
{
path: 'Register',
name: '注册',
component: C.Register
}
]
},
{
path: '/Backstage',
name: '后台',
component: C.Backstage,
children: [
{
path: 'Settings',
name: '设置',
component: C.Settings
},
{
path: 'DataEntry',
name: '数据录入',
component: C.DataEntry
},
{
path: 'DataShow',
name: '数据展示',
component: C.DataShow,
children: [
{
path: ':username',
name: '用户展示',
component: C.DataShow
}
]
},
{
path: 'InStationMail',
name: '站内邮件',
component: C.InStationMail
}
]
},
{
path: '/:pathMatch(.*)*',
name: '404',
// redirect: '/404',
component: C.NotFound
},
// {
// path: '/404',
// name: '404',
// component: NotFound
// }
]
})
createApp(App).use(router).mount('#app')
文件 App.vue
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
文件 Home.vue
主要保留了登录注册页的背景。
<template>
<div class="main">
<h1>个人记账分析系统</h1>
<div class="login box">
<router-view></router-view>
</div>
<div class="copywrite">
<p>Copyright © 2023</p>
</div>
</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
document.title = '个人记账分析系统'
const _this = getCurrentInstance().appContext.config.globalProperties // vue3获取当前this
if (_this.$route.query.next) {
_this.$router.push({ path: '/Home/Login', query: _this.$route.query})
} else {
_this.$router.push({ path: '/Home/Login', query: { next: '/Backstage' }})
}
</script>
<style scoped>
.main {
font-family: 'Catamaran', sans-serif;
font-size: 100%;
background: linear-gradient(to left top, #051937, #004d7a, #008793, #00bf72, #a8eb12);
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
-ms-background-size: cover;
min-height: 100vh;
text-align: center;
}
.login {
display: -webkit-flex;
display: -webkit-box;
display: -moz-flex;
display: -moz-box;
display: -ms-flexbox;
display: flex;
justify-content: center;
align-items: center;
-webkit-box-pack: center;
-moz-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
h1 {
font-size: 2.8em;
font-weight: 300;
text-transform: capitalize;
color: #fff;
text-shadow: 1px 1px 1px #000;
letter-spacing: 2px;
margin: 1.2em 1vw;
margin-top: 0px;
padding-top: 2.4em;
text-align: center;
font-family: 'Catamaran', sans-serif;
}
.copywrite {
margin: 4em 0 2em;
}
.copywrite p {
color: #fff;
font-size: 14.5px;
letter-spacing: 1.5px;
line-height: 1.8;
margin: 0 3vw;
}
.copywrite p a {
color: #fff;
transition: 0.5s all ease;
-webkit-transition: 0.5s all ease;
-moz-transition: 0.5s all ease;
-o-transition: 0.5s all ease;
-ms-transition: 0.5s all ease;
}
.copywrite p a:hover {
color: #fff;
text-decoration: underline;
}
@media(max-width:1024px) {
h1 {
font-size: 4.5vw;
}
}
@media(max-width:800px) {
h1 {
font-size: 5vw;
}
}
@media(max-width:480px) {
h1 {
font-size: 2.3em;
}
}
@media(max-width:568px) {
.login {
flex-direction: column;
}
}
@media(max-width:440px) {
h1 {
font-size: 2.1em;
}
}
@media(max-width:320px) {
h1 {
font-size: 1.8em;
}
}
</style>
文件 Login.vue
<template>
<form action="http://127.0.0.1:8000/login/" method="post" @submit.prevent="login">
<div class="agile-field-txt">
<input type="text" name="username" placeholder="用户名/邮箱" required="" />
</div>
<div class="agile-field-txt">
<input :type="pwd == '隐' ? 'text' : 'password'" name="password" placeholder="密码" required="" />
<a @click="pwd = pwd == '隐' ? '显' : '隐'">{{pwd}}</a>
</div>
<div class="yzm-field-txt">
<input type="text" id="yzm" name="yzm" placeholder="验证码" required="" />
<img :src="yzm_url" @click="yzm_url = 'http://127.0.0.1:8000/yzm/?time=' + new Date().getTime()"/>
</div>
<div class="bot">
<input type="submit" value="LOGIN" @click="check_yzm">
</div>
<div class="agile-field-txt">
<router-link to="/Home/Register?next=/Home/Login">还没有账户?点击注册</router-link>
</div>
</form>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
document.title = '个人记账分析系统'
const pwd = ref('显')
const yzm_url = ref('http://127.0.0.1:8000/yzm/')
const _this = getCurrentInstance().appContext.config.globalProperties // vue3获取当前this
const check_yzm = (event) => {
const yzm = document.getElementById('yzm')
var cookies = document.cookie.split("; ")
for (var index in cookies) {
if (cookies[index].startsWith('yzm=')) {
if (btoa(yzm.value) == cookies[index].replace('yzm="', '').replace('"', '')) {
return
}
}
}
yzm.value = ''
yzm_url.value = 'http://127.0.0.1:8000/yzm/?time=' + new Date().getTime()
}
const login = (event) => {
fetch('http://127.0.0.1:8000/login/', {
method: "POST",
credentials: "include",
headers: {
// 注意这里不要设置 Content-Type 请求头,否则会导致错误
},
Origin: window.location.protocol + "//" + window.location.host,
// fetch 的 body 发送 data
body: JSON.stringify({
username: event.target.username.value,
password: event.target.password.value
})
}).then(res => res.json()).then(json => {
console.log(json.msg)
if (!json.code) {
_this.$router.push({ path: _this.$route.query.next || '/Backstage'})
} else {
alert(json.msg)
}
})
}
</script>
<style scoped>
a {
top: 12px;
z-index: 1;
right: 10px;
color: yellow;
cursor: pointer;
position: absolute;
text-decoration: none;
}
.bot {
margin-top: 1em;
width: 100%;
}
form {
max-width: 500px;
margin: 0 5vw;
padding: 3.5vw;
border-width: 5px 0;
box-sizing: border-box;
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
background: rgba(252, 254, 255, 0.11);
}
.agile-field-txt {
position: relative;
flex-basis: 100%;
-webkit-flex-basis: 100%;
margin-bottom: 1.5em;
}
input[type="text"],
input[type="password"] {
width: 100%;
color: #fff;
outline: none;
background: rgba(0, 0, 0, 0.32);
font-size: 17px;
letter-spacing: 0.5px;
padding: 12px;
box-sizing: border-box;
border: none;
-webkit-appearance: none;
font-family: 'Catamaran', sans-serif;
}
::-moz-placeholder {
/* Undefined */
color: #eee;
}
::-webkit-input-placeholder {
/* Firefox */
color: #eee;
}
:-ms-input-placeholder {
/* Chrome */
color: #eee;
}
input[type=submit] {
color: #ffffff;
font-weight: 100;
width: 100%;
padding: 0.4em 0;
font-size: 1em;
font-weight: 400;
letter-spacing: 2px;
cursor: pointer;
border: none;
outline: none;
background: #000;
font-family: 'Catamaran', sans-serif;
transition: 0.5s all ease;
-webkit-transition: 0.5s all ease;
-moz-transition: 0.5s all ease;
-o-transition: 0.5s all ease;
-ms-transition: 0.5s all ease;
}
input[type=submit]:hover {
color: #fff;
box-shadow: 0 20px 5px -10px rgba(0, 0, 0, 0.4);
transform: translateY(5px);
}
.yzm-field-txt {
float: left;
width: 100%;
height: 60px;
position: relative;
}
.yzm-field-txt img {
float: left;
}
.yzm-field-txt input {
float: left;
margin: 7px 0;
width: calc(100% - 160px);
}
</style>
文件 Register.vue
<template>
<form action="http://127.0.0.1:8000/register/" method="post" @submit.prevent="register">
<div class="agile-field-txt">
<input type="text" name="username" placeholder="用户名" required="" />
</div>
<div class="agile-field-txt">
<input :type="pwd == '隐' ? 'text' : 'password'" name="password" placeholder="密码" required="" />
<a @click="pwd = pwd == '隐' ? '显' : '隐'">{{pwd}}</a>
</div>
<div class="agile-field-txt">
<input type="email" name="email" placeholder="邮箱" required="" v-model="email" />
</div>
<div class="yzm-field-txt">
<input type="text" name="yzm" placeholder="邮箱验证码" required="" />
<input type="button" id="verification" value="邮箱验证码" @click="send_email" />
</div>
<div class="bot">
<input type="submit" value="REGISTER">
</div>
<div class="agile-field-txt">
<router-link to="/Home/Login?next=/Backstage">已有账户?点击登录</router-link>
</div>
</form>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
document.title = '个人记账分析系统'
const _this = getCurrentInstance().appContext.config.globalProperties // vue3获取当前this
const pwd = ref('显')
const email = ref('')
const timer = ref(null)
const get_server_time = () => {
var time = new Date().getTime() / 1000
fetch('http://127.0.0.1:8000/api/server_timestamp', {
method: "GET",
credentials: "include",
headers: {
// 注意这里不要设置 Content-Type 请求头,否则会导致错误
},
Origin: window.location.protocol + "//" + window.location.host,
}).then(res => {time = parseFloat(res)})
return time
}
const calc_time = () => {
var verification = document.getElementById('verification')
var cookies = document.cookie.split("; ")
for (var index in cookies) {
if (cookies[index].startsWith('last=')) {
var last = parseFloat(cookies[index].replace('last=', ''))
var now = get_server_time()
if (last + 59 > now) {
verification.value = `重新发送(${parseInt(last + 59 - now)})`
timer.value = setTimeout(calc_time, 800)
} else {
verification.value = '邮箱验证码'
verification.disabled = false
clearTimeout(timer.value)
}
return
}
}
}
const send_email = () => {
var yzm = document.getElementById('verification')
if (!email.value) {
alert('请输入邮箱!')
return
}
fetch('http://127.0.0.1:8000/email/', {
method: "POST",
credentials: "include",
headers: {
// 注意这里不要设置 Content-Type 请求头,否则会导致错误
},
Origin: window.location.protocol + "//" + window.location.host,
// fetch 的 body 发送 data
body: JSON.stringify({
email: email.value
})
}).then(res => res.json()).then(json => {
alert(json.msg)
if (json.code >= 0) {
verification.disabled = true
timer.value = setTimeout(calc_time, 800)
}
})
}
const register = (event) => {
if (!/^\w{6,18}$/.test(event.target.username.value)) {
alert('用户名应该是长度为 6-18 的数字、大小写字母、下划线的组合!')
return
}
if (!/^\w{6,18}$/.test(event.target.password.value)) {
alert('密码应该是长度为 6-18 的数字、大小写字母、下划线的组合!')
return
}
fetch('http://127.0.0.1:8000/register/', {
method: "POST",
credentials: "include",
headers: {
// 注意这里不要设置 Content-Type 请求头,否则会导致错误
},
Origin: window.location.protocol + "//" + window.location.host,
// fetch 的 body 发送 data
body: JSON.stringify({
username: event.target.username.value,
password: event.target.password.value,
email: email.value,
yzm: event.target.yzm.value
})
}).then(res => res.json()).then(json => {
console.log(json.msg)
if (!json.code) {
_this.$router.push({ path: _this.$route.query.next || '/Home/Login'})
} else {
alert(json.msg)
}
})
}
</script>
<style scoped>
a {
top: 12px;
z-index: 1;
right: 10px;
color: yellow;
cursor: pointer;
position: absolute;
text-decoration: none;
}
.bot {
margin-top: 1em;
width: 100%;
}
form {
max-width: 500px;
margin: 0 5vw;
padding: 3.5vw;
border-width: 5px 0;
box-sizing: border-box;
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
background: rgba(252, 254, 255, 0.11);
}
.agile-field-txt {
position: relative;
flex-basis: 100%;
-webkit-flex-basis: 100%;
margin-bottom: 1.5em;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
color: #fff;
outline: none;
background: rgba(0, 0, 0, 0.32);
font-size: 17px;
letter-spacing: 0.5px;
padding: 12px;
box-sizing: border-box;
border: none;
-webkit-appearance: none;
font-family: 'Catamaran', sans-serif;
}
::-moz-placeholder {
/* Undefined */
color: #eee;
}
::-webkit-input-placeholder {
/* Firefox */
color: #eee;
}
:-ms-input-placeholder {
/* Chrome */
color: #eee;
}
input[type=button] {
color: yellow;
width: 160px;
height: 46px;
padding: 0.4em 0;
font-size: 1em;
font-weight: 400;
letter-spacing: 2px;
cursor: pointer;
border: 1px black solid;
outline: none;
background: rgba(0, 0, 0, 0);
}
input[type=submit] {
color: #ffffff;
font-weight: 100;
width: 100%;
padding: 0.4em 0;
font-size: 1em;
font-weight: 400;
letter-spacing: 2px;
cursor: pointer;
border: none;
outline: none;
background: #000;
font-family: 'Catamaran', sans-serif;
transition: 0.5s all ease;
-webkit-transition: 0.5s all ease;
-moz-transition: 0.5s all ease;
-o-transition: 0.5s all ease;
-ms-transition: 0.5s all ease;
}
input[type=submit]:hover {
color: #fff;
box-shadow: 0 20px 5px -10px rgba(0, 0, 0, 0.4);
transform: translateY(5px);
}
.yzm-field-txt {
float: left;
width: 100%;
position: relative;
}
.yzm-field-txt input[type=text] {
float: left;
width: calc(100% - 160px);
}
</style>
效果
总结
是不是看着登录注册界面简洁耐看呢~