refactor: 完成前后端认证体系重构与安全增强
本提交重构了全栈的认证与安全体系,包含以下核心变更: 1. **后端安全增强**: - 使用常量时间比较修复CSRF校验时序漏洞 - 重构refresh token存储为哈希+密文,支持轮转宽限期与盗用检测 - 新增cookie前缀自动配置,支持__Host-前缀强化生产环境安全 - 优化登出逻辑为单设备登出,合并用户信息与未读通知接口 - 增加数据库迁移脚本,兼容旧版refresh token数据升级 - 新增定时清理过期refresh token任务 2. **前端安全与体验优化**: - 新增cookie工具库,统一管理认证cookie名与转发逻辑 - 修复开放重定向漏洞,实现安全跳转校验 - 重构SSR请求逻辑,仅转发必要的access token cookie - 优化middleware轮转逻辑,修复并发请求去重与边缘部署兼容性 - 修复登录态静默校正逻辑,兼容旧版cookie路径与名称 - 新增环境变量示例配置文件 3. **工程化改进**: 修复Next.js类型文件导入路径,统一前后端配置与命名规范
This commit is contained in:
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { PenSquare, MessagesSquare, ChevronRight } from "lucide-react";
|
||||
import { fetchPosts, fetchBoards } from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import PostRow from "@/components/PostRow";
|
||||
import PinnedPosts from "@/components/PinnedPosts";
|
||||
@@ -26,7 +27,7 @@ export default async function BoardPage({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { page: pageStr } = await searchParams;
|
||||
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
|
||||
const cookie = (await cookies()).toString();
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const [postsData, boardsData] = await Promise.all([
|
||||
fetchPosts(page, 20, Number(id), "latest", "", cookie),
|
||||
fetchBoards(),
|
||||
|
||||
@@ -5,7 +5,8 @@ import "./globals.css";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import Toaster from "@/components/Toaster";
|
||||
import { fetchMe, fetchUnreadCount, type User } from "@/lib/api";
|
||||
import { fetchMe, type User } from "@/lib/api";
|
||||
import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies";
|
||||
|
||||
// 构建期下载并自托管字体:同源加载 + 自动 preload + fallback 度量校正,
|
||||
// 消除 F5 时 Google CDN 字体到达后整页"由粗变细"的 FOUT 闪动
|
||||
@@ -52,21 +53,19 @@ export default async function RootLayout({
|
||||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||||
|
||||
// SSR 直出登录态(access 过期时 middleware 已先行轮转,见 middleware.ts),
|
||||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"
|
||||
const cookieHeader = cookieStore.toString();
|
||||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"。
|
||||
// 旧版 refresh cookie 可能是 Path=/api/auth(页面请求不带),因此只要
|
||||
// token 或 refresh 任一存在就向后端确认一次,避免半登录态(列表点赞态与
|
||||
// Header 游客态不一致),并兼容 access 15 分钟内的自然窗口。
|
||||
let user: User | null = null;
|
||||
let unread = 0;
|
||||
if (cookieStore.get("j13_refresh")?.value) {
|
||||
try {
|
||||
const [me, count] = await Promise.all([
|
||||
fetchMe(cookieHeader),
|
||||
fetchUnreadCount(cookieHeader),
|
||||
]);
|
||||
user = me.user;
|
||||
unread = count;
|
||||
} catch {
|
||||
// 后端不可用时降级为游客视图,不阻塞页面渲染
|
||||
}
|
||||
const hasAuthCookie =
|
||||
!!cookieStore.get(TOKEN_COOKIE)?.value || !!cookieStore.get(REFRESH_COOKIE)?.value;
|
||||
if (hasAuthCookie) {
|
||||
// 用户信息与未读数合并为一次请求;只转发最小化的 access cookie
|
||||
const me = await fetchMe(authCookieHeader(cookieStore) || undefined);
|
||||
user = me.user;
|
||||
unread = me.unread_count;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,10 +6,18 @@ import Link from "next/link";
|
||||
import { MessagesSquare } from "lucide-react";
|
||||
import { apiLogin } from "@/lib/api";
|
||||
|
||||
// 仅允许同源相对路径跳转,拦截 //host、/\\host 与绝对 URL 形式的开放重定向
|
||||
function safeRedirect(to: string | null): string {
|
||||
if (!to || !to.startsWith("/") || to.startsWith("//") || to.startsWith("/\\")) {
|
||||
return "/";
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const redirect = searchParams.get("redirect") || "/";
|
||||
const redirect = safeRedirect(searchParams.get("redirect"));
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { PenSquare, SearchX, MessagesSquare, Check, ArrowRight } from "lucide-react";
|
||||
import { fetchPosts, fetchBoards } from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import PostRow from "@/components/PostRow";
|
||||
import PinnedPosts from "@/components/PinnedPosts";
|
||||
@@ -20,7 +21,8 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
const { q, page: pageStr } = await searchParams;
|
||||
const keyword = q?.trim() || "";
|
||||
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
|
||||
const cookie = (await cookies()).toString();
|
||||
// 仅转发最小化的 access cookie 供后端填充 liked 状态(不携带 refresh 等)
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const [postsData, boardsData] = await Promise.all([
|
||||
fetchPosts(page, 20, undefined, "latest", keyword, cookie),
|
||||
fetchBoards(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { cookies } from "next/headers";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { fetchPostDetail, fetchComments } from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import CommentSection from "@/components/CommentSection";
|
||||
import PostActions from "@/components/PostActions";
|
||||
import LikeButton from "@/components/LikeButton";
|
||||
@@ -35,7 +36,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
|
||||
export default async function PostDetailPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const cookie = (await cookies()).toString();
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const { post } = await fetchPostDetail(id, cookie);
|
||||
const { comments } = await fetchComments(id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user