next-auth使用指南与原理

NextAuth.js 超详细使用指南与原理

目录

  1. [NextAuth.js 简介](#NextAuth.js 简介 "#nextauthjs-%E7%AE%80%E4%BB%8B")
  2. 核心架构原理
  3. 工作流程详解
  4. 配置详解
  5. 安全机制
  6. 高级特性
  7. [Node.js 原生实现](#Node.js 原生实现 "#nodejs-%E5%8E%9F%E7%94%9F%E5%AE%9E%E7%8E%B0")

NextAuth.js 简介

什么是 NextAuth.js

NextAuth.js 是一个专为 Next.js 应用设计的开源认证解决方案,支持多种认证方式:

  • Credentials认证(用户名/密码)
  • OAuth 2.0(Google、GitHub、Facebook等)
  • Magic Links(无密码邮箱登录)
  • 企业级SSO(Azure AD、Auth0等)

核心优势

  • 零配置 - 默认配置即可使用
  • 安全性 - 内置CSRF、XSS防护
  • 灵活性 - 支持自定义认证逻辑
  • 多框架 - 支持Next.js、Express等
  • TypeScript - 完整的类型支持

核心架构原理

1. 整体架构图

scss 复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Client        │    │   Next.js API   │    │   Database      │
│   (Browser)     │───▶│   Routes        │───▶│   (Sessions)    │
│                 │    │   /api/auth/*   │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │              ┌─────────────────┐              │
         └─────────────▶│   JWT Tokens    │◀─────────────┘
                        │   & Cookies     │
                        └─────────────────┘

2. 核心组件

2.1 Session管理
typescript 复制代码
// 核心接口定义
interface Session {
  user: {
    id: string
    email: string
    name?: string
    image?: string
  }
  expires: string
  accessToken?: string
}

interface JWT {
  token: {
    sub: string          // 用户ID
    email: string        // 邮箱
    name?: string        // 姓名
    picture?: string     // 头像
    iat: number          // 签发时间
    exp: number          // 过期时间
  }
}
2.2 Cookie安全机制
typescript 复制代码
// 默认Cookie配置
const defaultCookies = {
  sessionToken: {
    name: `next-auth.session-token`,
    options: {
      httpOnly: true,
      sameSite: 'lax',
      path: '/',
      secure: true  // HTTPS only
    }
  },
  callbackUrl: {
    name: `next-auth.callback-url`,
    options: {
      sameSite: 'lax',
      path: '/',
      secure: true
    }
  },
  csrfToken: {
    name: `next-auth.csrf-token`,
    options: {
      httpOnly: true,
      sameSite: 'lax',
      path: '/',
      secure: true
    }
  }
}

工作流程详解

1. 登录流程(Credentials)

1.1 时序图
sequenceDiagram participant U as 用户 participant C as 客户端 participant A as /api/auth/signin participant P as Provider participant D as 数据库 participant S as SessionStore U->>C: 输入邮箱密码 C->>A: POST /api/auth/callback/credentials A->>P: 调用authorize函数 P->>D: 验证用户凭据 D-->>P: 返回用户信息 P-->>A: 返回用户对象 A->>S: 创建会话 S-->>A: 返回session token A-->>C: 设置cookie C-->>U: 登录成功
1.2 详细步骤

步骤1:用户提交表单

typescript 复制代码
// 客户端代码
import { signIn } from 'next-auth/react'

const handleSubmit = async (e: FormEvent) => {
  e.preventDefault()
  
  const result = await signIn('credentials', {
    email: 'user@example.com',
    password: 'password123',
    redirect: false, // 手动处理重定向
  })
  
  if (result?.error) {
    // 处理错误
    console.error('登录失败:', result.error)
  } else {
    // 登录成功
    router.push('/dashboard')
  }
}

步骤2:服务端验证

typescript 复制代码
// /api/auth/[...nextauth].ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        // 1. 验证输入
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Missing credentials')
        }
        
        // 2. 查询用户
        const user = await User.findOne({ email: credentials.email })
        if (!user) {
          throw new Error('User not found')
        }
        
        // 3. 验证密码
        const isValid = await bcrypt.compare(credentials.password, user.password)
        if (!isValid) {
          throw new Error('Invalid password')
        }
        
        // 4. 返回用户对象
        return {
          id: user._id.toString(),
          email: user.email,
          name: user.name,
          image: user.avatar
        }
      }
    })
  ]
})

2. Session验证流程

2.1 JWT验证
typescript 复制代码
// JWT验证过程
async function verifyJWT(token: string) {
  try {
    // 1. 解码JWT
    const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!)
    
    // 2. 验证过期时间
    if (decoded.exp && Date.now() >= decoded.exp * 1000) {
      throw new Error('Token expired')
    }
    
    // 3. 验证签名
    const signature = crypto
      .createHmac('sha256', process.env.NEXTAUTH_SECRET!)
      .update(JSON.stringify(decoded))
      .digest('hex')
    
    return decoded
  } catch (error) {
    throw new Error('Invalid token')
  }
}

配置详解

1. 基础配置

typescript 复制代码
// next-auth.config.ts
import { NextAuthOptions } from 'next-auth'

export const authOptions: NextAuthOptions = {
  // 1. 认证提供商
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: "邮箱", type: "email" },
        password: { label: "密码", type: "password" }
      },
      async authorize(credentials) {
        // 认证逻辑
        return user || null
      }
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
    })
  ],

  // 2. 会话配置
  session: {
    strategy: 'jwt', // 或 'database'
    maxAge: 30 * 24 * 60 * 60, // 30天
    updateAge: 24 * 60 * 60, // 24小时更新一次
  },

  // 3. JWT配置
  jwt: {
    maxAge: 30 * 24 * 60 * 60, // 30天
  },

  // 4. 页面路由
  pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error',
    verifyRequest: '/auth/verify-request',
    newUser: '/auth/new-user'
  },

  // 5. 回调函数
  callbacks: {
    async jwt({ token, user, account }) {
      // JWT创建/更新时调用
      if (user) {
        token.id = user.id
        token.email = user.email
      }
      return token
    },

    async session({ session, token }) {
      // 会话创建时调用
      if (token) {
        session.user.id = token.id
        session.user.email = token.email
      }
      return session
    },

    async signIn({ user, account, profile }) {
      // 登录前验证
      return true // 允许登录
    },

    async redirect({ url, baseUrl }) {
      // 重定向控制
      return url.startsWith(baseUrl) ? url : baseUrl
    }
  },

  // 6. 事件监听
  events: {
    async signIn({ user, account, profile }) {
      // 登录成功事件
      console.log('User signed in:', user.email)
    },
    async signOut({ token }) {
      // 登出事件
      console.log('User signed out')
    }
  }
}

2. 数据库适配器

typescript 复制代码
// MongoDB适配器
import { MongoDBAdapter } from '@next-auth/mongodb-adapter'
import clientPromise from '@/lib/mongodb'

export const authOptions: NextAuthOptions = {
  adapter: MongoDBAdapter(clientPromise),
  providers: [
    // ... providers
  ],
  session: {
    strategy: 'database' // 使用数据库存储会话
  }
}

安全机制

1. CSRF保护

typescript 复制代码
// CSRF Token生成
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex')
}

// CSRF验证中间件
async function csrfProtection(req: NextApiRequest, res: NextApiResponse) {
  const csrfToken = req.cookies['next-auth.csrf-token']
  const csrfHeader = req.headers['x-csrf-token']
  
  if (csrfToken !== csrfHeader) {
    return res.status(403).json({ error: 'CSRF token mismatch' })
  }
}

2. 密码安全

typescript 复制代码
// 密码强度验证
const passwordSchema = z.object({
  password: z.string()
    .min(8, '密码至少8位')
    .regex(/[A-Z]/, '至少一个大写字母')
    .regex(/[a-z]/, '至少一个小写字母')
    .regex(/[0-9]/, '至少一个数字')
    .regex(/[^A-Za-z0-9]/, '至少一个特殊字符')
})

// 密码哈希
async function hashPassword(password: string): Promise<string> {
  const saltRounds = 12
  return await bcrypt.hash(password, saltRounds)
}

// 密码验证
async function verifyPassword(
  plainPassword: string, 
  hashedPassword: string
): Promise<boolean> {
  return await bcrypt.compare(plainPassword, hashedPassword)
}

3. 会话安全

typescript 复制代码
// 会话配置
const sessionConfig = {
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    httpOnly: true, // 防止XSS
    sameSite: 'lax', // CSRF保护
    maxAge: 30 * 24 * 60 * 60, // 30天
    path: '/',
  },
  rolling: true, // 每次请求更新过期时间
  genid: () => crypto.randomUUID(), // 随机会话ID
}

高级特性

1. 角色权限管理

typescript 复制代码
// 角色定义
enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
  MODERATOR = 'moderator'
}

// 权限检查
function hasPermission(userRole: UserRole, requiredRole: UserRole): boolean {
  const roleHierarchy = {
    [UserRole.USER]: 1,
    [UserRole.MODERATOR]: 2,
    [UserRole.ADMIN]: 3
  }
  
  return roleHierarchy[userRole] >= roleHierarchy[requiredRole]
}

// JWT扩展
async function jwt({ token, user }) {
  if (user) {
    token.role = user.role
    token.permissions = user.permissions
  }
  return token
}

async function session({ session, token }) {
  session.user.role = token.role
  session.user.permissions = token.permissions
  return session
}

2. 多因素认证

typescript 复制代码
// 2FA配置
async function enableTwoFactor(userId: string, secret: string) {
  const qrCode = speakeasy.totp({
    secret: secret,
    encoding: 'base32',
    label: `MyApp (${user.email})`,
    issuer: 'MyApp'
  })
  
  await User.updateOne(
    { _id: userId }, 
    { twoFactorSecret: secret, twoFactorEnabled: true }
  )
  
  return qrCode
}

// 2FA验证
async function verifyTwoFactorToken(userId: string, token: string) {
  const user = await User.findById(userId)
  
  return speakeasy.totp.verify({
    secret: user.twoFactorSecret,
    encoding: 'base32',
    token: token,
    window: 2 // 允许30秒时间差
  })
}

3. 会话管理

typescript 复制代码
// 获取所有活跃会话
async function getActiveSessions(userId: string) {
  return await Session.find({ userId })
    .sort({ createdAt: -1 })
    .limit(10)
}

// 撤销会话
async function revokeSession(sessionId: string) {
  await Session.deleteOne({ _id: sessionId })
}

// 撤销所有会话
async function revokeAllSessions(userId: string) {
  await Session.deleteMany({ userId })
}

Node.js 原生实现

下面我们用纯Node.js实现一个简化版的NextAuth认证系统:

1. 项目结构

bash 复制代码
simple-auth/
├── src/
│   ├── auth/
│   │   ├── index.js          # 主认证模块
│   │   ├── providers.js      # 认证提供商
│   │   ├── session.js        # 会话管理
│   │   ├── crypto.js         # 加密工具
│   │   └── middleware.js     # 中间件
│   ├── routes/
│   │   ├── auth.js           # 认证路由
│   │   └── protected.js      # 受保护路由
│   ├── models/
│   │   └── user.js           # 用户模型
│   └── server.js             # 主服务器
├── .env
└── package.json

2. 核心实现

2.1 加密工具 (src/auth/crypto.js)
javascript 复制代码
const crypto = require('crypto');
const bcrypt = require('bcrypt');

class CryptoUtils {
  // 生成随机字符串
  static generateRandomString(length = 32) {
    return crypto.randomBytes(length).toString('hex');
  }

  // 哈希密码
  static async hashPassword(password) {
    const saltRounds = 12;
    return await bcrypt.hash(password, saltRounds);
  }

  // 验证密码
  static async verifyPassword(plainPassword, hashedPassword) {
    return await bcrypt.compare(plainPassword, hashedPassword);
  }

  // 生成JWT
  static generateJWT(payload, secret, expiresIn = '7d') {
    const header = {
      alg: 'HS256',
      typ: 'JWT'
    };

    const now = Math.floor(Date.now() / 1000);
    const exp = now + (expiresIn === '7d' ? 7 * 24 * 60 * 60 : expiresIn);

    const jwtPayload = {
      ...payload,
      iat: now,
      exp: exp
    };

    const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
    const encodedPayload = Buffer.from(JSON.stringify(jwtPayload)).toString('base64url');
    
    const signature = crypto
      .createHmac('sha256', secret)
      .update(`${encodedHeader}.${encodedPayload}`)
      .digest('base64url');

    return `${encodedHeader}.${encodedPayload}.${signature}`;
  }

  // 验证JWT
  static verifyJWT(token, secret) {
    try {
      const [header, payload, signature] = token.split('.');
      
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(`${header}.${payload}`)
        .digest('base64url');

      if (signature !== expectedSignature) {
        throw new Error('Invalid signature');
      }

      const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());
      
      if (decoded.exp && Date.now() >= decoded.exp * 1000) {
        throw new Error('Token expired');
      }

      return decoded;
    } catch (error) {
      throw new Error('Invalid token');
    }
  }

  // 生成CSRF令牌
  static generateCSRFToken() {
    return this.generateRandomString(32);
  }

  // 生成会话ID
  static generateSessionId() {
    return `sess_${this.generateRandomString(24)}`;
  }
}

module.exports = CryptoUtils;
2.2 会话管理 (src/auth/session.js)
javascript 复制代码
const CryptoUtils = require('./crypto');

class SessionManager {
  constructor(options = {}) {
    this.sessions = new Map(); // 内存存储(生产环境用Redis)
    this.secret = options.secret || process.env.AUTH_SECRET || 'fallback-secret';
    this.maxAge = options.maxAge || 7 * 24 * 60 * 60 * 1000; // 7天
  }

  // 创建会话
  async createSession(userId, userData) {
    const sessionId = CryptoUtils.generateSessionId();
    const token = CryptoUtils.generateJWT(
      { 
        sub: userId, 
        email: userData.email, 
        name: userData.name 
      },
      this.secret,
      Math.floor(this.maxAge / 1000)
    );

    const session = {
      id: sessionId,
      userId,
      token,
      userData,
      createdAt: new Date(),
      expiresAt: new Date(Date.now() + this.maxAge),
      lastAccessed: new Date()
    };

    this.sessions.set(sessionId, session);
    
    return {
      sessionId,
      token,
      expires: session.expiresAt
    };
  }

  // 验证会话
  async validateSession(sessionId) {
    const session = this.sessions.get(sessionId);
    
    if (!session) {
      return null;
    }

    if (session.expiresAt < new Date()) {
      this.sessions.delete(sessionId);
      return null;
    }

    // 更新最后访问时间
    session.lastAccessed = new Date();
    
    try {
      const decoded = CryptoUtils.verifyJWT(session.token, this.secret);
      return {
        sessionId,
        userId: decoded.sub,
        email: decoded.email,
        name: decoded.name,
        expires: session.expiresAt
      };
    } catch (error) {
      this.sessions.delete(sessionId);
      return null;
    }
  }

  // 销毁会话
  async destroySession(sessionId) {
    return this.sessions.delete(sessionId);
  }

  // 获取用户所有会话
  async getUserSessions(userId) {
    const userSessions = [];
    for (const [id, session] of this.sessions) {
      if (session.userId === userId) {
        userSessions.push({
          id,
          createdAt: session.createdAt,
          expiresAt: session.expiresAt,
          lastAccessed: session.lastAccessed
        });
      }
    }
    return userSessions;
  }

  // 清理过期会话
  async cleanupExpiredSessions() {
    const now = new Date();
    let cleanedCount = 0;
    
    for (const [id, session] of this.sessions) {
      if (session.expiresAt < now) {
        this.sessions.delete(id);
        cleanedCount++;
      }
    }
    
    return cleanedCount;
  }

  // 获取会话统计
  async getSessionStats() {
    const now = new Date();
    let active = 0;
    let expired = 0;
    
    for (const session of this.sessions.values()) {
      if (session.expiresAt > now) {
        active++;
      } else {
        expired++;
      }
    }
    
    return {
      total: this.sessions.size,
      active,
      expired
    };
  }
}

module.exports = SessionManager;
2.3 用户模型 (src/models/user.js)
javascript 复制代码
const fs = require('fs');
const path = require('path');
const CryptoUtils = require('../auth/crypto');

class UserModel {
  constructor() {
    this.dataFile = path.join(__dirname, '../data/users.json');
    this.ensureDataFile();
    this.users = this.loadUsers();
  }

  ensureDataFile() {
    const dir = path.dirname(this.dataFile);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    if (!fs.existsSync(this.dataFile)) {
      fs.writeFileSync(this.dataFile, JSON.stringify([]));
    }
  }

  loadUsers() {
    try {
      const data = fs.readFileSync(this.dataFile, 'utf8');
      return JSON.parse(data);
    } catch (error) {
      return [];
    }
  }

  saveUsers() {
    fs.writeFileSync(this.dataFile, JSON.stringify(this.users, null, 2));
  }

  async createUser(userData) {
    const existingUser = this.users.find(u => u.email === userData.email);
    if (existingUser) {
      throw new Error('Email already exists');
    }

    const user = {
      id: CryptoUtils.generateRandomString(16),
      email: userData.email,
      name: userData.name,
      password: await CryptoUtils.hashPassword(userData.password),
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    this.users.push(user);
    this.saveUsers();

    // 返回不包含密码的用户对象
    const { password, ...userWithoutPassword } = user;
    return userWithoutPassword;
  }

  async findByEmail(email) {
    const user = this.users.find(u => u.email === email);
    if (!user) return null;
    
    const { password, ...userWithoutPassword } = user;
    return {
      ...userWithoutPassword,
      password // 仅在需要时包含密码
    };
  }

  async findById(id) {
    const user = this.users.find(u => u.id === id);
    if (!user) return null;
    
    const { password, ...userWithoutPassword } = user;
    return userWithoutPassword;
  }

  async validateCredentials(email, password) {
    const user = await this.findByEmail(email);
    if (!user || !user.password) {
      return null;
    }

    const isValid = await CryptoUtils.verifyPassword(password, user.password);
    if (!isValid) {
      return null;
    }

    const { password: _, ...userWithoutPassword } = user;
    return userWithoutPassword;
  }

  async updateUser(id, updates) {
    const userIndex = this.users.findIndex(u => u.id === id);
    if (userIndex === -1) {
      throw new Error('User not found');
    }

    if (updates.password) {
      updates.password = await CryptoUtils.hashPassword(updates.password);
    }

    this.users[userIndex] = {
      ...this.users[userIndex],
      ...updates,
      updatedAt: new Date().toISOString()
    };

    this.saveUsers();

    const { password, ...userWithoutPassword } = this.users[userIndex];
    return userWithoutPassword;
  }

  getAllUsers() {
    return this.users.map(({ password, ...user }) => user);
  }
}

module.exports = UserModel;
2.4 认证中间件 (src/auth/middleware.js)
javascript 复制代码
const SessionManager = require('./session');

class AuthMiddleware {
  constructor(sessionManager) {
    this.sessionManager = sessionManager;
  }

  // 认证中间件
  requireAuth() {
    return async (req, res, next) => {
      try {
        const token = this.extractToken(req);
        
        if (!token) {
          return res.status(401).json({
            error: 'No token provided',
            code: 'NO_TOKEN'
          });
        }

        const session = await this.sessionManager.validateSession(token);
        
        if (!session) {
          return res.status(401).json({
            error: 'Invalid or expired token',
            code: 'INVALID_TOKEN'
          });
        }

        req.user = session;
        next();
      } catch (error) {
        res.status(401).json({
          error: 'Authentication failed',
          code: 'AUTH_FAILED'
        });
      }
    };
  }

  // 可选认证
  optionalAuth() {
    return async (req, res, next) => {
      try {
        const token = this.extractToken(req);
        
        if (token) {
          const session = await this.sessionManager.validateSession(token);
          if (session) {
            req.user = session;
          }
        }
        
        next();
      } catch (error) {
        next();
      }
    };
  }

  // 角色权限检查
  requireRole(allowedRoles) {
    return (req, res, next) => {
      if (!req.user) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      const userRole = req.user.role || 'user';
      const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
      
      if (!roles.includes(userRole)) {
        return res.status(403).json({
          error: 'Insufficient permissions',
          code: 'FORBIDDEN'
        });
      }

      next();
    };
  }

  // 提取token
  extractToken(req) {
    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      return authHeader.substring(7);
    }

    return req.cookies?.session || req.query?.token;
  }

  // CSRF保护
  csrfProtection() {
    return (req, res, next) => {
      if (req.method === 'GET' || req.method === 'HEAD') {
        return next();
      }

      const csrfToken = req.headers['x-csrf-token'];
      const sessionToken = req.cookies['csrf-token'];

      if (!csrfToken || csrfToken !== sessionToken) {
        return res.status(403).json({
          error: 'CSRF token validation failed',
          code: 'CSRF_FAILED'
        });
      }

      next();
    };
  }
}

module.exports = AuthMiddleware;
2.5 认证路由 (src/routes/auth.js)
javascript 复制代码
const express = require('express');
const UserModel = require('../models/user');
const SessionManager = require('../auth/session');
const CryptoUtils = require('../auth/crypto');

function createAuthRouter(sessionManager) {
  const router = express.Router();
  const userModel = new UserModel();

  // 注册
  router.post('/register', async (req, res) => {
    try {
      const { email, password, name } = req.body;

      // 验证输入
      if (!email || !password || !name) {
        return res.status(400).json({
          error: 'Email, password, and name are required'
        });
      }

      if (password.length < 6) {
        return res.status(400).json({
          error: 'Password must be at least 6 characters'
        });
      }

      // 创建用户
      const user = await userModel.createUser({ email, password, name });
      
      // 创建会话
      const session = await sessionManager.createSession(user.id, user);

      res.status(201).json({
        message: 'User registered successfully',
        user,
        session
      });
    } catch (error) {
      res.status(400).json({
        error: error.message || 'Registration failed'
      });
    }
  });

  // 登录
  router.post('/login', async (req, res) => {
    try {
      const { email, password } = req.body;

      if (!email || !password) {
        return res.status(400).json({
          error: 'Email and password are required'
        });
      }

      // 验证凭据
      const user = await userModel.validateCredentials(email, password);
      if (!user) {
        return res.status(401).json({
          error: 'Invalid email or password'
        });
      }

      // 创建会话
      const session = await sessionManager.createSession(user.id, user);

      // 设置cookie
      res.cookie('session', session.sessionId, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
      });

      res.json({
        message: 'Login successful',
        user,
        session
      });
    } catch (error) {
      res.status(500).json({
        error: 'Login failed'
      });
    }
  });

  // 登出
  router.post('/logout', async (req, res) => {
    try {
      const token = req.cookies?.session || req.headers.authorization?.substring(7);
      
      if (token) {
        await sessionManager.destroySession(token);
      }

      res.clearCookie('session');
      res.json({ message: 'Logout successful' });
    } catch (error) {
      res.status(500).json({
        error: 'Logout failed'
      });
    }
  });

  // 验证会话
  router.get('/me', async (req, res) => {
    try {
      const token = req.cookies?.session || req.headers.authorization?.substring(7);
      
      if (!token) {
        return res.status(401).json({
          error: 'No session token provided'
        });
      }

      const session = await sessionManager.validateSession(token);
      
      if (!session) {
        return res.status(401).json({
          error: 'Invalid or expired session'
        });
      }

      res.json({
        user: session,
        expires: session.expires
      });
    } catch (error) {
      res.status(500).json({
        error: 'Session validation failed'
      });
    }
  });

  // 会话统计
  router.get('/stats', async (req, res) => {
    try {
      const stats = await sessionManager.getSessionStats();
      res.json(stats);
    } catch (error) {
      res.status(500).json({
        error: 'Failed to get session stats'
      });
    }
  });

  return router;
}

module.exports = createAuthRouter;
2.6 主服务器 (src/server.js)
javascript 复制代码
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

const SessionManager = require('./auth/session');
const AuthMiddleware = require('./auth/middleware');
const createAuthRouter = require('./routes/auth');

class SimpleAuthServer {
  constructor(options = {}) {
    this.app = express();
    this.port = options.port || 3000;
    this.sessionManager = new SessionManager(options);
    this.authMiddleware = new AuthMiddleware(this.sessionManager);
    
    this.setupMiddleware();
    this.setupRoutes();
    this.setupErrorHandling();
  }

  setupMiddleware() {
    // 安全中间件
    this.app.use(helmet());
    this.app.use(cors({
      origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
      credentials: true
    }));
    
    // 解析中间件
    this.app.use(express.json({ limit: '10mb' }));
    this.app.use(express.urlencoded({ extended: true }));
    this.app.use(cookieParser());
    this.app.use(morgan('combined'));
  }

  setupRoutes() {
    // 健康检查
    this.app.get('/health', (req, res) => {
      res.json({ 
        status: 'ok', 
        timestamp: new Date().toISOString(),
        uptime: process.uptime()
      });
    });

    // 认证路由
    const authRouter = createAuthRouter(this.sessionManager);
    this.app.use('/api/auth', authRouter);

    // 受保护的路由示例
    this.app.get('/api/protected', 
      this.authMiddleware.requireAuth(),
      (req, res) => {
        res.json({
          message: 'This is a protected route',
          user: req.user
        });
      }
    );

    // 可选认证路由
    this.app.get('/api/profile',
      this.authMiddleware.optionalAuth(),
      (req, res) => {
        if (req.user) {
          res.json({
            message: 'Welcome back!',
            user: req.user
          });
        } else {
          res.json({
            message: 'Welcome! Please log in to see personalized content.'
          });
        }
      }
    );

    // 管理员路由
    this.app.get('/api/admin',
      this.authMiddleware.requireAuth(),
      this.authMiddleware.requireRole(['admin']),
      (req, res) => {
        res.json({
          message: 'Admin dashboard',
          user: req.user
        });
      }
    );
  }

  setupErrorHandling() {
    // 404处理
    this.app.use((req, res) => {
      res.status(404).json({
        error: 'Route not found',
        path: req.path
      });
    });

    // 全局错误处理
    this.app.use((error, req, res, next) => {
      console.error('Error:', error);
      
      res.status(error.status || 500).json({
        error: error.message || 'Internal server error',
        code: error.code || 'INTERNAL_ERROR'
      });
    });
  }

  // 会话清理任务
  startCleanupTask() {
    setInterval(async () => {
      try {
        const cleaned = await this.sessionManager.cleanupExpiredSessions();
        if (cleaned > 0) {
          console.log(`Cleaned up ${cleaned} expired sessions`);
        }
      } catch (error) {
        console.error('Session cleanup error:', error);
      }
    }, 5 * 60 * 1000); // 每5分钟清理一次
  }

  async start() {
    this.startCleanupTask();
    
    this.app.listen(this.port, () => {
      console.log(`🚀 SimpleAuth server running on port ${this.port}`);
      console.log(`📊 Health check: http://localhost:${this.port}/health`);
      console.log(`🔐 Auth endpoints: http://localhost:${this.port}/api/auth`);
    });
  }

  async stop() {
    console.log('🛑 Shutting down server...');
    process.exit(0);
  }
}

// 启动服务器
if (require.main === module) {
  require('dotenv').config();
  
  const server = new SimpleAuthServer({
    port: process.env.PORT || 3000,
    secret: process.env.AUTH_SECRET || 'your-secret-key-change-this'
  });

  server.start();

  // 优雅关闭
  process.on('SIGTERM', () => server.stop());
  process.on('SIGINT', () => server.stop());
}

module.exports = SimpleAuthServer;

3. 使用示例

3.1 启动服务器
bash 复制代码
# 安装依赖
npm install express cookie-parser cors helmet morgan bcrypt dotenv

# 创建数据目录
mkdir -p src/data

# 启动服务器
node src/server.js
3.2 测试API
bash 复制代码
# 注册用户
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123","name":"Test User"}'

# 用户登录
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123"}' \
  -c cookies.txt

# 访问受保护路由
curl http://localhost:3000/api/protected \
  -b cookies.txt

# 获取用户信息
curl http://localhost:3000/api/auth/me \
  -b cookies.txt

# 用户登出
curl -X POST http://localhost:3000/api/auth/logout \
  -b cookies.txt
3.3 Node.js客户端使用
javascript 复制代码
const axios = require('axios');

class SimpleAuthClient {
  constructor(baseURL = 'http://localhost:3000') {
    this.client = axios.create({
      baseURL,
      withCredentials: true
    });
  }

  async register(userData) {
    const response = await this.client.post('/api/auth/register', userData);
    return response.data;
  }

  async login(credentials) {
    const response = await this.client.post('/api/auth/login', credentials);
    return response.data;
  }

  async getProfile() {
    const response = await this.client.get('/api/auth/me');
    return response.data;
  }

  async logout() {
    const response = await this.client.post('/api/auth/logout');
    return response.data;
  }
}

// 使用示例
async function example() {
  const client = new SimpleAuthClient();
  
  try {
    // 注册用户
    const user = await client.register({
      email: 'demo@example.com',
      password: 'demo123',
      name: 'Demo User'
    });
    console.log('Registered:', user);
    
    // 用户登录
    const session = await client.login({
      email: 'demo@example.com',
      password: 'demo123'
    });
    console.log('Logged in:', session);
    
    // 获取用户信息
    const profile = await client.getProfile();
    console.log('Profile:', profile);
    
  } catch (error) {
    console.error('Error:', error.response?.data || error.message);
  }
}

// example();

4. 高级特性

4.1 刷新令牌
javascript 复制代码
// 添加刷新令牌支持
class RefreshTokenManager {
  constructor(sessionManager) {
    this.sessionManager = sessionManager;
    this.refreshTokens = new Map();
  }

  async createRefreshToken(userId) {
    const refreshToken = CryptoUtils.generateRandomString(40);
    const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30天
    
    this.refreshTokens.set(refreshToken, {
      userId,
      expiresAt,
      createdAt: new Date()
    });
    
    return refreshToken;
  }

  async refreshAccessToken(refreshToken) {
    const tokenData = this.refreshTokens.get(refreshToken);
    
    if (!tokenData || tokenData.expiresAt < new Date()) {
      throw new Error('Invalid or expired refresh token');
    }
    
    // 创建新的访问令牌
    return await this.sessionManager.createSession(tokenData.userId, {});
  }
}
4.2 OAuth集成
javascript 复制代码
// Google OAuth示例
class GoogleOAuthProvider {
  constructor(clientId, clientSecret) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.redirectUri = 'http://localhost:3000/api/auth/callback/google';
  }

  async getAuthorizationUrl() {
    const params = new URLSearchParams({
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      response_type: 'code',
      scope: 'openid email profile',
      state: CryptoUtils.generateCSRFToken()
    });
    
    return `https://accounts.google.com/oauth2/auth?${params.toString()}`;
  }

  async exchangeCodeForToken(code) {
    const response = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        client_id: this.clientId,
        client_secret: this.clientSecret,
        code,
        redirect_uri: this.redirectUri,
        grant_type: 'authorization_code'
      })
    });
    
    const data = await response.json();
    return data.access_token;
  }
}

总结

通过本指南,我们深入了解了:

  1. NextAuth.js核心原理:JWT、Session管理、安全机制
  2. 完整工作流程:从用户登录到会话验证的每一步
  3. 安全配置:CSRF保护、密码加密、会话安全
  4. Node.js原生实现:不依赖任何框架的认证系统

这个Node.js实现包含了:

  • ✅ 用户注册/登录
  • ✅ JWT令牌管理
  • ✅ 会话存储和验证
  • ✅ 密码安全加密
  • ✅ 中间件保护
  • ✅ RESTful API设计
  • ✅ 错误处理
  • ✅ 会话清理
  • ✅ 统计监控
相关推荐
paopaokaka_luck6 分钟前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
Flobby52941 分钟前
Go语言新手村:轻松理解变量、常量和枚举用法
开发语言·后端·golang
Warren982 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
伍哥的传说2 小时前
Radash.js 现代化JavaScript实用工具库详解 – 轻量级Lodash替代方案
开发语言·javascript·ecmascript·tree-shaking·radash.js·debounce·throttle
程序视点3 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
xidianhuihui3 小时前
go install报错: should be v0 or v1, not v2问题解决
开发语言·后端·golang
前端程序媛-Tian3 小时前
【dropdown组件填坑指南】—怎么实现下拉框的位置计算
前端·javascript·vue
iamlujingtao3 小时前
js多边形算法:获取多边形中心点,且必定在多边形内部
javascript·算法
嘉琪0013 小时前
实现视频实时马赛克
linux·前端·javascript
烛阴4 小时前
Smoothstep
前端·webgl