管理员登录

前端

Login

首先绘制一个登录页面

参考 element-plus 官网提供的表单https://element-plus.org/zh-CN/component/form.html

在项目路径/src/views下创建一个Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form">
<el-form-item>
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item>
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%;font-size: 18px"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";

const data = reactive({
form: {},
});
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

背景图片

添加一张登录背景图片到/src/assets/imags/login_background.png

https://iconscout.com/free-illustration/children-are-doing-chemical-experiments-10946611

路由添加

/src/router/index.js中添加一下上面这个Login的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Manager",
component: () => import("@/views/Manager.vue"),
redirect: "/home",
children: [
{
path: "home",
name: "Home",
component: () => import("@/views/manager/Home.vue"),
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
],
});

export default router;

通过http://localhost:5173/login可以访问到登录页面

表单校验和登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%;font-size: 18px"
@click="login"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { reactive, ref } from "vue";
import request from "@/utils/request";
import { ElMessage } from "element-plus";
import router from "@/router";

const data = reactive({
form: {},
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const login = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/login", data.form)
.then((res) => {
if (res.code === "200") {
localStorage.setItem("student-user", JSON.stringify(res.data));
ElMessage.success("登录成功");
router.push("/home");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

后端

数据库

IDE 连接 MySQL

创建库

首先在数据库中创建一个 student_info 库

创建 admin 表

插入一条数据

数据库配置

查看 sqlalchemy 官方文档https://docs.sqlalchemy.org/en/20/dialects/mysql.html#module-sqlalchemy.dialects.mysql.mysqldb

requirements.txt

通过 sqlalchemy 连接数据库需要增加两个驱动

1
2
3
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23

.env

在.env 文件下添加数据库的配置信息(记得改成你们自己的内容)

1
2
3
4
5
6
7
8
9
HOST = "localhost"
PORT = "9090"

MYSQL_DIALECT = "mysql+mysqldb"
MYSQL_HOST = "localhost"
MYSQL_PORT = "3306"
MYSQL_USER = "root"
MYSQL_PASSWORD = "XXXXXX"
MYSQL_DATABASE = "student_info"

constant.py

添加数据库配置常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding:utf-8 -*-
# Author: Zachary

from common.config import config

HOST = config.env.get("HOST")
PORT = config.env.get("PORT")

MYSQL_DIALECT = config.env.get("MYSQL_DIALECT")
MYSQL_HOST = config.env.get("MYSQL_HOST")
MYSQL_PORT = config.env.get("MYSQL_PORT")
MYSQL_USER = config.env.get("MYSQL_USER")
MYSQL_PASSWORD = config.env.get("MYSQL_PASSWORD")
MYSQL_DATABASE = config.env.get("MYSQL_DATABASE")

model/init.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from common.constant import *


class Base(DeclarativeBase):
pass


# mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = create_engine(
f"{MYSQL_DIALECT}://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4",
echo=True)

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

管理员实体类定义

/model下创建一个admin.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)


class AdminModel(BaseModel):
username: str
password: str

管理员登录的 api 接口

为了实现每一个 api 都能拿到一个连接数据库的 session,在/model/__init__.py中实现一个 session 的获取方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from common.constant import *


class Base(DeclarativeBase):
pass


# mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = create_engine(
f"{MYSQL_DIALECT}://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4",
echo=True)

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)


def get_db_session():
session = Session()
try:
yield session
finally:
session.close()

adminApi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import Body, Depends
from fastapi.encoders import jsonable_encoder

from api import app
from common.result import Result, ResultModel
from model import Session, get_db_session
from model.admin import AdminModel
from service.adminService import AdminService


@app.post("/login", response_model=ResultModel)
async def login(admin: AdminModel = Body(...), db_session: Session = Depends(get_db_session)):
dbadmin = AdminService.login(admin, db_session)
return Result.success(jsonable_encoder(dbadmin))

adminService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import select

from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
result = db_session.execute(query).scalars().first()
if not result:
raise UserNotFoundException("用户不存在")
if result.password != admin.password:
raise PasswordNotMatchException("身份验证未通过")
return result

exception

创建自定义异常的exception的 package

然后创建一个customException.py

1
2
3
4
5
6
7
8
9
10
11
# -*- coding:utf-8 -*-
# Author: Zachary

class UserNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class PasswordNotMatchException(Exception):
def __init__(self, message: str):
self.message = message

exceptionHandler

/api包下面创建一个exceptionHandler.py用于处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse

from api import app
from fastapi import Request

from common.result import Result
from exception.customException import UserNotFoundException, PasswordNotMatchException


@app.exception_handler(UserNotFoundException)
async def user_not_fount_exception_handler(request: Request, exc: UserNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(PasswordNotMatchException)
async def password_not_fount_exception_handler(request: Request, exc: PasswordNotMatchException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))

/api/init.py

1
2
3
4
5
6
7
8
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI

app = FastAPI()

from api import adminApi, exceptionHandler

测试

postman 测试后端接口

正确登录:

密码错误:

账号错误:

前后端测试

跨域问题

发生了跨域 CORS 的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()

# 跨域问题
origins = ["http://localhost:5173", "http://127.0.0.1:5173"] # 替换为你的前端应用的实际地址

app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 允许跨域访问的来源域名列表
allow_credentials=True, # 是否允许携带cookie
allow_methods=["*"], # 允许的方法,默认包含常见的GET、POST等,"*"表示所有方法
allow_headers=["*"], # 允许的请求头,默认包含常见的Content-Type等,"*"表示所有请求头
)

from api import adminApi, exceptionHandler

正确账号密码:

错误账号:

错误密码:

数据库密码

为了安全,数据库中的 password 不应该以明文显示,需要做个加密

这里需要使用到一个库 bcrypt

1
2
3
4
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23
bcrypt==4.1.1

密码加密

通过hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) 依据生成的盐值,生成 hash 后的密码值,结果是字节码

1
2
3
4
5
6
7
8
9
10
(venv) ➜  studentbackend python3
Python 3.10.11 (v3.10.11:7d4cc5aa85, Apr 4 2023, 19:05:19) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import bcrypt
>>> password = "admin"
>>> hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
>>> print(hashed_pw)
b'$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum'
>>> print(hashed_pw.decode('utf-8'))
$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum

将密码对应的加密密码 $2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum 替换到数据库中的 admin 账号中(这一步操作其实应该是,在注册的时候将这个密码值写入数据库 de~)

密码验证

通过bcrypt.check(check_pw.encode(), hashed_pw)检验密码是否匹配

给 Admin 实体类定义一个密码检查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding:utf-8 -*-
# Author: Zachary
import bcrypt
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)

def password_check(self, password):
return bcrypt.checkpw(password.encode(), self.password.encode())


class AdminModel(BaseModel):
username: str
password: str
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import select

from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
result = db_session.execute(query).scalars().first()
if not result:
raise UserNotFoundException("用户不存在")
if result.password_check(admin.password) is False:
raise PasswordNotMatchException("身份验证未通过")
return result

前后端测试

密码错误:

密码正确:


更新: 2024-05-03 22:08:05
原文: https://www.yuque.com/zacharyblock/iacda/qhg30r2hbk5wb58m