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的工作流程

  1. 用户输入用户名和密码进行登录
  2. 服务器验证用户名和密码
  3. 服务器生成JWT并发送给客户端
  4. 客户端存储JWT(通常存储在localStorage或Cookie中)
  5. 客户端在后续请求中携带JWT(通常在Authorization头中)
  6. 服务器验证JWT的有效性
  7. 服务器处理请求并返回响应

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的授权流程

  1. 客户端请求用户授权
  2. 用户同意授权
  3. 客户端获取授权码
  4. 客户端使用授权码向授权服务器请求令牌
  5. 授权服务器颁发令牌
  6. 客户端使用令牌向资源服务器请求资源
  7. 资源服务器验证令牌并返回资源

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 router

7. 组件中使用权限检查

<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>

认证与授权的最佳实践

  1. 使用HTTPS:确保所有认证和授权相关的通信都通过HTTPS进行,防止数据被窃取。
  2. 安全存储令牌
    • 对于Web应用,推荐使用localStorage或sessionStorage存储令牌
    • 对于原生应用,推荐使用安全的存储机制,如Keychain(iOS)或Keystore(Android)
    • 避免将令牌存储在Cookie中,除非设置了httpOnly和secure属性
  3. 令牌过期处理
    • 设置合理的令牌过期时间
    • 实现令牌刷新机制
    • 处理令牌过期的情况,如重定向到登录页
  4. 最小权限原则:只授予用户完成任务所需的最小权限。
  5. 定期检查权限:定期检查用户的权限,确保用户只拥有当前所需的权限。
  6. 日志记录:记录所有认证和授权相关的操作,便于审计和排查问题。
  7. 防止CSRF攻击:使用CSRF Token或SameSite Cookie防止CSRF攻击。
  8. 防止XSS攻击:对用户输入进行验证和过滤,防止XSS攻击导致令牌泄露。
  9. 使用成熟的认证库:如Passport.js、Auth0、Firebase Auth等,避免自行实现复杂的认证逻辑。
  10. 定期安全审计:定期进行安全审计,检查认证和授权机制是否存在漏洞。

总结

认证与授权是Web应用安全的核心组成部分,Vue 3应用中可以采用多种认证方式和授权模型。本集介绍了常见的认证方式(如JWT、OAuth 2.0)和授权模型(如RBAC、ABAC、ACL),并通过具体的代码示例展示了如何在Vue 3应用中实现认证和授权。

在实际应用中,应根据应用的规模、安全性要求和业务需求选择合适的认证和授权方案,并遵循安全最佳实践,确保应用的安全性。

扩展阅读

  1. JWT官方文档
  2. OAuth 2.0官方文档
  3. OpenID Connect官方文档
  4. RBAC Wikipedia
  5. ABAC Wikipedia
  6. Vue Router官方文档 - 导航守卫
  7. Passport.js官方文档
  8. Auth0官方文档
  9. Firebase Auth官方文档
  10. OWASP认证 cheat sheet
« 上一篇 Vue 3 认证与授权机制:构建安全的用户访问控制 下一篇 » Vue 3 输入验证与过滤:确保数据安全与完整性