🌈据说,看我文章时 关注、点赞、收藏 的 帅哥美女们 心情都会不自觉的好起来。
前言:
🧡作者简介:大家好我是 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>
效果

 
总结
是不是看着登录注册界面简洁耐看呢~















![[NCTF2019]SQLi](https://img-blog.csdnimg.cn/c9a891c04626421998e8330691045826.png)


![[附源码]计算机毕业设计springboot公共台账管理系统](https://img-blog.csdnimg.cn/fd30e088d06a4f53bffe09dd2323e858.png)
