Gwork.cc

production 0d22d0
gwork.cc
/NextJs服务端和前端共用认证/

Next.js 混合渲染项目的认证方案实践

背景

最近在做一个 Next.js 项目,用到了 App Router。项目里既有客户端渲染的页面(比如打卡页面需要交互),也有服务端渲染的页面(比如财务统计页面需要 SEO)。

一开始因为是纯前端渲染, 想着用 localStorage 存 token,但这样服务端渲染的页面就无法验证了,后改为 Cookie 验证. 这样客户端和服务端都能读,而且浏览器会自动携带。

核心思路

用户登录
  ↓
设置 HttpOnly Cookie (服务端设置)
  ↓
┌─────────────────┬──────────────────┐
│  客户端页面      │   服务端页面        │
│  ('use client') │  (default)       │
│                 │                   │
│ fetch/axios     │ cookies() API     │
│ 自动携带Cookie   │ 直接读取Cookie     │
│     ↓           │      ↓            │
│ API接受Cookie验证│ requireAuth()验证 │
└─────────────────┴──────────────────┘

1. 登录时设置 Cookie(服务端)

改造登录 API(app/api/auth/login/route.ts):

import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await validateUser(body.username, body.password);

  if (user) {
    const token = generateToken(user);

    // 服务端设置 HttpOnly Cookie
    cookies().set({
      name: 'm_token',
      value: token,
      httpOnly: true, // 防止 JS 读取,更安全
      secure: process.env.NODE_ENV === 'production', // 生产环境强制 HTTPS
      sameSite: 'lax', // CSRF 保护
      maxAge: 60 * 60 * 24 * 7, // 7 天
      path: '/',
    });

    return NextResponse.json({
      ret: 0,
      data: { user },
      message: '登录成功',
    });
  }

  return NextResponse.json(
    {
      ret: 1001,
      message: '用户名或密码错误',
    },
    { status: 401 },
  );
}

关键点

  • httpOnly: true - JS 无法读取,防止 XSS 攻击窃取 token
  • secure: true - 生产环境强制 HTTPS 传输
  • sameSite: 'lax' - 防止 CSRF 攻击
  • 服务端设置,客户端不需要任何操作

2. 客户端登录逻辑简化

客户端只需要调用 API(app/login/page.tsx):

'use client';

const onSubmit = async (data) => {
  // 直接调用 API,Cookie 由服务端设置
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    credentials: 'include', // 重要:允许发送和接收 Cookie
  });

  const result = await response.json();
  if (result.ret === 0) {
    // Cookie 已经自动设置好了
    window.location.href = '/';
  } else {
    toast.error(result.message);
  }
};

关键点

  • credentials: 'include' - 必须加这个,否则浏览器不会发送/接收 Cookie
  • 不需要手动操作 Cookie,浏览器自动处理

3. axios 全局配置

如果用 axios,全局配置一次即可(lib/request.ts):

import axios from 'axios';

const request = axios.create({
  baseURL: '/api',
  timeout: 10000,
  withCredentials: true, // 全局启用 Cookie
});

// 不需要拦截器手动加 Authorization 头了
// Cookie 会自动携带

export default request;

4. 服务端认证封装

和之前一样,封装 requireAuthlib/serverAuth.ts):

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { getUserByToken } from '#/app/api/models/users';

export async function requireAuth() {
  const cookieStore = cookies();
  const token = cookieStore.get('m_token')?.value;

  if (!token) {
    redirect('/login');
  }

  const user = await getUserByToken(token);
  if (!user) {
    redirect('/login');
  }

  return user;
}

使用简单:

// 任何服务端页面
export default async function BalancePage() {
  await requireAuth();

  // 业务逻辑...
}

5. 后端 API 验证 Cookie

API 路由也能直接读 Cookie(app/api/lib/verifyToken.ts):

import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';

export class AuthError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthError';
  }
}

