Vue 3 全栈教程:224.认证与授权机制
概述
认证与授权是Web应用安全的核心组成部分。认证(Authentication)验证用户的身份,确保用户是其所声称的人;授权(Authorization)决定用户可以访问哪些资源和执行哪些操作。本集将深入探讨Vue 3应用中的认证与授权机制,包括常见的认证方式、授权模型以及具体的实现方案。
认证机制
1. 常见的认证方式
用户名密码认证
这是最传统的认证方式,用户通过输入用户名和密码来验证身份。
优点:简单易实现,用户熟悉。
缺点:安全性较低,容易受到密码泄露、暴力破解等攻击。
Token认证
Token认证是一种无状态的认证方式,服务器生成一个Token并发送给客户端,客户端在后续请求中携带该Token来验证身份。
优点:
- 无状态,服务器不需要存储会话信息
- 支持跨域请求
- 可以在不同设备间共享
- 便于实现单点登录(SSO)
缺点:
- Token一旦泄露,攻击者可以使用该Token访问资源
- Token过期处理需要额外的机制
常见的Token认证方案:
- JWT (JSON Web Token)
- OAuth 2.0
- OpenID Connect
生物认证
生物认证使用用户的生物特征(如指纹、面部识别、虹膜扫描等)来验证身份。
优点:安全性高,用户体验好。
缺点:需要硬件支持,实现复杂。
多因素认证(MFA)
多因素认证结合多种认证方式,如密码+短信验证码、密码+指纹识别等,提高认证的安全性。
优点:安全性高,即使一种认证方式被破解,还有其他方式保护。
缺点:用户体验稍差,增加了认证步骤。
2. JWT认证详解
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT由三部分组成:Header、Payload和Signature。
JWT的结构
xxxxx.yyyyy.zzzzz- Header:包含令牌类型和加密算法
- Payload:包含声明(claims),如用户ID、角色、过期时间等
- Signature:使用Header中指定的算法对Header和Payload进行签名,确保数据完整性
JWT的工作流程
- 用户输入用户名和密码进行登录
- 服务器验证用户名和密码
- 服务器生成JWT并发送给客户端
- 客户端存储JWT(通常存储在localStorage或Cookie中)
- 客户端在后续请求中携带JWT(通常在Authorization头中)
- 服务器验证JWT的有效性
- 服务器处理请求并返回响应
Vue 3中使用JWT认证
<template>
<div>
<h2>JWT认证示例</h2>
<div v-if="!isAuthenticated">
<h3>登录</h3>
<form @submit.prevent="login">
<div>
<label>用户名:</label>
<input v-model="username" type="text">
</div>
<div>
<label>密码:</label>
<input v-model="password" type="password">
</div>
<button type="submit">登录</button>
</form>
</div>
<div v-else>
<h3>欢迎,{{ userInfo.username }}!</h3>
<p>角色:{{ userInfo.role }}</p>
<button @click="logout">退出登录</button>
<button @click="fetchProtectedData">获取受保护数据</button>
<div v-if="protectedData">
<h4>受保护数据:</h4>
<pre>{{ protectedData }}</pre>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import axios from 'axios'
const username = ref('admin')
const password = ref('password123')
const protectedData = ref(null)
// 从localStorage获取token
const getToken = () => {
return localStorage.getItem('token')
}
// 保存token到localStorage
const saveToken = (token) => {
localStorage.setItem('token', token)
}
// 删除token
const removeToken = () => {
localStorage.removeItem('token')
}
// 解析JWT获取用户信息
const parseToken = (token) => {
try {
const payload = token.split('.')[1]
return JSON.parse(atob(payload))
} catch (error) {
return null
}
}
// 检查是否已认证
const isAuthenticated = computed(() => {
const token = getToken()
if (!token) return false
const userInfo = parseToken(token)
if (!userInfo || !userInfo.exp) return false
// 检查token是否过期
const now = Date.now() / 1000
return userInfo.exp > now
})
// 获取用户信息
const userInfo = computed(() => {
const token = getToken()
if (!token) return {}
return parseToken(token)
})
// 登录
const login = async () => {
try {
const response = await axios.post('/api/login', {
username: username.value,
password: password.value
})
// 保存token
saveToken(response.data.token)
} catch (error) {
console.error('登录失败:', error)
alert('登录失败,请检查用户名和密码')
}
}
// 退出登录
const logout = () => {
removeToken()
protectedData.value = null
}
// 获取受保护数据
const fetchProtectedData = async () => {
try {
const token = getToken()
const response = await axios.get('/api/protected', {
headers: {
Authorization: `Bearer ${token}`
}
})
protectedData.value = response.data
} catch (error) {
console.error('获取受保护数据失败:', error)
alert('获取受保护数据失败,请检查权限')
}
}
</script>后端实现(Express):
const express = require('express')
const jwt = require('jsonwebtoken')
const app = express()
app.use(express.json())
// 密钥,实际应用中应该从环境变量中获取
const secretKey = 'secret-key-123456'
// 模拟用户数据
const users = [
{ id: 1, username: 'admin', password: 'password123', role: 'admin' },
{ id: 2, username: 'user', password: 'password123', role: 'user' }
]
// 登录API
app.post('/api/login', (req, res) => {
const { username, password } = req.body
// 查找用户
const user = users.find(u => u.username === username && u.password === password)
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' })
}
// 生成JWT
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
secretKey,
{ expiresIn: '1h' } // Token有效期为1小时
)
res.json({ token })
})
// 验证JWT的中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (!token) {
return res.status(401).json({ error: '未提供Token' })
}
jwt.verify(token, secretKey, (err, user) => {
if (err) {
return res.status(403).json({ error: '无效的Token' })
}
req.user = user
next()
})
}
// 受保护的API
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: '这是受保护的数据',
user: req.user
})
})
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000')
})3. OAuth 2.0认证
OAuth 2.0是一种授权框架,允许第三方应用在用户授权的情况下访问用户资源。
OAuth 2.0的角色
- 资源所有者:用户
- 客户端:第三方应用
- 授权服务器:验证用户身份并颁发授权令牌
- 资源服务器:存储用户资源的服务器
OAuth 2.0的授权流程
- 客户端请求用户授权
- 用户同意授权
- 客户端获取授权码
- 客户端使用授权码向授权服务器请求令牌
- 授权服务器颁发令牌
- 客户端使用令牌向资源服务器请求资源
- 资源服务器验证令牌并返回资源
Vue 3中使用OAuth 2.0认证
以GitHub OAuth为例:
<template>
<div>
<h2>GitHub OAuth 2.0认证示例</h2>
<div v-if="!isAuthenticated">
<button @click="loginWithGitHub">使用GitHub登录</button>
</div>
<div v-else>
<h3>欢迎,{{ userInfo.name }}!</h3>
<img :src="userInfo.avatar_url" alt="头像" style="width: 100px; border-radius: 50%">
<p>GitHub用户名:{{ userInfo.login }}</p>
<button @click="logout">退出登录</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
const userInfo = ref(null)
// 检查URL中是否有授权码
const checkAuthCode = () => {
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
if (code) {
// 移除URL中的授权码,避免刷新页面时重复处理
window.history.replaceState({}, document.title, window.location.pathname)
// 使用授权码获取access token
getAccessToken(code)
}
}
// 使用授权码获取access token
const getAccessToken = async (code) => {
try {
const response = await axios.post('/api/github/token', {
code
})
const { access_token } = response.data
// 保存access token
localStorage.setItem('github_token', access_token)
// 获取用户信息
await fetchGitHubUserInfo(access_token)
} catch (error) {
console.error('获取access token失败:', error)
}
}
// 获取GitHub用户信息
const fetchGitHubUserInfo = async (token) => {
try {
const response = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `token ${token}`
}
})
userInfo.value = response.data
} catch (error) {
console.error('获取GitHub用户信息失败:', error)
}
}
// 检查是否已认证
const isAuthenticated = computed(() => {
return !!userInfo.value
})
// 使用GitHub登录
const loginWithGitHub = () => {
const clientId = 'your-github-client-id' // 替换为你的GitHub Client ID
const redirectUri = 'http://localhost:5173' // 替换为你的回调URL
const scope = 'user' // 请求的权限范围
// 重定向到GitHub授权页面
window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`
}
// 退出登录
const logout = () => {
localStorage.removeItem('github_token')
userInfo.value = null
}
onMounted(() => {
// 检查URL中是否有授权码
checkAuthCode()
// 如果已有access token,获取用户信息
const token = localStorage.getItem('github_token')
if (token) {
fetchGitHubUserInfo(token)
}
})
</script>授权机制
1. 授权模型
RBAC(基于角色的访问控制)
RBAC(Role-Based Access Control)是一种常见的授权模型,它将权限分配给角色,然后将角色分配给用户。
优点:
- 简化权限管理,便于批量授权
- 职责分明,便于审计
- 易于扩展,适合大型应用
RBAC的核心概念:
- 用户:系统的使用者
- 角色:一组权限的集合
- 权限:对资源的访问或操作权限
- 资源:系统中的各种资源,如页面、API、数据等
ABAC(基于属性的访问控制)
ABAC(Attribute-Based Access Control)是一种更灵活的授权模型,它根据用户属性、资源属性、环境属性等动态决定用户是否有权限访问资源。
优点:
- 灵活性高,支持复杂的授权规则
- 可以根据上下文动态调整权限
- 适合需要细粒度授权的场景
缺点:
- 实现复杂
- 性能开销较大
ACL(访问控制列表)
ACL(Access Control List)是一种简单的授权模型,它为每个资源维护一个访问控制列表,列出哪些用户或角色可以访问该资源。
优点:
- 简单易实现
- 适合小型应用
缺点:
- 难以管理,资源数量多时维护成本高
- 不适合复杂的权限关系
2. Vue 3中实现RBAC授权
1. 角色定义
// src/constants/roles.js
export const ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
}2. 权限定义
// src/constants/permissions.js
export const PERMISSIONS = {
// 用户管理
USER_LIST: 'user:list',
USER_CREATE: 'user:create',
USER_EDIT: 'user:edit',
USER_DELETE: 'user:delete',
// 文章管理
ARTICLE_LIST: 'article:list',
ARTICLE_CREATE: 'article:create',
ARTICLE_EDIT: 'article:edit',
ARTICLE_DELETE: 'article:delete'
}3. 角色权限映射
// src/constants/rolePermissions.js
import { ROLES } from './roles'
import { PERMISSIONS } from './permissions'
export const ROLE_PERMISSIONS = {
[ROLES.ADMIN]: [
PERMISSIONS.USER_LIST,
PERMISSIONS.USER_CREATE,
PERMISSIONS.USER_EDIT,
PERMISSIONS.USER_DELETE,
PERMISSIONS.ARTICLE_LIST,
PERMISSIONS.ARTICLE_CREATE,
PERMISSIONS.ARTICLE_EDIT,
PERMISSIONS.ARTICLE_DELETE
],
[ROLES.USER]: [
PERMISSIONS.ARTICLE_LIST,
PERMISSIONS.ARTICLE_CREATE,
PERMISSIONS.ARTICLE_EDIT
],
[ROLES.GUEST]: [
PERMISSIONS.ARTICLE_LIST
]
}4. 权限检查工具函数
// src/utils/permission.js
import { ROLE_PERMISSIONS } from '../constants/rolePermissions'
export const hasPermission = (role, permission) => {
if (!role || !permission) return false
const permissions = ROLE_PERMISSIONS[role] || []
return permissions.includes(permission)
}
export const hasAnyPermission = (role, permissions) => {
if (!role || !permissions || !permissions.length) return false
const userPermissions = ROLE_PERMISSIONS[role] || []
return permissions.some(permission => userPermissions.includes(permission))
}
export const hasAllPermissions = (role, permissions) => {
if (!role || !permissions || !permissions.length) return false
const userPermissions = ROLE_PERMISSIONS[role] || []
return permissions.every(permission => userPermissions.includes(permission))
}5. 权限指令
// src/directives/permission.js
import { hasPermission } from '../utils/permission'
export const permissionDirective = {
mounted(el, binding, vnode) {
const { value: permission } = binding
const app = vnode.appContext.app
const userRole = app.config.globalProperties.$userRole || 'guest'
if (permission && !hasPermission(userRole, permission)) {
el.style.display = 'none'
}
},
updated(el, binding, vnode) {
const { value: permission } = binding
const app = vnode.appContext.app
const userRole = app.config.globalProperties.$userRole || 'guest'
if (permission && !hasPermission(userRole, permission)) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}6. 路由守卫
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { hasPermission } from '../utils/permission'
import { PERMISSIONS } from '../constants/permissions'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/articles',
name: 'ArticleList',
component: () => import('../views/ArticleList.vue'),
meta: {
permission: PERMISSIONS.ARTICLE_LIST
}
},
{
path: '/articles/create',
name: 'ArticleCreate',
component: () => import('../views/ArticleCreate.vue'),
meta: {
permission: PERMISSIONS.ARTICLE_CREATE
}
},
{
path: '/users',
name: 'UserList',
component: () => import('../views/UserList.vue'),
meta: {
permission: PERMISSIONS.USER_LIST
}
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫,检查权限
router.beforeEach((to, from, next) => {
// 跳过不需要权限的路由
if (!to.meta.permission) {
return next()
}
// 获取用户角色(实际应用中应该从token或store中获取)
const userRole = localStorage.getItem('userRole') || 'guest'
// 检查用户是否有权限访问该路由
if (hasPermission(userRole, to.meta.permission)) {
next()
} else {
// 没有权限,跳转到403页面
next('/403')
}
})
export default router7. 组件中使用权限检查
<template>
<div>
<h2>文章管理</h2>
<!-- 使用v-permission指令控制按钮显示 -->
<button v-permission="PERMISSIONS.ARTICLE_CREATE" @click="createArticle">创建文章</button>
<table>
<thead>
<tr>
<th>标题</th>
<th>作者</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="article in articles" :key="article.id">
<td>{{ article.title }}</td>
<td>{{ article.author }}</td>
<td>
<!-- 使用v-permission指令控制按钮显示 -->
<button v-permission="PERMISSIONS.ARTICLE_EDIT" @click="editArticle(article.id)">编辑</button>
<button v-permission="PERMISSIONS.ARTICLE_DELETE" @click="deleteArticle(article.id)">删除</button>
</td>
</tr>
</tbody>
</table>
<!-- 使用hasPermission函数条件渲染 -->
<div v-if="hasPermission(PERMISSIONS.ARTICLE_LIST)">
<h3>文章统计</h3>
<p>总文章数:{{ articles.length }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { hasPermission } from '../utils/permission'
import { PERMISSIONS } from '../constants/permissions'
const articles = ref([
{ id: 1, title: 'Vue 3 入门教程', author: 'admin' },
{ id: 2, title: 'Vue 3 组合式API详解', author: 'user' }
])
const createArticle = () => {
console.log('创建文章')
}
const editArticle = (id) => {
console.log('编辑文章', id)
}
const deleteArticle = (id) => {
console.log('删除文章', id)
}
</script>认证与授权的最佳实践
- 使用HTTPS:确保所有认证和授权相关的通信都通过HTTPS进行,防止数据被窃取。
- 安全存储令牌:
- 对于Web应用,推荐使用localStorage或sessionStorage存储令牌
- 对于原生应用,推荐使用安全的存储机制,如Keychain(iOS)或Keystore(Android)
- 避免将令牌存储在Cookie中,除非设置了httpOnly和secure属性
- 令牌过期处理:
- 设置合理的令牌过期时间
- 实现令牌刷新机制
- 处理令牌过期的情况,如重定向到登录页
- 最小权限原则:只授予用户完成任务所需的最小权限。
- 定期检查权限:定期检查用户的权限,确保用户只拥有当前所需的权限。
- 日志记录:记录所有认证和授权相关的操作,便于审计和排查问题。
- 防止CSRF攻击:使用CSRF Token或SameSite Cookie防止CSRF攻击。
- 防止XSS攻击:对用户输入进行验证和过滤,防止XSS攻击导致令牌泄露。
- 使用成熟的认证库:如Passport.js、Auth0、Firebase Auth等,避免自行实现复杂的认证逻辑。
- 定期安全审计:定期进行安全审计,检查认证和授权机制是否存在漏洞。
总结
认证与授权是Web应用安全的核心组成部分,Vue 3应用中可以采用多种认证方式和授权模型。本集介绍了常见的认证方式(如JWT、OAuth 2.0)和授权模型(如RBAC、ABAC、ACL),并通过具体的代码示例展示了如何在Vue 3应用中实现认证和授权。
在实际应用中,应根据应用的规模、安全性要求和业务需求选择合适的认证和授权方案,并遵循安全最佳实践,确保应用的安全性。