管理员登录 前端 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 from common.config import configHOST = 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 from sqlalchemy import create_enginefrom sqlalchemy.orm import DeclarativeBase, sessionmakerfrom common.constant import *class Base (DeclarativeBase ): pass 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 from pydantic import BaseModelfrom sqlalchemy import Integer, Stringfrom sqlalchemy.orm import Mapped, mapped_columnfrom model import Baseclass 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 from sqlalchemy import create_enginefrom sqlalchemy.orm import DeclarativeBase, sessionmakerfrom common.constant import *class Base (DeclarativeBase ): pass 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 from fastapi import Body, Dependsfrom fastapi.encoders import jsonable_encoderfrom api import appfrom common.result import Result, ResultModelfrom model import Session, get_db_sessionfrom model.admin import AdminModelfrom 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 from sqlalchemy import selectfrom exception.customException import UserNotFoundException, PasswordNotMatchExceptionfrom model import Sessionfrom model.admin import Admin, AdminModelclass 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 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 from fastapi.encoders import jsonable_encoderfrom starlette.responses import JSONResponsefrom api import appfrom fastapi import Requestfrom common.result import Resultfrom 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 from fastapi import FastAPIapp = 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 from fastapi import FastAPIfrom starlette.middleware.cors import CORSMiddlewareapp = FastAPI() origins = ["http://localhost:5173" , "http://127.0.0.1:5173" ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True , allow_methods=["*" ], allow_headers=["*" ], ) 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 import bcryptfrom pydantic import BaseModelfrom sqlalchemy import Integer, Stringfrom sqlalchemy.orm import Mapped, mapped_columnfrom model import Baseclass 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 from sqlalchemy import selectfrom exception.customException import UserNotFoundException, PasswordNotMatchExceptionfrom model import Sessionfrom model.admin import Admin, AdminModelclass 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
Author:
Zachary Block
Permalink:
http://blockzachary.cn/blog/701228072/
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE