CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/认证与登录门控
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

认证与登录门控

cookie token、Server 读 session、LoginGate 模式。

30 分钟

认证与登录门控

这一课你将能做到

  1. 画出完整的登录链路:提交凭证 → 后端校验签发 token → Cookie 存储 → 请求携带 → 服务端校验 → 门控 UI
  2. 用 JWT + HttpOnly Cookie + middleware 实现一个可运行的登录门控(含登出与未登录跳转)
  3. 说出 session 管理两种模式(JWT / 服务端 Session)的取舍,答好「token 放哪、middleware 做什么」两道高频面试题

为什么要学

真实产品一定有「未登录不能看」的内容——本站平台就是这样:免费课时随便看,后面课时锁着,练习记录、个人中心必须登录。这套「登录门控」是前后端协作的综合题,最能暴露一个人的全栈功底,所以也是面试常客:「token 存 localStorage 还是 Cookie?为什么?」「middleware 层做什么鉴权?」「JWT 和 Session 怎么选?」

本课的链路设计遵循本站真实架构:凭证校验由 Spring Boot 后端负责(数据库、密码哈希都在 Java 侧),Next.js 负责「持有凭证、控制访问、渲染门控 UI」。这个分工在企业里最常见——前端框架不做用户体系,只做鉴权协议的客户端。

先立全链路总图,本课所有代码都在填这张图的格子:

① 登录页提交账号密码
        ↓
② Next.js BFF 转发 → Spring Boot 校验 → 签发 JWT
        ↓
③ Set-Cookie: token=xxx (HttpOnly) 存入浏览器
        ↓
④ 之后每个请求自动携带 Cookie
   ├─ middleware.ts:路由级门控(未登录 → 跳 /login)
   ├─ Server 组件:读 Cookie 调后端取个人数据
   └─ Client 组件:只管展示,不碰 token
        ↓
⑤ 登出:清除 Cookie + 后端吊销(可选)

先建立图像

JWT 的结构——三段式,用点分隔:

eyJhbGciOiJIUzI1NiJ9  .  eyJ1c2VySWQiOjQyfQ  .  SflKxwRJ...
      头部(header)            载荷(payload)          签名(signature)
   算法:HS256             {userId: 42, exp:...}   HMAC(header+payload, secret)

三个关键认知:

  • payload 只是 Base64 编码不是加密,任何人都能解开看——所以里面只放 userId、过期时间这类非敏感信息,绝不能放密码、手机号
  • 签名保证「没被篡改」:改一个字,服务端验签就失败
  • 服务端无状态:不用存 token,验签即信任——这是它和 Session 的本质区别

两种 session 管理模式对照:

JWT(无状态)服务端 Session(有状态)
凭证存哪客户端自持服务端存(Redis/内存),客户端只拿 sessionId
校验方式验签(不算库)查库/Redis
吊销难(等过期,或配黑名单)删记录即生效
典型选型前后端分离、微服务传统单体、强安全要求

跟着做

步骤 1:登录页(Client 组件收凭证)

// src/app/login/page.tsx
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";

export default function LoginPage() {
  const router = useRouter();
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError("");
    const res = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    });
    setLoading(false);
    if (res.ok) {
      router.push("/me");          // 登录成功进个人页
      router.refresh();            // 刷新服务端渲染数据
    } else {
      setError("用户名或密码错误");
    }
  }

  return (
    <form onSubmit={handleSubmit} className="mx-auto max-w-sm space-y-3 p-6">
      <h1 className="text-2xl font-bold">登录</h1>
      <input
        className="w-full rounded border p-2"
        placeholder="用户名"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        className="w-full rounded border p-2"
        type="password"
        placeholder="密码"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      {error && <p className="text-sm text-red-600">{error}</p>}
      <button
        className="w-full rounded bg-blue-600 py-2 text-white disabled:opacity-50"
        disabled={loading}
      >
        {loading ? "登录中…" : "登录"}
      </button>
    </form>
  );
}

步骤 2:BFF 登录接口(转发 + 签发 Cookie)

npm install jose

jose 是标准 JWT 库(Serverless 环境可用)。接口代码:

// src/app/api/login/route.ts
import { NextRequest, NextResponse } from "next/server";