export async function verifyToken(request: NextRequest) {
  const cookieStore = cookies();
  const token = cookieStore.get('m_token')?.value;

  if (!token) {
    throw new AuthError('未登录');
  }

  const user = await getUserByToken(token);
  if (!user) {
    throw new AuthError('Token 无效');
  }

  return user;
}

API 路由使用:

export async function GET(request: NextRequest) {
  try {
    const user = await verifyToken(request); // 从 Cookie 读取

    // 业务逻辑...
    return NextResponse.json({ ret: 0, data: {...} });
  } catch (error) {
    return handleApiError(error, '获取数据');
  }
}

6. 登出逻辑

登出也在服务端处理(app/api/auth/logout/route.ts):

import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function POST() {
  // 删除 Cookie
  cookies().delete('m_token');

  return NextResponse.json({
    ret: 0,
    message: '登出成功',
  });
}

客户端调用:

'use client';

const handleLogout = async () => {
  await fetch('/api/auth/logout', {
    method: 'POST',
    credentials: 'include',
  });
  window.location.href = '/login';
};

踩过的坑总结

  1. 忘记设置 credentials: 'include'
    fetch/axios 请求必须加这个,否则 Cookie 不会发送。

  2. HttpOnly Cookie 在客户端读不到
    这是特性不是 bug!HttpOnly 就是为了防止 JS 读取。如果需要客户端读取用户信息,让 API 返回用户数据,不要读 Cookie。

  3. 开发环境 Cookie 丢失
    localhost 不支持 secure: true,要在配置里判断环境:

    secure: process.env.NODE_ENV === 'production';
    
  4. 跨域问题
    如果前后端分离(不同域名),需要配置 CORS:

    // next.config.js
    async headers() {
      return [{
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
          { key: 'Access-Control-Allow-Origin', value: 'https://your-frontend.com' },
        ],
      }];
    }
    
  5. Cookie 的 path 必须是 /
    否则子路由读不到。

完整登录流程示例

// 1. 用户提交登录表单
const onSubmit = async (data) => {
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    credentials: 'include', // ← 关键
  });

  const result = await response.json();
  if (result.ret === 0) {
    window.location.href = '/'; // Cookie 已设置,直接跳转
  }
};

// 2. 访问需要登录的页面
export default async function ProtectedPage() {
  const user = await requireAuth(); // ← 自动从 Cookie 验证

  return <div>欢迎,{user.username}</div>;
}

// 3. 客户端 API 请求
const fetchData = async () => {
  // Cookie 自动携带,不需要手动处理
  const response = await fetch('/api/data', {
    credentials: 'include', // ← 关键
  });
  const data = await response.json();
};

// 4. 登出
const logout = async () => {
  await fetch('/api/auth/logout', {
    method: 'POST',
    credentials: 'include', // ← 关键
  });
  window.location.href = '/login';
};

安全最佳实践

  1. 生产环境必须用 HTTPS
    secure: true 强制加密传输。

  2. 设置合理的过期时间
    我设的 7 天,根据业务调整。敏感操作可以设置短期 token。

  3. 启用 SameSite 防护
    sameSite: 'lax''strict' 防止 CSRF。

  4. 定期刷新 Token
    可以在中间件里检查 token 过期时间,快到期自动刷新。

  5. 记录登录日志
    异常登录及时报警。

后续优化方向

  1. Refresh Token 机制
    Access Token 短期(1 小时),Refresh Token 长期(7 天),快过期自动刷新。

  2. 记住我功能
    勾选"记住我"设置更长的过期时间(30 天)。

  3. 多设备登录管理
    Token 绑定设备信息,支持查看/踢出其他设备。

  4. 中间件统一拦截
    用 Next.js middleware 在路由层面验证,不用每个页面写 requireAuth()

总结

纯 Cookie 方案的优势:

统一数据源 - 不用维护 localStorage 和 Cookie 两套
更安全 - HttpOnly + Secure + SameSite 三重保护
自动携带 - 浏览器自动在请求中带上 Cookie
服务端可读 - 完美支持 Next.js 混合渲染
代码更简洁 - 不需要手动管理 token 存储和发送