文章目录
vue3_mysql__1">node.js + vue3 +mysql 应用
以下是一个使用koa
作为后端(包含登录、token
验证、用户增删改查和权限控制),MySQL
作为数据库,Vue 3
作为前端展示的示例:
后端实现(koa部分)
1. 项目初始化与依赖安装
创建后端项目文件夹,在命令行进入该文件夹后执行以下命令:
npm init -y
npm install koa koa-router koa-bodyparser jsonwebtoken mysql2 bcryptjs cors
2. 目录结构
backend
|-- app.js
|-- controllers
| |-- authController.js
| |-- userController.js
|-- middlewares
| |-- authMiddleware.js
|-- routes
| |-- index.js
| |-- user.js
|-- models
| |-- user.js
|-- config
| |-- db.js
3. config/db.js
- 数据库配置与连接
const mysql = require('mysql2/promise');const pool = mysql.createPool({host: 'localhost',user: 'root',password: 'your_password',database: 'your_database_name',waitForConnections: true,connectionLimit: 10,queueLimit: 0
});module.exports = pool;
4. models/user.js
- 用户模型及数据库操作
const pool = require('../config/db.js');class User {static async findByUsername(username) {const [rows] = await pool.query('SELECT * FROM users WHERE username =?', [username]);return rows[0];}static async findById(id) {const [rows] = await pool.query('SELECT * FROM users WHERE id =?', [id]);return rows[0];}static async create(userData) {const { username, password, role, menus } = userData;const [result] = await pool.query('INSERT INTO users (username, password, role, menus) VALUES (?,?,?,?)', [username, password, role, menus]);return result.insertId;}static async update(id, updatedUser) {const { password, role, menus } = updatedUser;const query = 'UPDATE users SET password =?, role =?,menus =? WHERE id =?';const [result] = await pool.query(query, [password, role, menus, id]);return result.affectedRows > 0;}static async delete(id) {const [result] = await pool.query('DELETE FROM users WHERE id =?', [id]);return result.affectedRows > 0;}static async getAll() {const [rows] = await pool.query('SELECT * FROM users');return rows;}
}module.exports = User;
5. controllers/authController.js
- 认证控制器
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/user.js');const login = async (ctx) => {const { username, password } = ctx.request.body;const user = await User.findByUsername(username);if (user) {const isMatch = await bcrypt.compare(password, user.password);if (isMatch) {const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, 'your_secret_key', { expiresIn: '1h' });ctx.body = { success: true, token };} else {ctx.status = 401;ctx.body = { success: false, message: '密码错误' };}} else {ctx.status = 404;ctx.body = { success: false, message: '用户不存在' };}
};module.exports = {login
};
6. controllers/userController.js
- 用户操作控制器
const User = require('../models/user.js');const getUsers = async (ctx) => {const users = await User.getAll();ctx.body = users;
};const getUserById = async (ctx) => {const id = ctx.params.id;const user = await User.findById(id);if (user) {ctx.body = user;} else {ctx.status = 404;ctx.body = { message: '用户不存在' };}
};const updateUser = async (ctx) => {const id = ctx.params.id;const updatedUser = ctx.request.body;const result = await User.update(id, updatedUser);if (result) {ctx.body = { message: '用户更新成功' };} else {ctx.status = 404;ctx.body = { message: '用户不存在,无法更新' };}
};const deleteUser = async (ctx) => {const id = ctx.params.id;const result = await User.delete(id);if (result) {ctx.body = { message: '用户删除成功' };} else {ctx.status = 404;ctx.body = { message: '用户不存在,无法删除' };}
};module.exports = {getUsers,getUserById,updateUser,deleteUser
};
7. middlewares/authMiddleware.js
- token
验证中间件
const jwt = require('jsonwebtoken');const authMiddleware = async (ctx, next) => {const token = ctx.headers.authorization && ctx.headers.authorization.split(' ')[1];if (token) {try {const decoded = jwt.verify(token, 'your_secret_key');ctx.state.user = decoded;await next();} catch (error) {ctx.status = 401;ctx.body = { success: false, message: '无效的token' };}} else {ctx.status = 401;ctx.body = { success: false, message: '未提供token' };}
};module.exports = authMiddleware;
8. routes/index.js
- 首页路由
const Router = require('koa-router');
const router = new Router();router.get('/', (ctx) => {ctx.body = '这是首页';
});module.exports = router;
9. routes/user.js
- 用户管理路由
const Router = require('koa-router');
const userController = require('../controllers/userController.js');
const authMiddleware = require('../middlewares/authMiddleware.js');
const router = new Router({ prefix: '/users' });router.get('/', authMiddleware, userController.getUsers);
router.get('/:id', authMiddleware, userController.getUserById);
router.put('/:id', authMiddleware, userController.updateUser);
router.delete('/:id', authMiddleware, userController.deleteUser);module.exports = router;
10. app.js
- 应用入口文件
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('cors');
const indexRouter = require('./routes/index.js');
const userRouter = require('./routes/user.js');
const authController = require('./controllers/authController.js');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();// 使用cors中间件,允许跨域请求
app.use(cors());
// 应用中间件
app.use(bodyParser());// 登录路由
router.post('/login', authController.login);// 挂载首页路由
app.use(indexRouter.routes()).use(indexRouter.allowedMethods());
// 挂载用户管理路由
app.use(userRouter.routes()).use(userRouter.allowedMethods());
// 挂载总路由
app.use(router.routes()).use(router.allowedMethods());app.listen(3000, () => {console.log('后端服务器启动,监听3000端口');
});
前端实现(Vue 3
部分)
1. 创建Vue 3
项目
使用@vue/cli
创建项目(如果没有安装,先执行npm install -g @vue/cli
):
vue create frontend
在创建项目过程中选择Vue 3
相关选项。
2. 目录结构(主要部分)
frontend
|-- src
| |-- components
| | |-- Login.vue
| | |-- Home.vue
| | |-- Detail.vue
| | |-- UserManagement.vue
| |-- router
| | |-- index.js
| |-- store
| | |-- index.js
| |-- main.js
3. src/main.js
- 入口文件
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
import store from './store/index';createApp(App).use(router).use(store).mount('#app');
4. src/router/index.js
- 路由配置
import { createRouter, createWebHistory } from 'vue-router';
import Login from '../components/Login.vue';
import Home from '../components/Home.vue';
import Detail from '../components/Detail.vue';
import UserManagement from '../components/UserManagement.vue';const routes = [{ path: '/', redirect: '/login' },{ path: '/login', component: Login },{ path: '/home', component: Home },{ path: '/detail', component: Detail },{ path: '/user-management', component: UserManagement }
];const router = createRouter({history: createWebHistory(),routes
});export default router;
vue___322">5. src/components/Login.vue
- 登录组件
<template><div><input v-model="username" placeholder="用户名" /><input type="password" v-model="password" placeholder="密码" /><button @click="login">登录</button></div>
</template><script>
import axios from 'axios';export default {data() {return {username: '',password: ''};},methods: {async login() {try {const response = await axios.post('http://localhost:3000/login', {username: this.username,password: this.password});if (response.data.success) {localStorage.setItem('token', response.data.token);this.$router.push('/home');}} catch (error) {console.error('登录失败', error);}}}
};
</script>
vue___363">6. src/components/Home.vue
- 首页组件
<template><div>这是首页</div>
</template><script>
export default {// 这里可以添加需要的逻辑,比如获取用户信息(如果有需要)
};
</script>
vue___376">7. src/components/Detail.vue
- 详情页组件
<template><div>这是详情页</div>
</template><script>
export default {// 这里可以添加详情页相关逻辑
};
</script>
vue___389">8. src/components/UserManagement.vue
- 用户管理组件
<template><div><button v-if="user.role === 'admin'" @click="getUsers">获取用户列表</button><!-- 这里可以添加更多用户管理相关的UI和操作 --></div>
</template><script>
import axios from 'axios';export default {data() {return {user: {},users: []};},created() {const token = localStorage.getItem('token');if (token) {axios.get('http://localhost:3000/users', {headers: {Authorization: 'Bearer'+ token}}).then(response => {this.user = response.data;}).catch(error => {console.error('获取用户信息失败', error);});}},methods: {async getUsers() {const token = localStorage.getItem('token');try {const response = axios.get('http://localhost:3000/users', {headers: {Authorization: 'Bearer'+ token}});this.users = (await response).data;} catch (error) {console.error('获取用户列表失败', error);}}}
};
</script>
9. src/store/index.js
- Vuex
存储(示例,可根据需要扩展)
import { createStore } from 'vuex';const store = createStore({state() {return {user: null};},mutations: {setUser(state, user) {state.user = user;}},actions: {// 可以添加获取用户信息等相关的异步操作}
});export default store;
注意事项
- 在实际应用中,需要对密码进行更安全的处理,比如使用更强的
bcrypt
哈希算法参数。 - 对于
token
的存储和验证机制,可以进一步优化,比如添加token
刷新逻辑。 - 前端的
axios
请求可以添加更多的错误处理和加载状态提示。 - 权限控制在前端可以根据后端返回的用户角色和菜单权限更精细地控制组件的显示和隐藏。
小结
以下是上述代码实例直接应用时需要注意的一些总结与建议:
一、安全相关
- 密码加密强度
- 注意事项:在后端的
authController.js
中使用bcryptjs
进行密码加密时,应合理设置加密成本因子(cost
参数)来增强密码安全性。默认的cost
值可能不足以抵御强力的暴力破解攻击。 - 实例剖析:例如,当前代码可能像这样使用
bcrypt
比较密码(简化示例):
但没有设置const isMatch = await bcrypt.compare(password, user.password);
cost
参数,实际应用中可在密码创建或更新时明确设置合适的强度,像这样:const saltRounds = 12; // 合适的成本因子,可根据服务器性能等调整 const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
- 注意事项:在后端的
- Token 管理
- 注意事项:
token
的生成与验证使用固定的secret_key
,且缺少刷新机制,同时存储和传输过程也需更安全的处理。 - 实例剖析:
- 在
authController.js
中生成token
时:
这里的const token = jwt.sign({ id: user.id, username: user.username,role: user.role }, 'your_secret_key', { expiresIn: '1h' });
your_secret_key
应配置为环境变量,避免硬编码在代码中导致泄露风险,比如在服务器启动时通过环境变量读取:const secretKey = process.env.JWT_SECRET_KEY; const token = jwt.sign({... }, secretKey, { expiresIn: '1h' });
- 关于
token
刷新,当token
快过期时,前端没有机制向后台请求新的token
来无缝续期,用户体验不佳,需要实现类似的逻辑,例如设置定时器,在token
有效期的一定时间(如还剩 10 分钟过期时)向专门的token
刷新接口发起请求获取新token
。
- 在
- 注意事项:
- 数据库连接安全
- 注意事项:数据库连接配置中用户名、密码等信息硬编码不安全,并且缺少完善的错误处理和连接池管理。
- 实例剖析:在
config/db.js
中:
应改为通过环境变量来设置这些敏感信息,如:const pool = mysql.createPool({host: 'localhost',user: 'root',password: 'your_password',database: 'your_database_name',//... 其他配置 });
同时,对于数据库操作可能出现的错误(如连接失败、查询超时等),目前只是简单返回结果,没有针对性的错误处理,例如在const pool = mysql.createPool({host: process.env.DB_HOST,user: process.env.DB_USER,password: process.env.DB_PASSWORD,database: process.env.DB_NAME,//... 其他配置 });
models/user.js
中查询用户时,如果数据库连接出现问题,应进行更好的错误捕获和处理,像这样:static async findByUsername(username) {try {const [rows] = await pool.query('SELECT * FROM users WHERE username =?', [username]);return rows[0];} catch (error) {console.error('数据库查询用户出错:', error);throw new Error('数据库查询用户时遇到问题,请稍后再试');} }
二、错误处理
- 统一错误处理机制
- 注意事项:后端各个功能模块中的错误处理较分散、简单,缺乏统一规范,不利于排查和定位问题。
- 实例剖析:比如在
userController.js
中不同操作(如获取用户、更新用户等)都各自返回简单的错误信息,像更新用户操作:
可以创建一个统一的错误处理中间件,将各种错误进行分类,返回更规范、详细的错误响应给前端,例如创建const updateUser = async (ctx) => {const id = ctx.params.id;const updatedUser = ctx.request.body;const result = await User.update(id, updatedUser);if (result) {ctx.body = { message: '用户信息更新成功' };} else {ctx.status = 404;ctx.body = { message: '用户不存在,无法更新' };} };
errorMiddleware.js
:
然后在module.exports = async (ctx, next) => {try {await next();} catch (error) {ctx.status = error.statusCode || 500;ctx.body = {success: false,message: error.message || '服务器内部错误'};} };
app.js
中应用这个中间件,使整个应用能统一处理错误情况。
三、代码结构与可维护性
- 分层与模块化
- 注意事项:随着业务扩展,目前代码的分层不够清晰,业务逻辑分散在控制器和模型中,不利于维护和扩展,可添加服务层来封装业务逻辑。
- 实例剖析:例如用户注册功能,如果后续需要添加更多复杂逻辑(如验证手机号格式、发送注册短信验证码等),目前这些逻辑可能会散落在控制器里的注册方法中,更好的做法是创建
userService.js
服务层文件,在里面封装注册相关的完整逻辑,像这样:
然后在const User = require('./models/user.js'); const bcrypt = require('bcryptjs'); const register = async (userData) => {// 验证手机号等额外逻辑(此处省略具体实现)const hashedPassword = await bcrypt.hash(userData.password, 10);const newUser = {username: userData.username,password: hashedPassword,role: userData.role,menus: userData.menus};return User.create(newUser); }; module.exports = {register };
authController.js
中调用这个服务层方法来进行注册操作,使得业务逻辑更加清晰,便于后续维护和扩展。 - 数据库操作可移植性
- 注意事项:直接写
SQL
语句进行数据库操作,后期若更换数据库(如从MySQL
换为PostgreSQL
),改动成本大,可考虑引入ORM
框架。 - 实例剖析:当前在
models/user.js
中有很多原生SQL
查询语句,如:
若使用static async findByUsername(username) {const [rows] = await pool.query('SELECT * FROM users WHERE username =?', [username]);return rows[0]; }
Sequelize
(一种常见ORM
框架),代码可以改为:
这样后续切换数据库时,只需调整const { Model, DataTypes } = require('sequelize'); const sequelize = require('../config/database');class User extends Model {}User.init({id: {type: DataTypes.INTEGER,autoIncrement: true,primaryKey: true},username: {type: DataTypes.STRING,allowNull: false,unique: true},password: {type: DataTypes.STRING,allowNull: false},role: {type: DataTypes.STRING,allowNull: false},menus: {type: DataTypes.TEXT},created_at: {type: DataTypes.DATE,defaultValue: DataTypes.NOW},updated_at: {type: DataTypes.DATE,defaultValue: DataTypes.NOW,onUpdate: DataTypes.NOW} }, {sequelize,modelName: 'User' });module.exports = User;
Sequelize
的配置,而不用大量修改具体的数据库操作语句。 - 注意事项:直接写
四、权限控制
- 细粒度权限
- 注意事项:当前基于用户角色和菜单的权限控制较粗,实际可能需要更细粒度,比如对具体功能按钮等操作进行权限管控。
- 实例剖析:在前端
UserManagement.vue
组件中,目前只是简单根据角色判断是否显示获取用户列表按钮:
但可能对于“删除用户”这个操作,不仅要判断角色是<button v-if="user.role === 'admin'" @click="getUsers">获取用户列表</button>
admin
,还需要后端进一步返回针对每个用户是否有删除权限的具体标识,前端根据这个标识来决定按钮是否可用,后端则在对应的删除用户接口处详细校验这个权限,比如在userController.js
的deleteUser
方法中添加更细的权限判断逻辑:const deleteUser = async (ctx) => {const id = ctx.params.id;const user = ctx.state.user;if (user.role === 'admin' && user.permissions.includes('delete_user')) {const result = await User.delete(id);if (result) {ctx.body = { message: '用户删除成功' };} else {ctx.status = 404;ctx.body = { message: '用户不存在,无法删除' };}} else {ctx.status = 403;ctx.body = { message: '您没有权限执行此操作' };} };
- 权限数据校验
- 注意事项:后端没有对前端传递的权限相关数据严格校验,存在前端篡改数据越权访问风险。
- 实例剖析:当前前端获取用户信息后可以拿到权限相关数据(如角色、菜单权限等),若前端恶意篡改这些数据再发起请求,后端没有有效校验机制就可能导致越权访问。比如在后端的
authMiddleware.js
中验证token
后获取用户信息时,可以增加对权限数据格式、范围等的校验逻辑,确保其合法性,像这样:
const authMiddleware = async (ctx, next) => {const token = ctx.headers.authorization && ctx.headers.authorization.split(' ')[1];if (token) {try {const decoded = jwt.verify(token,'secret_key');// 校验权限相关字段是否符合预期格式和范围if (typeof decoded.role ==='string' && Array.isArray(decoded.menus)) {ctx.state.user = decoded;await next();} else {ctx.status = 401;ctx.body = { success: false, message: '权限数据格式错误' };}} catch (error) {ctx.status = 401;ctx.body = { success: false, message: '无效的token' };}} else {ctx.status = 401;ctx.body = { success: false, message: '未提供token' };} };
五、前端相关
-
Token 存储安全
- 注意事项:前端将
token
存储在localStorage
容易遭受XSS
攻击,应采用更安全的存储方式。 - 实例剖析:在
Login.vue
组件中登录成功后这样存储token
:
localStorage.setItem('token', response.data.token);
可以考虑使用
cookie
并设置httpOnly
属性来存储token
,在后端设置cookie
相关属性(例如通过koa
的中间件来设置),让前端无法通过脚本访问到token
,增强安全性。 - 注意事项:前端将
-
用户输入验证
- 注意事项:前端对用户输入验证简单,容易被利用发起恶意攻击,应加强输入框的验证。
- 实例剖析:在
Login.vue
组件中,用户名和密码输入框没有限制输入内容,像这样:
<input v-model="username" placeholder="用户名" /> <input type="password" v-model="password" placeholder="密码" />
可以添加
v-validate
(使用vee-validate
等验证库)等方式来限制输入长度、格式等,例如:<input v-model="username" placeholder="用户名" v-validate="'required|min:3|max:20'" /> <input type="password" v-model="password" placeholder="密码" v-validate="'required|min:6|max:20'" />
并根据验证结果给出相应提示,防止不合理的输入传递到后端。
-
接口请求优化与统一处理
- 注意事项:前端使用
axios
进行接口请求时,缺少统一的拦截处理,导致代码冗余且不利于维护,同时存在重复请求等性能问题。 - 实例剖析:在多个组件(如
UserManagement.vue
)中都有发起获取用户相关信息的请求,每次都要写类似这样的代码:
const token = localStorage.getItem('token'); try {const response = axios.get('http://localhost:3000/users', {headers: {Authorization: 'Bearer'+ token}});this.users = (await response).data; } catch (error) {console.error('获取用户列表失败', error); }
可以创建
axios
实例并设置统一的请求拦截器(添加请求头、处理加载状态等)和响应拦截器(处理错误、统一解析数据等),例如在src/api/index.js
创建:import axios from 'axios';const instance = axios.create({baseURL: 'http://localhost:3000' });instance.interceptors.request.use((config) => {const token = localStorage.getItem('token');if (token) {config.headers.Authorization = `Bearer ${token}`;}return config; }, (error) => {return Promise.reject(error); });instance.interceptors.response.use((response) => {return response.data; }, (error) => {console.error('接口请求出错:', error);return Promise.reject(error); });export default instance;
然后在组件中直接使用这个
axios
实例进行请求,减少重复代码,也方便对请求进行统一管理和优化。 - 注意事项:前端使用
总之,上述代码示例要直接应用到实际生产环境,需要从安全、错误处理、代码结构、权限控制以及前端相关等多方面进行完善和优化,以保障应用的稳定、安全和可维护性。
补充(前端将 token 存储在 localStorage 容易遭受 XSS 攻击,应采用更安全的存储方式,具体如下:)
- HttpOnly Cookie
- 原理:
HttpOnly
是一个设置在Cookie
上的属性,当一个Cookie
被标记为HttpOnly
时,通过浏览器端的脚本(如JavaScript)将无法访问该Cookie
。这样可以有效防止XSS
攻击获取存储在Cookie
中的敏感信息,如token
。 - 后端设置示例(以
koa
为例):
- 原理:
const Koa = require('koa');
const app = new Koa();
const cookie = require('koa-cookie');// 应用中间件
app.use(cookie());// 假设在登录成功后设置token到Cookie
const login = async (ctx) => {//... 登录验证逻辑const token = jwt.sign({... },'secret_key', { expiresIn: '1h' });ctx.cookies.set('token', token, {httpOnly: true,// 还可以设置其他属性,如maxAge(过期时间,单位为毫秒)、path(Cookie生效的路径)等maxAge: 3600 * 1000, path: '/'});ctx.body = { success: true };
};
- 前端注意事项:由于
HttpOnly
的特性,前端无法通过document.cookie
等方式获取token
,因此在发送需要认证的请求时,浏览器会自动将带有HttpOnly
属性的Cookie
包含在请求头中(前提是请求的域名、路径等符合Cookie
设置的要求)。但要注意,这种方式在跨域场景下可能需要额外配置(如设置withCredentials
属性为true
),同时要确保后端正确处理跨域的Cookie
。
- Encrypted LocalStorage
- 原理:在将
token
存储到localStorage
之前,使用加密算法对其进行加密。这样即使攻击者能够访问localStorage
,获取到的也是加密后的内容,无法直接使用。在需要使用token
时,再通过解密算法还原。 - 示例代码(使用
crypto-js
库进行加密和解密): - 首先安装
crypto - js
:
- 原理:在将
npm install crypto-js
- 存储
token
(在登录成功后,假设在一个Vue
组件中):
import CryptoJS from 'crypto-js';
const SECRET_KEY = 'your_secret_key'; // 用于加密和解密的密钥,要妥善保管const storeToken = (token) => {const encryptedToken = CryptoJS.AES.encrypt(token, SECRET_KEY).toString();localStorage.setItem('encrypted_token', encryptedToken);
};
- 获取和使用
token
(在需要发送认证请求的组件方法中):
const getToken = () => {const encryptedToken = localStorage.getItem('encrypted_token');if (encryptedToken) {const bytes = CryptoJS.AES.decrypt(encryptedToken, SECRET_KEY);const originalToken = bytes.toString(CryptoJS.enc.Utf8);return originalToken;}return null;
};
- 注意事项:加密密钥的安全性至关重要,不能在代码中硬编码,应该像存储
token
的secret_key
一样,通过环境变量等方式配置。同时,加密和解密操作可能会对性能产生一定的影响,尤其是在频繁存储和获取token
的场景下。
- Memory Storage(In - Memory Session)
- 原理:将
token
存储在内存中,例如在Vue
应用中,可以使用一个全局的变量(如Vuex
状态管理中的状态)或者一个单例对象来存储token
。这种方式下,token
不会持久化存储在本地存储介质中,只要页面关闭或者应用退出,token
就会消失,因此可以避免一些存储层面的安全风险。 - 示例(使用
Vuex
存储): - 在
src/store/index.js
中:
- 原理:将
import { createStore } from 'vuex';const store = createStore({state() {return {token: null};},mutations: {setToken(state, token) {state.token = token;}},actions: {// 例如在登录成功后设置tokenloginSuccess({ commit }, token) {commit('setToken', token);}}
});export default store;
- 在登录组件(如
Login.vue
)中:
import { mapActions } from 'vuex';
export default {methods: {...mapActions(['loginSuccess']),async login() {try {const response = await axios.post('http://localhost:3000/login', {//... 登录参数});if (response.data.success) {this.loginSuccess(response.data.token);//... 其他逻辑,如路由跳转}} catch (error) {console.error('登录失败', error);}}}
};
- 注意事项:这种方式的缺点是,如果用户刷新页面或者意外关闭标签页后重新打开,用户可能需要重新登录获取
token
,会影响用户体验。为了缓解这个问题,可以结合其他存储方式(如前面提到的加密localStorage
)来在一定程度上恢复用户状态,但这也增加了复杂性和潜在的安全风险。同时,在多标签页或者多窗口场景下,内存中的存储方式需要考虑如何在不同标签页之间同步token
状态等问题。