文章目录

⭐前言
大家好,我是yma16,本文分享关于 vue3+threejs+koa可视化项目——实现登录注册。
jwt登录注册
JWT(JSON Web Token)是一种标准的身份验证和授权解决方案,它通过使用JSON格式的令牌来实现用户的身份验证和授权,避免了传统的基于会话的身份验证方案的一些问题。
JWT登录注册的原理如下:
-
注册:用户在注册时提供用户名和密码,服务器将用户信息保存在数据库中。密码通常需要进行哈希处理,以增加安全性。
-
登录:用户提供用户名和密码进行身份验证时,服务器验证用户名和密码是否匹配数据库中的记录。如果匹配成功,服务器会为该用户生成一个JWT令牌。
-
令牌生成:JWT由三部分组成,分别是头部(header)、载荷(payload)和签名(signature)。头部包含算法和令牌类型等信息,载荷包含用户的身份信息和其他自定义信息,签名用于验证令牌的合法性。
-
令牌签名:服务器使用服务器端的私钥对头部和载荷进行签名,生成签名部分。客户端接收到令牌后,可以使用服务器端的公钥进行验证,确保令牌没有被修改过。
-
令牌的验证和使用:客户端在每次请求时将令牌作为请求的一部分(通常是在请求头的"Authorization"字段中)发送到服务器。服务器通过解析令牌的签名部分并验证签名的合法性,确定令牌的有效性。如果通过验证,服务器可以根据令牌中的载荷部分进行用户的身份验证和授权等操作。
-
令牌的刷新:JWT令牌通常有一个过期时间,当令牌过期时,客户端需要重新获取新的令牌。在令牌过期之前,客户端可以使用刷新令牌来获取新的令牌,而无需重新进行身份验证。
通过JWT登录注册的方式,可以实现无状态的身份验证和授权,减少服务器的负担和数据库的访问频率。同时,JWT还可以跨多个服务进行验证和授权,提高了系统的可扩展性和安全性。
💖往期node系列文章
node_windows环境变量配置
node_npm发布包
linux_配置node
node_nvm安装配置
node笔记_http服务搭建(渲染html、json)
node笔记_读文件
node笔记_写文件
node笔记_连接mysql实现crud
node笔记_formidable实现前后端联调的文件上传
node笔记_koa框架介绍
node_koa路由
node_生成目录
node_读写excel
node笔记_读取目录的文件
node笔记——调用免费qq的smtp发送html格式邮箱
node实战——搭建带swagger接口文档的后端koa项目(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
node实战——koa实现文件下载和图片/pdf/视频预览(node后端储备知识)
💖threejs系列相关文章
THREE实战1_代码重构点、线、面
THREE实战2_正交投影相机与透视相机
THREE实战3_理解光源
THREE实战4_3D纹理
THREE实战5_canvans纹理
THREE实战6_加载fbx模型
💖vue3+threejs系列
vue3+threejs可视化项目——搭建vue3+ts+antd路由布局(第一步)
vue3+threejs可视化项目——引入threejs加载钢铁侠模型(第二步)
⭐koa后端登录注册逻辑(jwt)
用户表配置(用户名、密码、token)
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80016
Source Host : localhost:3306
Source Schema : threejs_data
Target Server Type : MySQL
Target Server Version : 80016
File Encoding : 65001
Date: 28/01/2024 23:56:13
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'id',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '加密的密码',
`real_pwd` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '真实密码',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'token',
`lasted_login_time` datetime(0) NULL DEFAULT NULL COMMENT '最近登录时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
💖 koa登录注册
逻辑实现
const Router = require('koa-router');
const router = new Router();
const {execMysql}=require('../../utils/mysql/index')
const {decrypt}=require('../../utils/aes/index')
const jwtToken = require("jsonwebtoken");
const {getRedisKey,setRedisConfig}=require('../../utils/redis/index');
//appKey
const {appKey}=require('../../common/const')
// 唯一字符串
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 当前时间
const getCurrentTime=() =>{
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth()
const date = now.getDate()
const hour = now.getHours()
const minutes = now.getMinutes()
const second = now.getSeconds()
const formatNum = (n) => {
return n > 9 ? n.toString() : '0' + n
}
return `${year}-${formatNum(month + 1)}-${formatNum(date)} ${formatNum(hour)}:${formatNum(minutes)}:${formatNum(second)}`
}
// 注册
router.post('/register', async (ctx) => {
try{
// 解析参数
const bodyParams = ctx.request.body
const {username,password,emailCode} = bodyParams;
console.log('emailCode',emailCode)
console.log('emailCode',emailCode)
if(!username||!password){
return ctx.body = {
code: 0 ,
msg:'username or password is null'
};
}
const emailRedisCode=await getRedisKey(username)
console.log('emailRedisCode',emailRedisCode)
if(emailCode!==emailRedisCode){
return ctx.body = {
code: 0 ,
msg:'email code is error'
};
}
// 查询重复
const search=await execMysql(`select count(1) as total from user where username='${username}';`)
console.log('search',search)
if(search[0].total>0){
return ctx.body = {
code: 0 ,
msg:'user is exist'
};
}
// id 唯一字符
const id= uuid()
const create_time=getCurrentTime()
console.log('password',password)
const real_pwd=await decrypt(password)
console.log('real_pwd',real_pwd)
// 插入 数据
const createRes=await execMysql(`INSERT INTO user (id,username,password,real_pwd,create_time) VALUES ('${id}', '${username}','${password}','${real_pwd}','${create_time}');`)
// 更新token update_time
const token=jwtToken.sign(
{
username,
password
},
appKey, // secret
{ expiresIn: 24 * 60 * 60 } // 60 * 60 s
)
const update_time=getCurrentTime()
const tokenRes=await execMysql(`update user set token='${token}', update_time='${update_time}' where username='${username}';`)
ctx.body = {
code:200,
data:{
createSqlData:createRes,
tokenSqlData:tokenRes
},
msg:' insert success',
token:token
};
}
catch (e) {
ctx.body = {
code:0,
msg:JSON.stringify(e)
};
}
});
// 获取token
router.post('/token/gen', async (ctx) => {
try{
// 解析参数
const bodyParams = ctx.request.body
const {username,password} = bodyParams;
const real_pwd=await decrypt(password);
// 查询 用户
const search=await execMysql(`select count(1) as total from user where username='${username}' and real_pwd='${real_pwd}';`)
if(search[0].total>0){
// 更新token update_time
const token=jwtToken.sign(
{
username,
password
},
appKey, // secret
{ expiresIn: 24 * 60 * 60 } // 60 * 60 s
)
const update_time=getCurrentTime()
// 更新token
const tokenRes=await execMysql(`update user set token='${token}', update_time='${update_time}' where username='${username}';`)
// 配置token
const AUTHORIZATION='Authorization'
ctx.set(AUTHORIZATION,token)
return ctx.body = {
code:200,
msg:'login success',
token:token
};
}
ctx.body = {
code:0,
msg:' login fail',
};
}
catch (e) {
ctx.body = {
code:0,
msg:e
};
}
});
// token 登录
router.post('/token/login',async (ctx) => {
try{
// 解析参数
const bodyParams = ctx.request.body
const {token} = bodyParams;
const payload = jwtToken.verify(token, appKey);
const {username,password} =payload
const real_pwd=await decrypt(password);
// 查询 用户
const search=await execMysql(`select count(1) as total from user where username='${username}' and real_pwd='${real_pwd}';`)
console.log(search)
if(search[0].total>0){
const last_login_time=getCurrentTime()
// last_login_time 登录时间
const tokenRes=await execMysql(`update user set lasted_login_time='${last_login_time}' where username='${username}' and password='${password}';`)
return ctx.body = {
code:200,
msg:'login success',
data:{
username
}
};
}
ctx.body = {
code:0,
msg:' login fail',
};
}
catch (e) {
console.log('e',e)
ctx.body = {
code:0,
msg:JSON.stringify(e)
};
}
})
module.exports = router;
⭐vue3前端登录注册权限控制
💖 登录页面
login/index.vue
<template>
<div class="container">
<div class="loginUser-container">
<div class="loginUser-title"></div>
<a-form
:model="state.formState"
:label-col="state.layoutConfig.labelCol"
:wrapper-col="state.layoutConfig.wrapperCol"
:rules="state.formRule"
ref="formRef"
layout="vertical"
autocomplete="off"
>
<a-form-item label="账号" name="username">
<a-input
v-model:value="state.formState.username"
allowClear
placeholder="账号"
:disabled="state.spinning"
/>
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password
v-model:value="state.formState.password"
:disabled="state.spinning"
allowClear
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item name="remember" :wrapper-col="state.wrapperCol">
<a-checkbox
v-model:checked="state.formState.remember"
:disabled="state.spinning"
>记住密码</a-checkbox
>
</a-form-item>
<a-form-item :wrapper-col="state.submitWrapperCol" class="submit-box">
<a-button
type="primary"
html-type="submit"
@click="loginAction"
:loading="state.spinning"
style="width: 100%; font-size: 16px; font-weight: bolder"
>登录</a-button
>
</a-form-item>
</a-form>
<div class="description">
<span class="description-prefix">没账号?</span>
<span
@click="jumpRegister"
class="description-after"
:disabled="state.spinning"
>去注册</span
>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { message } from "ant-design-vue";
import {aes} from '@/utils/index'
import type { FormInstance } from "ant-design-vue";
interface FormStateType {
username: string;
password: string;
remember: boolean;
}
interface FormRuleType {
username: Object;
password: Object;
}
interface stateType {
formState: FormStateType;
formRule: FormRuleType;
layoutConfig: any;
wrapperCol: any;
submitWrapperCol: any;
spinning: boolean;
backgroundImgUrl: string;
}
// 路由
const router = useRouter();
//store
const store = useStore();
const formRef = ref<FormInstance>();
const state: stateType = reactive({
formState: {
username: "",
password: "",
remember: false,
},
spinning: false,
formRule: {
username: [{ required: true, message: "请输入账号!" }],
password: [{ required: true, message: "请输入密码!" }],
},
layoutConfig: {
labelCol: {
span: 8,
},
wrapperCol: {
span: 24,
},
},
wrapperCol: { offset: 0, span: 24 },
submitWrapperCol: { offset: 0, span: 24 },
backgroundImgUrl:
"http://www.yongma16.xyz/staticFile/common/img/background.png",
});
/**
* 初始化表单内容
*/
const initForm = () => {
const userInfoItem: any = window.localStorage.getItem("userInfo");
interface userInfoType {
username: string;
password: string;
remember: boolean;
}
const userInfo: userInfoType = userInfoItem
? JSON.parse(userInfoItem)
: {
username: "",
password: "",
remember: false,
};
if (userInfo.username && userInfo.password) {
state.formState.username = userInfo.username;
state.formState.password = aes.decrypt(userInfo.password);
state.formState.remember = userInfo.remember;
}
};
/**
* 前往注册!
*/
const jumpRegister = () => {
// 带 hash,结果是 /about#team
router.push({ path: "/register" });
};
/**
* 前往home!
*/
const jumpHome = () => {
// 带 hash,结果是 /about#team
router.push({ path: "/" });
};
/**
* 记住密码
* @param params
*/
const rememberAction = (params: Object) => {
window.localStorage.setItem("userInfo", JSON.stringify(params));
};
/**
* 登录
*/
const loginAction = () => {
if (formRef.value) {
formRef.value.validate().then(async (res: any) => {
state.spinning = true;
const params = {
username: state.formState.username,
password: aes.encrypt(state.formState.password),
};
if (state.formState.remember) {
rememberAction({ ...params, remember: state.formState.remember });
}
try {
console.log('登录',params)
// @ts-ignore
await store.dispatch(
"user/getUserTokenAction",
params
);
// 跳转
setTimeout(() => {
jumpHome();
}, 500);
state.spinning = false;
} catch (r: any) {
message.error(JSON.stringify(r));
state.spinning = false;
throw Error(r);
}
});
}
};
onMounted(() => {
//初始化
initForm();
});
</script>
<style lang="less">
.background {
/*background: #1890ff; !* fallback for old browsers *!*/
/*background: -webkit-linear-gradient(to top, #000C40, #F0F2F0); !* Chrome 10-25, Safari 5.1-6 *!*/
/*background: linear-gradient(to top, #000C40, #F0F2F0); !* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ *!*/
/*background-image: url("http://yongma16.xyz/staticFile/common/img/background.png");*/
/*background-repeat: no-repeat;*/
/*background-size: 100%;*/
}
.container {
/*background: #262626;*/
background-clip: border-box;
position: absolute;
width: 100vw;
height: 100vh;
.background();
}
.loginUser-container {
position: absolute;
min-width: 400px;
min-height: 350px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.submit-box {
text-align: center;
width: 100%;
margin: 0 auto;
}
.loginUser-container {
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
padding: 0 20px;
}
.loginUser-title {
margin-top: 20px;
width: 100%;
text-align: center;
font-weight: bolder;
font-size: 16px;
}
.description {
margin-top: 20px;
width: 100%;
text-align: center;
.description-after {
color: #1890ff;
cursor: pointer;
}
}
</style>
登录页面
💖 注册页面
register/index.vue
<template>
<a-spin tip="登录中..." :spinning="state.spinning">
<!-- <div-->
<!-- class="container"-->
<!-- :style="{-->
<!-- backgroundImage:url(state.backgroundImgUrl),-->
<!-- }"-->
<!-- >-->
<div class="container">
<div class="register-container">
<div class="register-title"></div>
<a-form
:model="state.formState"
:label-col="state.layoutConfig.labelCol"
:wrapper-col="state.layoutConfig.wrapperCol"
:rules="state.formRule"
ref="formRef"
layout="vertical"
autocomplete="off"
>
<a-row>
<a-col :span="16">
<a-form-item label="邮箱" name="username">
<a-input
v-model:value="state.formState.username"
allowClear
placeholder="请输入邮箱"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item style="margin-top:31.53px;text-align: right">
<a-button @click="sendEmail" type="primary" :loading="state.loadingEmailCode" >
{{state.awaitTime>0?`${state.awaitTime}s`:'发送验证码'}}
</a-button>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="验证码" name="code">
<a-input
:disabled="state.isErrorEmail"
v-model:value="state.formState.code"
allowClear
placeholder="请输入验证码"
/>
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password
:disabled="state.isErrorEmail"
v-model:value="state.formState.password"
allowClear
placeholder="请输入验证码"
/>
</a-form-item>
<a-form-item label="确认密码" name="passwordBack">
<a-input-password
:disabled="state.isErrorEmail"
v-model:value="state.formState.passwordBack"
allowClear
placeholder="请确认密码"
/>
</a-form-item>
<a-form-item :wrapper-col="state.submitWrapperCol" class="submit-box">
<a-button
type="primary"
html-type="submit"
@click="registerAction"
:loading="state.spinning"
style="width: 100%; font-weight: bolder"
>注册</a-button
>
</a-form-item>
</a-form>
<div class="description">
<span class="description-prefix">有账号?</span>
<span @click="jumpLogin" class="description-after">去登录</span>
</div>
</div>
</div>
</a-spin>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { message } from "ant-design-vue";
import { registerUser,getEmailCode } from "@/service/user/index";
import {aes} from '@/utils/index'
import type { Rule } from "ant-design-vue/es/form";
import type { FormInstance } from "ant-design-vue";
import { useStore } from "vuex";
interface FormStateType {
username: string;
password: string;
passwordBack: string;
code:string;
remember: boolean;
}
interface FormRuleType {
username: Object;
password: Object;
}
interface stateType {
formState: FormStateType;
formRule: FormRuleType;
layoutConfig: any;
wrapperCol: any;
submitWrapperCol: any;
spinning: boolean;
backgroundImgUrl: string;
isErrorEmail:boolean,
remoteEmailCode:string,
loadingEmailCode:boolean,
awaitTime:number
}
// 路由
const router = useRouter();
//store
const store = useStore();
const formRef = ref<FormInstance>();
const state: stateType = reactive({
formState: {
code:'',
username: "1432448610@qq.com",
password: "",
passwordBack: "",
remember: false,
},
spinning: false,
formRule: {
username: [{ required: true, message: "请输入邮箱!" },{validator:validatorEmail,trigger:'blur'}],
code: [{ required: false, message: "请输入验证码!" }],
password: [{ required: true, message: "请输入密码!" }],
passwordBack: [
{ required: true },
{ validator: validatePass, trigger: "blur" },
],
},
layoutConfig: {
labelCol: {
span: 8,
},
wrapperCol: {
span: 24,
},
},
awaitTime:-1,
remoteEmailCode:'',
isErrorEmail:true,
loadingEmailCode:false,
wrapperCol: { offset: 0, span: 24 },
submitWrapperCol: { offset: 0, span: 24 },
backgroundImgUrl:
"http://www.yongma16.xyz/staticFile/common/img/background.png",
});
// sleep
const sleep=(delay:number)=>{
return new Promise(resolve=>setTimeout(resolve,delay*1000))
}
/**
* 前往登录!
*/
const jumpLogin = () => {
// 带 hash,结果是 /about#team
router.push({ path: "/login" });
};
// 确认密码
async function validatePass(_rule: Rule, value: string) {
if (value === "") {
return Promise.reject("请确认密码!");
} else if (value !== state.formState.password) {
return Promise.reject("密码不一致!");
} else {
return Promise.resolve();
}
}
// 校验邮箱
async function validatorEmail(_rule: Rule, value: string) {
if (value === "") {
state.isErrorEmail=true
return Promise.reject("请输入邮箱!");
} else {
const reg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
if (!reg.test(value)) {
state.isErrorEmail=true
return Promise.reject("邮箱格式不正确!");
}
state.isErrorEmail=false
return Promise.resolve();
}
}
// 校验邮箱
async function validatorEmailCode(_rule: Rule, value: string) {
if (value!==state.remoteEmailCode) {
return Promise.reject("验证码不正确!");
}
return Promise.resolve();
}
/**
* 前往home!
*/
const jumpHome = () => {
// 带 hash,结果是 /about#team
router.push({ path: "/" });
};
/**
* 注册
*/
const registerAction = () => {
if (formRef.value) {
formRef.value.validate().then((res: any) => {
state.spinning = true;
const params = {
username: state.formState.username,
password: aes.encrypt(state.formState.password),
emailCode:state.formState.code
};
registerUser(params)
.then((res: any) => {
state.spinning = false;
const { data: response } = res;
console.log('response',response)
if (response.code === 200) {
store.commit("user/setUserToken", response.data);
// 跳转
setTimeout(()=>{
jumpHome();
},500)
message.success(response.message);
} else {
message.warning(response.msg);
}
})
.catch((r: any) => {
state.spinning = false;
message.error(JSON.stringify(r));
throw Error(r);
});
});
}
};
const delayTime=async ()=>{
if (state.awaitTime>0) {
await sleep(1)
state.awaitTime-=1
delayTime()
}
};
const sendEmail=async ()=>{
if(state.awaitTime>0){
return
}
if(!state.formState.username){
return message.warn('请输入邮箱!')
}
// if(state.isErrorEmail)
// {
// return message.warn('请检查邮箱格式!')
// }
try{
state.loadingEmailCode=true
const res=await getEmailCode({
email:state.formState.username
})
if(res?.data?.data?.emailRes?.code==200){
// state.remoteEmailCode=res.data.data.code
state.isErrorEmail=false
// 倒计时
state.awaitTime=10
message.success('发送邮件成功!请查收\t'+state.formState.username)
delayTime()
}
else{
state.isErrorEmail=false
message.warn(res.data.data.emailRes.msg.response)
}
}
catch (e) {
message.error(JSON.stringify(e))
}
finally {
state.loadingEmailCode=false
}
}
</script>
<style lang="less">
.background {
/*background: #1890ff; !* fallback for old browsers *!*/
/*background: -webkit-linear-gradient(to top, #000C40, #F0F2F0); !* Chrome 10-25, Safari 5.1-6 *!*/
/*background: linear-gradient(to top, #000C40, #F0F2F0); !* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ *!*/
}
.container {
/*background: #262626;*/
background-clip: border-box;
position: absolute;
width: 100vw;
height: 100vh;
.background();
}
.register-container {
position: absolute;
min-width: 400px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
}
.submit-box {
text-align: center;
width: 100%;
margin: 0 auto;
}
.register-container {
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
padding: 0 20px;
}
.register-title {
/*background: #1890ff;*/
/*color:#fff;*/
width: 100%;
text-align: center;
font-weight: bolder;
padding: 20px;
font-size: 24px;
}
.description {
margin-top: 20px;
width: 100%;
text-align: center;
.description-after {
color: #1890ff;
cursor: pointer;
}
}
</style>
注册页面
注册发送邮件
⭐总结
前端vue3
a. 路由拦截
b. store缓存token
后端koa
a. jwt配置
b. jwt的白名单(发送验证码、注册、获取token)
⭐结束
本文分享到这结束,如有错误或者不足之处欢迎指出!