本次目标完成一个简易的授权登录:
- 前端使用
Vue3、VueRouter、Paina、axios和Element Plush- 服务端使用
Node+Express做简易的JWT鉴权
前端登录页面搭建
简易登录界面实现
效果图
代码实例
在项目的src/views/Login下新建一个index.vue来实现组件的内容
<template>
<div class="login-container">
<el-card class="login-card">
<el-form
ref="loginForm"
:model="form"
:rules="rules"
label-position="top"
class="login-form"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="form.username"
prefix-icon="el-icon-user"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
type="password"
prefix-icon="el-icon-lock"
placeholder="请输入密码"
show-password
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="login-button"
@click="submitForm"
:loading="loading"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import axios from "@/axios";
import { ElMessage } from "element-plus";
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/store/auth";
const router = useRouter();
const form = reactive({
username: "",
password: "",
});
const rules = {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{ min: 3, max: 20, message: "长度在 3 到 20 个字符", trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" },
],
};
const loginForm = ref(null);
const loading = ref(false);
const submitForm = () => {
loginForm.value.validate((valid) => {
if (valid) {
loading.value = true;
axios.post("/login", form).then(({ token }) => {
ElMessage.success("登录成功");
loading.value = false;
useAuthStore().login(token);
router.push("/");
});
} else {
ElMessage.error("表单验证失败");
}
});
};
</script>
<style scoped>
/* 全屏背景色 */
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw; /* 确保宽度铺满全屏 */
background-color: #f5f7fa; /* 淡色背景 */
overflow: hidden;
}
/* 登录卡片样式 */
.login-card {
width: 400px;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
background-color: #ffffff; /* 卡片背景色 */
}
/* 登录表单样式 */
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 登录按钮样式 */
.login-button {
width: 100%;
}
</style>
在src/下新建一个axios.ts的脚本文件来做简易的axios的封装和请求、响应拦截内容
// src/axios.js
import axios from 'axios';
const instance = axios.create({
baseURL: '/api', // 设置基础 URL
timeout: 5000, // 设置超时时间
});
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么,例如添加 Token
config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`;
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 对响应数据做点什么
return response.data;
},
(error) => {
// 对响应错误做点什么
if (error.response.status === 401) {
// 处理 401 未授权错误
console.error('未授权,请重新登录');
}
return Promise.reject(error);
}
);
export default instance;
src/store新建auth.ts来存放登录相关的逻辑处理(项目中不存在store文件夹自行创建),这里会将获取到的token信息临时存放到sessionStorage中,后期会调整
import { defineStore } from "pinia";
interface State {
token: string | null;
isLoggedIn: boolean;
}
export const useAuthStore = defineStore("auth", {
state: (): State => ({
token: sessionStorage.getItem("token"),
isLoggedIn: !!sessionStorage.getItem("token"),
}),
actions: {
login(token: string) {
this.token = token;
this.isLoggedIn = true;
sessionStorage.setItem("token", token);
},
logout() {
this.token = null;
this.isLoggedIn = false;
sessionStorage.removeItem("token");
}
},
});
vite代码配置
在vite.config.ts中配置代理来保证登录接口的调用
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': '/src', // 确保这里的路径是正确的
},
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})
服务端搭建与实现
项目主要练习的是Vue3的相关内容所以服务端的搭建相对简单,能支持正常授权登录即可
- 服务端监听了3000端口,如果冲突可自行修改,修改后需要同步调整前端代理的端口,否则会导致登录接口调用不通
- 因目前没有数据库支撑,为了能完成认证签信息签发,系统可以正常登录,代码中预制了
admin和password123作为默认的用户名和密码,后续接入数据库后会将用户信息持久化到数据库。
新建index.js文件来搭建一个简易的node服务支持登录、注册和登出等需求,具体代码如下:
const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
require('dotenv').config();
const { JWT_SECRET } = process.env;
const app = express();
const port = 3000;
// 中间件
app.use(bodyParser.json());
// 模拟用户数据
const users = [
{
id: 1,
username: 'admin',
password: bcrypt.hashSync('password123', 10) // 使用 bcrypt 加密密码
}
];
// 注册接口
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = bcrypt.hashSync(password, 10);
const newUser = {
id: users.length + 1,
username,
password: hashedPassword
};
users.push(newUser);
res.send({ message: 'User registered successfully' });
});
// 登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).send({ message: 'Invalid credentials' });
}
// 生成 JWT
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });
res.send({ message: 'Login successful', token });
});
// 验证 JWT 的中间件
const authenticateToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).send({ message: 'Unauthorized' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).send({ message: 'Forbidden' });
}
req.user = user;
next();
});
};
// 获取用户信息接口
app.get('/profile', authenticateToken, (req, res) => {
res.send({ message: 'Welcome to your profile', user: req.user.username });
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
至此,已经成功的搭建了一套简易的登录页面,如有问题欢迎留言讨论,共同学习~