海信聚好学手机版
8.81M · 2025-10-27
在前后端项目里,登录注册和权限鉴权基本算是“老大难”问题。做得不严谨,系统就会有漏洞;做得太复杂,又会让开发效率大打折扣。 这篇文章,我结合 Vue3.5 和 Node.js + Express,带大家梳理一下完整的登录注册鉴权流程。
先交代一下这次用到的工具和框架:
可以看到,这套组合既保证了开发效率,又覆盖了常见的安全环节。
简单说,鉴权系统就是解决一个核心问题:“如何证明用户身份?”。 所以整套流程其实可以分为三步:
用户之后的每一次请求,都要带上这个 JWT。后端拿到之后做几件事:
通过了,才放行。
几个关键点值得一提:
要让鉴权体系跑起来,第一步当然是数据库建模。这里我用 Prisma 来管理数据结构,主要优势是:类型安全、自动生成客户端操作方法,还能避免写生涩的 SQL。
在我们的场景里,最核心的数据模型就是 用户表。它必须存储用户名、邮箱、加密密码,还有用户角色。
prisma/schema.prisma)// 数据源配置(开发环境使用SQLite,生产可切换为MySQL/PostgreSQL)
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// 生成客户端代码
generator client {
provider = "prisma-client-js"
}
// 用户模型(核心数据结构)
model User {
id Int @id @default(autoincrement()) // 自增ID
username String @unique // 用户名唯一
email String @unique // 邮箱唯一
password String // 存储加密后的密码
role Role @default(USER) // 角色,默认为普通用户
createdAt DateTime @default(now()) // 创建时间
updatedAt DateTime @updatedAt // 更新时间
}
// 角色枚举(权限控制基础)
enum Role {
USER
ADMIN
}
定义模型后,通过命令生成类型安全的客户端代码:
npx prisma migrate dev --name init # 创建数据库迁移并生成表结构
npx prisma generate # 生成Prisma客户端
设计思路:
username和email唯一,避免重复注册role字段实现基础权限区分,为后续扩展预留空间createdAt和updatedAt,便于数据追踪注册是用户与系统建立信任的第一步,需确保数据合法性和密码安全性。
用户提交基本信息(用户名、邮箱、密码),经过前后端双重验证后,密码加密存储到数据库。
// 1. 注册表单逻辑(Vue3.5 Composition API)
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import api from '@/utils/api'
const register = async (userData) => {
// 基础验证(前端体验优化)
if (!userData.username || userData.username.length < 3) {
throw new Error('用户名至少3个字符')
}
const emailReg = /^[^s@]+@[^s@]+.[^s@]+$/
if (!emailReg.test(userData.email)) {
throw new Error('请输入有效邮箱')
}
// withCredentials: true 确保跨域时Cookie能正常传递
const response = await api.post('/auth/register', userData, {
withCredentials: true,
})
return response.data
}
const { PrismaClient } = require('@prisma/client')
const bcrypt = require('bcrypt')
const prisma = new PrismaClient()
// 注册处理(验证并创建用户)
const register = async (req, res) => {
const { username, email, password } = req.body
// 1. 验证用户名是否已存在
const existingUser = await prisma.user.findUnique({
where: { username },
})
if (existingUser) {
return res.status(409).json({
error: { field: 'username', message: '用户名已被占用' },
})
}
// 2. 验证邮箱是否已注册
const existingEmail = await prisma.user.findUnique({
where: { email },
})
if (existingEmail) {
return res.status(409).json({
error: { field: 'email', message: '邮箱已被注册' },
})
}
// 3. 密码加密(10轮盐值,平衡安全性和性能)
const hashedPassword = await bcrypt.hash(password, 10)
// 4. 创建用户(Prisma类型安全的数据库操作)
const newUser = await prisma.user.create({
data: { username, email, password: hashedPassword },
select: { id: true, username: true, email: true, role: true }, // 只返回非敏感信息
})
res.status(201).json({ message: '注册成功', user: newUser })
}
设计思路详解:
登录是验证用户身份并发放信任凭证(JWT)的核心环节,需兼顾安全性和用户体验。
将用户提交的"用户名+密码"转化为服务器可识别的加密令牌(JWT),并通过 HttpOnly Cookie 安全存储。
// 1. 登录请求(仅负责发送凭证)
const login = async (username, password) => {
// withCredentials: true 是跨域携带Cookie的关键
const response = await axios.post(
'/api/auth/login',
{ username, password },
{ withCredentials: true }
)
// 仅存储用户基本信息,不处理令牌(令牌在Cookie中)
return { user: response.data.user, isLogin: true }
}
// 2. 状态管理(Pinia)
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
isLogin: false,
}),
actions: {
setUser(userInfo) {
this.user = userInfo
this.isLogin = true
},
clearUser() {
this.user = null
this.isLogin = false
},
},
})
const jwt = require('jsonwebtoken')
// 1. 登录处理(生成并存储令牌)
const login = async (req, res) => {
const { username, password } = req.body
// 验证用户凭据
const user = await prisma.user.findUnique({
where: { username },
})
// 统一错误信息,避免信息泄露
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: '认证失败,请检查用户名或密码' })
}
// 生成JWT令牌(仅包含必要信息)
const token = jwt.sign(
{ userId: user.id, role: user.role }, // payload
process.env.JWT_SECRET, // 密钥
{ expiresIn: '24h' } // 有效期
)
// 存储令牌到HttpOnly Cookie(核心安全措施)
res.cookie('token', token, {
httpOnly: true, // 禁止JS访问,防XSS
secure: process.env.NODE_ENV === 'production', // 生产环境强制HTTPS
sameSite: 'lax', // 防CSRF
maxAge: 24 * 60 * 60 * 1000, // 24小时有效期
})
// 返回用户信息(不含敏感数据)
res.json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
})
}
设计思路详解:
所有受保护的请求自动携带 Cookie 中的令牌,服务器通过中间件验证令牌有效性和权限。
所有受保护的请求自动携带 Cookie 中的令牌,服务器通过中间件验证令牌有效性和权限。
// 1. 请求配置(自动携带令牌)
const api = axios.create({
baseURL: '/api',
withCredentials: true, // 自动携带Cookie,无需手动添加Token
})
// 2. 响应拦截器(处理令牌过期)
api.interceptors.response.use(
(response) => response,
(err) => {
if (err.response?.status === 401) {
const userStore = useUserStore()
userStore.clearUser() // 清理前端状态
router.push(`/login?redirect=${router.currentRoute.value.fullPath}`)
}
return Promise.reject(err)
}
)
// 3. 路由守卫(前端权限控制)
router.beforeEach(async (to, from, next) => {
if (!to.meta.requiresAuth) return next()
try {
const { data } = await api.get('/auth/status')
const userStore = useUserStore()
userStore.setUser(data.user)
// 验证角色权限
if (to.meta.requiredRole && data.user.role !== to.meta.requiredRole) {
return next('/403')
}
next()
} catch (err) {
next(`/login?redirect=${to.fullPath}`)
}
})
// 1. 认证中间件(验证令牌)
const authMiddleware = async (req, res, next) => {
// 从Cookie提取令牌(核心提取逻辑)
const token = req.cookies.token
if (!token) return res.status(401).json({ message: '未登录' })
try {
// 验证令牌有效性
const decoded = jwt.verify(token, process.env.JWT_SECRET)
// 验证用户存在性(防止已注销用户访问)
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, username: true, role: true, email: true },
})
if (!user) return res.status(401).json({ message: '用户不存在' })
// 将用户信息注入请求对象
req.user = user
next() // 验证通过,继续处理请求
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: '登录已过期' })
}
return res.status(401).json({ message: '令牌无效' })
}
}
// 2. 权限中间件(验证角色)
const checkRole = (requiredRole) => (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: '未登录' })
}
if (req.user.role !== requiredRole) {
return res.status(403).json({ message: '无权限访问' })
}
next()
}
// 3. 路由使用示例
router.get('/profile', authMiddleware, getUserProfile)
router.get('/admin', authMiddleware, checkRole('ADMIN'), getAdminData)
// 登录状态接口(供前端路由守卫验证)
router.get('/auth/status', authMiddleware, (req, res) => {
res.json({ isLogin: true, user: req.user })
})
设计思路详解:
通过清除 Cookie 和前端状态,确保过期/注销的令牌无法继续使用。
通过清除 Cookie 和前端状态,确保过期/注销的令牌无法继续使用。
// 登出功能
const logout = async () => {
try {
await api.post('/auth/logout') // 调用后端登出接口
} catch (err) {
console.error('登出接口失败:', err)
// 即使接口失败,仍清理前端状态(保障用户体验)
} finally {
const userStore = useUserStore()
userStore.clearUser() // 清理前端状态
router.push('/login') // 跳转登录页
}
}
// 登出接口(清除令牌)
const logout = (req, res) => {
// 清除HttpOnly Cookie(必须与设置时配置一致)
res.clearCookie('token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
})
res.json({ message: '登出成功' })
}
设计思路详解:
app.js)const express = require('express')
const cors = require('cors')
const cookieParser = require('cookie-parser')
const authRoutes = require('./routes/authRoutes')
const app = express()
// 中间件配置
app.use(express.json())
app.use(cookieParser())
app.use(
cors({
origin: process.env.CLIENT_URL || 'http://localhost:5173',
credentials: true, // 允许跨域携带Cookie
})
)
// 路由挂载
app.use('/api/auth', authRoutes)
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`)
})
.env)DATABASE_URL="file:./dev.db"
JWT_SECRET="your-super-secret-jwt-key"
CLIENT_URL="http://localhost:5173"
NODE_ENV="development"
多模态Ai项目全流程开发中,从需求分析,到Ui设计,程序开发,部署上线,感兴趣扫描(带项目功能演示)
本文基于 Vue3.5 + Node.js Express + Prisma 实现了完整的登录注册鉴权流程,核心围绕"信任生命周期"设计,通过 JWT 令牌和 HttpOnly Cookie 实现安全的身份验证。
这套极简方案的优势在于:
实际项目中,可根据需求进一步扩展,如添加验证码、密码重置、多因素认证等功能,但其核心鉴权流程可复用本文设计,是现代 Web 应用鉴权的理想选择。