TOP全栈项目:博客系统的用户认证与权限管理

TOP全栈项目:博客系统的用户认证与权限管理

【免费下载链接】curriculum TheOdinProject/curriculum: The Odin Project 是一个免费的在线编程学习平台,这个仓库是其课程大纲和教材资源库,涵盖了Web开发相关的多种技术栈,如HTML、CSS、JavaScript以及Ruby on Rails等。 【免费下载链接】curriculum 项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

引言:你还在为博客系统的安全漏洞发愁吗?

当你搭建一个博客系统时,是否曾遇到以下问题:匿名用户篡改他人文章、未授权用户访问管理后台、密码明文存储导致数据泄露?据OWASP 2024年报告,身份认证失效和访问控制缺陷仍是Web应用排名前两位的安全风险。本文将基于The Odin Project全栈课程,从零构建一套企业级博客系统的用户认证与权限管理方案,涵盖从密码加密到RBAC权限模型的完整实现。

读完本文你将掌握:

  • 基于Passport.js的用户认证全流程(注册/登录/会话管理)
  • 采用bcrypt的密码安全存储方案
  • 利用Context API实现前端认证状态管理
  • 基于JWT的API访问控制策略
  • 从会员到管理员的多级权限设计
  • 完整的前后端权限中间件实现

技术栈概览

技术领域核心技术应用场景
后端框架Express.js构建RESTful API
认证中间件Passport.js本地策略认证
密码加密bcryptjs安全存储用户密码
状态管理React Context API前端认证状态共享
API保护JWT无状态身份验证
数据库PostgreSQL用户/权限数据存储
ORMPrisma数据库模型定义

用户认证系统设计与实现

认证流程总览

mermaid

1. 数据库模型设计

-- 用户表设计
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) UNIQUE NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  role VARCHAR(20) DEFAULT 'member', -- member/admin
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章表设计(含权限控制字段)
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200) NOT NULL,
  content TEXT NOT NULL,
  author_id INTEGER REFERENCES users(id),
  is_published BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. 后端认证实现(Passport.js)

安装核心依赖
npm install express passport passport-local express-session bcryptjs jsonwebtoken
Passport Local策略配置
// config/passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

passport.use(new LocalStrategy(
  async (username, password, done) => {
    try {
      const user = await prisma.user.findUnique({ where: { username } });
      
      if (!user) {
        return done(null, false, { message: '用户名不存在' });
      }
      
      const isMatch = await bcrypt.compare(password, user.password_hash);
      if (!isMatch) {
        return done(null, false, { message: '密码错误' });
      }
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// 序列化用户(存储到session)
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// 反序列化用户(从session读取)
passport.deserializeUser(async (id, done) => {
  try {
    const user = await prisma.user.findUnique({ where: { id } });
    done(null, user);
  } catch (err) {
    done(err);
  }
});

module.exports = passport;
用户注册与密码加密
// controllers/authController.js
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

exports.signup = async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // 密码强度验证
    if (password.length < 8) {
      return res.status(400).json({ message: '密码至少8个字符' });
    }
    
    // 密码加密(10轮盐值)
    const salt = await bcrypt.genSalt(10);
    const password_hash = await bcrypt.hash(password, salt);
    
    // 创建用户
    const user = await prisma.user.create({
      data: {
        username,
        email,
        password_hash,
        // 默认角色为普通会员
        role: 'member'
      },
      // 不返回密码哈希
      select: {
        id: true,
        username: true,
        email: true,
        role: true,
        created_at: true
      }
    });
    
    res.status(201).json(user);
  } catch (err) {
    if (err.code === 'P2002') {
      return res.status(400).json({ message: '用户名或邮箱已存在' });
    }
    res.status(500).json({ message: '服务器错误' });
  }
};

权限管理系统设计

RBAC权限模型

mermaid

权限级别定义

角色权限描述典型操作
游客(未登录)只读访问公开内容浏览已发布文章
会员(已登录)基本交互权限发表评论、管理个人资料
作者内容创建权限发布/编辑自己的文章
管理员系统管理权限删除任何内容、管理用户

后端权限中间件实现

// middleware/auth.js
const jwt = require('jsonwebtoken');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

// JWT验证中间件
exports.authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: '未提供认证令牌' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ message: '令牌无效或已过期' });
  }
};

// 角色检查中间件
exports.authorizeRoles = (...roles) => {
  return async (req, res, next) => {
    try {
      const user = await prisma.user.findUnique({ 
        where: { id: req.user.id },
        select: { role: true }
      });
      
      if (!user) {
        return res.status(404).json({ message: '用户不存在' });
      }
      
      if (!roles.includes(user.role)) {
        return res.status(403).json({ message: '没有足够的权限' });
      }
      
      next();
    } catch (err) {
      res.status(500).json({ message: '服务器错误' });
    }
  };
};

受保护路由配置

// routes/postRoutes.js
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');
const { authenticateJWT, authorizeRoles } = require('../middleware/auth');

// 公开路由
router.get('/', postController.getAllPublishedPosts);
router.get('/:id', postController.getPostById);

// 受保护路由
router.use(authenticateJWT); // 所有后续路由都需要认证

// 会员权限
router.post('/:id/comments', postController.addComment);

// 作者权限
router.post('/', authorizeRoles('author', 'admin'), postController.createPost);
router.put('/:id', authorizeRoles('author', 'admin'), postController.updatePost);

// 管理员权限
router.delete('/:id', authorizeRoles('admin'), postController.deletePost);
router.put('/:id/publish', authorizeRoles('admin'), postController.togglePublish);

module.exports = router;

前端认证状态管理

Context API实现全局认证状态

// context/AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import api from '../services/api';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 初始化时检查本地存储的令牌
    const loadUserFromToken = async () => {
      const token = localStorage.getItem('token');
      if (token) {
        api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
        try {
          const res = await api.get('/api/auth/me');
          setCurrentUser(res.data);
        } catch (err) {
          localStorage.removeItem('token');
          delete api.defaults.headers.common['Authorization'];
        }
      }
      setLoading(false);
    };

    loadUserFromToken();
  }, []);

  // 登录函数
  const login = async (username, password) => {
    try {
      const res = await api.post('/api/auth/login', { username, password });
      const { token, user } = res.data;
      
      localStorage.setItem('token', token);
      api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      
      setCurrentUser(user);
      return user;
    } catch (err) {
      setError(err.response?.data?.message || '登录失败');
      throw err;
    }
  };

  // 登出函数
  const logout = () => {
    localStorage.removeItem('token');
    delete api.defaults.headers.common['Authorization'];
    setCurrentUser(null);
  };

  return (
    <AuthContext.Provider value={{ 
      currentUser, 
      loading, 
      error, 
      login, 
      logout,
      isAuthenticated: !!currentUser,
      isAdmin: currentUser?.role === 'admin',
      isAuthor: currentUser?.role === 'author' || currentUser?.role === 'admin'
    }}>
      {children}
    </AuthContext.Provider>
  );
};

// 自定义Hook简化使用
export const useAuth = () => useContext(AuthContext);

前端权限控制组件

// components/PrivateRoute.jsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

// 基于角色的路由保护组件
export const PrivateRoute = ({ roles = [] }) => {
  const { currentUser, loading, isAuthenticated } = useAuth();

  if (loading) return <div>加载中...</div>;

  // 未登录用户重定向到登录页
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  // 检查角色权限
  if (roles.length > 0 && !roles.includes(currentUser.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  // 有权限,渲染子路由
  return <Outlet />;
};

// 使用示例:
// <Route element={<PrivateRoute roles={['admin']} />}>
//   <Route path="/admin" element={<AdminDashboard />} />
// </Route>

条件渲染UI元素

// components/PostActions.jsx
import React from 'react';
import { useAuth } from '../context/AuthContext';

const PostActions = ({ post, onEdit, onDelete, onPublish }) => {
  const { currentUser, isAuthor, isAdmin } = useAuth();
  
  // 检查是否是文章作者
  const isOwner = post.authorId === currentUser?.id;
  
  return (
    <div className="post-actions">
      {/* 作者可以编辑自己的文章 */}
      {(isOwner && isAuthor) && (
        <button onClick={onEdit} className="btn btn-edit">编辑</button>
      )}
      
      {/* 管理员可以删除任何文章和切换发布状态 */}
      {isAdmin && (
        <>
          <button onClick={onDelete} className="btn btn-delete">删除</button>
          <button onClick={onPublish} className="btn btn-publish">
            {post.isPublished ? '取消发布' : '发布'}
          </button>
        </>
      )}
    </div>
  );
};

export default PostActions;

安全最佳实践

密码安全

  1. 使用bcrypt进行单向哈希
    • 自动生成随机盐值
    • 计算强度可调(成本因子)
    • 抵抗彩虹表攻击
// 推荐配置
const saltRounds = 12; // 平衡安全性和性能
const hash = await bcrypt.hash(password, saltRounds);
  1. 密码策略 enforcement
    • 最小长度(8字符)
    • 复杂度要求(大小写、数字、特殊字符)
    • 禁止常见密码

会话管理

  1. JWT最佳实践
    • 短期有效期(15-60分钟)
    • 使用刷新令牌机制
    • 存储在HttpOnly Cookie中
// JWT配置示例
const token = jwt.sign(
  { id: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '30m' } // 短期有效
);
  1. 安全退出流程
    • 清除客户端存储的令牌
    • 使服务器端令牌失效
    • 清除相关Cookie

防护措施

  1. CSRF保护

    • 使用SameSite Cookie
    • 实现CSRF令牌验证
  2. 速率限制

    • 登录尝试限制
    • API请求限流
// 使用express-rate-limit
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 5, // 最多5次尝试
  message: '登录尝试次数过多,请15分钟后再试'
});

app.use('/api/auth/login', loginLimiter);

项目实战: 会员专属内容功能

功能需求

实现一个"会员专属"内容区,仅登录会员可见完整内容,游客只能看到预览,管理员可管理所有会员内容。

数据库设计

CREATE TABLE exclusive_content (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200) NOT NULL,
  preview_text TEXT NOT NULL,
  full_text TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

API实现

// controllers/contentController.js
exports.getExclusiveContent = async (req, res) => {
  try {
    const content = await prisma.exclusiveContent.findUnique({ 
      where: { id: parseInt(req.params.id) } 
    });
    
    if (!content) {
      return res.status(404).json({ message: '内容不存在' });
    }
    
    // 根据认证状态返回不同内容
    if (req.user) {
      // 已登录用户返回完整内容
      return res.json(content);
    } else {
      // 游客只返回预览内容
      return res.json({
        id: content.id,
        title: content.title,
        preview_text: content.preview_text,
        is_preview: true
      });
    }
  } catch (err) {
    res.status(500).json({ message: '服务器错误' });
  }
};

前端实现

// components/ExclusiveContent.jsx
import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import api from '../services/api';
import LoginPrompt from './LoginPrompt';

const ExclusiveContent = ({ contentId }) => {
  const [content, setContent] = useState(null);
  const [loading, setLoading] = useState(true);
  const { isAuthenticated } = useAuth();

  useEffect(() => {
    const fetchContent = async () => {
      try {
        const res = await api.get(`/api/exclusive/${contentId}`);
        setContent(res.data);
      } catch (err) {
        console.error('获取内容失败', err);
      } finally {
        setLoading(false);
      }
    };

    fetchContent();
  }, [contentId]);

  if (loading) return <div>加载中...</div>;
  if (!content) return <div>内容不存在</div>;

  return (
    <div className="exclusive-content">

【免费下载链接】curriculum TheOdinProject/curriculum: The Odin Project 是一个免费的在线编程学习平台,这个仓库是其课程大纲和教材资源库,涵盖了Web开发相关的多种技术栈,如HTML、CSS、JavaScript以及Ruby on Rails等。 【免费下载链接】curriculum 项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值