const BACKEND = process.env.API_INTERNAL_URL ?? "http://localhost:8080";

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

  // 1. 凭证校验交给 Spring Boot(真实架构)
  const backendRes = await fetch(`${BACKEND}/api/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });

  if (!backendRes.ok) {
    return NextResponse.json({ message: "用户名或密码错误" }, { status: 401 });
  }

  const { token } = await backendRes.json(); // 后端签发的 JWT

  // 2. 写入 HttpOnly Cookie
  const res = NextResponse.json({ ok: true, username });
  res.cookies.set("token", token, {
    httpOnly: true,                      // JS 不可读,防 XSS
    sameSite: "lax",                     // 基础 CSRF 防护
    secure: process.env.NODE_ENV === "production",
    maxAge: 60 * 60 * 24 * 7,            // 7 天
    path: "/",
  });
  return res;
}

强调这个设计:token 永远不出现在前端 JS 的手里。登录响应体里不返回 token 给页面,直接进 HttpOnly Cookie——XSS 偷不走。

步骤 3:middleware 路由级门控

// src/middleware.ts(或 src/proxy.ts,视版本约定)
import { NextRequest, NextResponse } from "next/server";

const PROTECTED = ["/me", "/practice", "/progress"]; // 需登录的前缀

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const isProtected = PROTECTED.some((p) => pathname.startsWith(p));
  const token = request.cookies.get("token")?.value;

  if (isProtected && !token) {
    // 未登录访问受保护页:跳登录页并记住来源
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("from", pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 已登录访问 /login:送回个人页,避免重复登录
  if (pathname === "/login" && token) {
    return NextResponse.redirect(new URL("/me", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/me/:path*", "/practice/:path*", "/progress/:path*", "/login"],
};

middleware 跑在边缘层、每次请求最先执行,用户根本看不到受保护页的闪烁——这是体验最好的门控位置。注意 middleware 里只判断「token 存不存在」这种轻量逻辑,验签和用户信息查询仍在后端/Server 组件里(middleware 运行环境受限,也不该每次请求都做重活)。

登录成功后回跳来源页,登录页补一行:

const from = new URLSearchParams(window.location.search).get("from") ?? "/me";
// router.push(from) 替代固定跳 /me

步骤 4:Server 组件读登录态渲染个人数据

// src/lib/serverAuth.ts —— 本站同款封装
import { cookies } from "next/headers";

export async function getServerToken(): Promise<string | undefined> {
  const store = await cookies();
  return store.get("token")?.value;
}
// src/app/me/page.tsx
import { getServerToken } from "@/lib/serverAuth";
import { redirect } from "next/navigation";

export const dynamic = "force-dynamic"; // 个人页必须动态渲染

export default async function MePage() {
  const token = await getServerToken();
  if (!token) redirect("/login?from=/me"); // 组件层二道门(middleware 之外兜底)

  const res = await fetch(`${process.env.API_INTERNAL_URL}/api/me`, {
    headers: { Authorization: `Bearer ${token}` },
    cache: "no-store",
  });
  if (res.status === 401) redirect("/login"); // token 过期/无效
  const user = await res.json();

  return (
    <main className="mx-auto max-w-2xl p-6">
      <h1 className="text-2xl font-bold">你好,{user.username}</h1>
      <p className="mt-2 text-gray-600">已学 {user.lessonCount} 课时</p>
    </main>
  );
}

注意 dynamic = "force-dynamic"——上一课的知识点在这里落地:读 Cookie 的页面天生动态,显式声明以防万一。

步骤 5:登出

// src/app/api/logout/route.ts
import { NextResponse } from "next/server";

export async function POST() {
  const res = NextResponse.json({ ok: true });
  res.cookies.set("token", "", { maxAge: 0, path: "/" }); // 置空 + 立即过期
  return res;
}
// 放在个人页里的登出按钮(Client 组件)
"use client";
import { useRouter } from "next/navigation";

export default function LogoutButton() {
  const router = useRouter();
  return (
    <button
      className="rounded border px-3 py-1 text-sm"
      onClick={async () => {
        await fetch("/api/logout", { method: "POST" });
        router.push("/login");
        router.refresh();
      }}
    >
      退出登录
    </button>
  );
}

讲清楚要点

  1. 三层门控纵深防御:middleware(路由级,最早拦截)→ Server 组件(数据级,兜底验签)→ 后端 API(最终裁决,所有写操作必须鉴权)。跳过前两层或许能用,但后端校验永远不能省——直接 curl 你的 API 可绕过一切前端门控。
  2. token 存储的定论:HttpOnly Cookie 优先。localStorage 能被 XSS 一锅端;Cookie 的 httpOnly 让脚本读不到,sameSite 挡基础 CSRF,secure 保证只走 HTTPS。面试答这句就是满分线。
  3. Next.js 侧的职责边界:Next.js 持有与转发凭证、做 UI 门控与重定向,不做密码校验、不存用户表——那是 Spring Boot 的事。前端框架里的认证代码只应该有「搬运」和「判断」,不该有「验证」。
  4. router.refresh() 是被低估的一步:登录后若不刷新,Server 组件缓存的服务端渲染结果还是「未登录态」。router.refresh() 让当前路由的服务端重新渲染,导航栏上的头像才会出现。
  5. 登录态过期要有优雅路径:后端 401 → Server 组件 redirect("/login");Client 请求 401 → 统一在 fetch 封装里处理跳转。用户感知应该是「请你重新登录」而不是白屏或报错堆栈。

你可能会懵的地方

  • middleware 里为什么不验 JWT 签名? 三个原因:middleware 运行在边缘环境,功能受限且应极快;验签需要密钥,把 secret 暴露到更广的执行面增加风险面;门控的目的是「体验层拦截」,真正的安全裁决在 API 层。middleware 判有无 token、Server 组件验证有效性,分工明确。

  • JWT 无法主动失效,用户改密码后旧 token 还能用怎么办? 纯 JWT 确实如此。工程解法:① 短有效期(如 2h)+ Refresh Token 轮换;② 关键操作(改密码)后维护一个 token 黑名单或把用户版本号编进 payload 验签时比对;③ 对安全要求高的系统改用服务端 Session(Redis 存储,删了即失效)。这是 JWT vs Session 面试题的进阶答案。

  • CSRF 和 XSS,Cookie 方案防了哪个? sameSite 防 CSRF(跨站请求不自动带 Cookie),httpOnly 防 XSS 窃取。但 XSS 能偷不到却「能用」——脚本可以在受害浏览器里直接发请求。所以 Cookie 方案也要配合输出转义、CSP 等防 XSS 手段,不是一劳永逸。

  • NextAuth(Auth.js)什么时候用? 需要「第三方登录」(GitHub/Google/微信)或多租户用户体系时,它的 OAuth 流程、账号关联、session 适配是现成的。本场景(自研 Spring Boot 用户体系 + JWT)用 NextAuth 反而要写自定义 provider 绕远路——自研后端 + BFF 转发是更短路径。面试说清这个选型逻辑比报菜名重要。

  • 为什么登录成功要 router.refresh(),而不只是 push? push 只做客户端导航,服务端组件的渲染数据(如导航栏的用户信息)可能来自旧缓存或根本没取过;refresh 触发当前路由服务端重渲染,整个页面状态与登录态对齐。

错法对照

错法问题正解
登录后把 token 存 localStorageXSS 可窃取HttpOnly Cookie
middleware 里 jwt.verify 全量验签边缘层过重、密钥扩散middleware 只判存在性,验签留后端
只做 middleware 门控,后端接口裸奔curl 直连绕过一切门控后端 API 独立鉴权(Security 注解/过滤器)
受保护页面没配动态渲染读 Cookie 的页面可能被静态缓存串号export const dynamic = "force-dynamic"
登出只删前端状态不清 Cookie下次请求仍带旧 token服务端 Set-Cookie maxAge=0 清除

卡点急救

  1. 登录成功但刷新后又是未登录态:Cookie 没写上。检查接口响应头有没有 Set-Cookie;dev 环境 secure: true 会导致 HTTP 下 Cookie 被丢弃(本课代码已按 NODE_ENV 区分)。
  2. middleware 报错或完全不执行:确认文件位置(src/middleware.ts 与 src/app 同级)与导出名(middleware + config);matcher 路径写错会导致永不过滤;Next 16 起文件名约定改为 proxy.ts 的版本注意对照官方文档。
  3. Server 组件拿到的 token 是 undefined:cookies() 在 Next 15 是异步的,必须 await cookies();另外确认请求确实带了 Cookie(同域、未开隐身、Cookie 未过期)。

面试常问

  • JWT 和 Session 的区别与选型? JWT 无状态:凭证自含签名,服务端验签不查库,适合分布式与前后端分离,但吊销困难、payload 可解码不可放敏感数据;Session 有状态:服务端存储会话(Redis),客户端只持 sessionId,吊销即时、控制力强,代价是存储与查询开销。选型:自研平台级产品倾向 Session 或「短 JWT + Refresh」,微服务与跨域场景倾向 JWT。

  • token 为什么放 HttpOnly Cookie 而不是 localStorage? localStorage 可被同源 JS 读取,一旦存在 XSS 漏洞(UGC 内容、三方脚本)token 即泄露;HttpOnly Cookie 对 JS 不可见,切断了窃取路径,配合 sameSite 缓解 CSRF、secure 限定 HTTPS。代价是需要 BFF 层处理 Cookie 读写与 CSRF 防护——本站登录接口就是 Next.js BFF 负责把后端签发的 token 写入 Cookie。

  • 描述你们的登录门控链路。 参考回答:登录页(Client 组件)提交凭证 → Next.js BFF 转发 Spring Boot 校验 → 后端签发 JWT → BFF 写入 HttpOnly Cookie;之后每个请求自动携带,middleware 对 /me /practice 等前缀做路由级门控(未登录重定向并记录来源),Server 组件读 Cookie 后携带 Authorization 调后端取个人数据(no-store),后端对每个 API 独立鉴权;登出在服务端清除 Cookie。三层门控 + 凭证不过前端 JS 是安全设计核心。

自测三问

  1. 从点「登录」到看到个人页,完整请求链路经过哪几层?每层各做什么?
  2. middleware、Server 组件、后端 API 三层门控,哪层绝对不能省?为什么?
  3. JWT 的 payload 能放手机号吗?为什么?

小结一句话

认证的本质是「凭证的签发、携带、校验、吊销」四件事:后端签发 JWT、HttpOnly Cookie 持有、middleware 做路由门控、Server 组件带凭证取数,后端永远保留最终裁决权。

延伸

  • Next.js 官方文档:Authentication
  • 下一课:打开本站 platform 源码,把 1-10 课的知识全部对上号。
加载本节练习…
← 上一课Tailwind 与样式下一课 →读 platform 源码