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:
@@ -17,7 +17,8 @@ import {
|
||||
Bell,
|
||||
Home,
|
||||
} from "lucide-react";
|
||||
import { apiLogout, type User } from "@/lib/api";
|
||||
import { apiLogout, apiMe, type User } from "@/lib/api";
|
||||
import { hasAuthCookieHint } from "@/lib/cookies";
|
||||
import NotificationBell from "./NotificationBell";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
@@ -36,6 +37,7 @@ export default function Header({
|
||||
const [theme, setTheme] = useState<"light" | "dark">(initialTheme);
|
||||
// 登录态以 SSR 直出为初始值,F5 无切换闪动;router.refresh() 后随 props 校正
|
||||
const [user, setUser] = useState<User | null>(initialUser);
|
||||
const [unread, setUnread] = useState(initialUnread);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -44,6 +46,23 @@ export default function Header({
|
||||
setUser(initialUser);
|
||||
}, [initialUser]);
|
||||
|
||||
useEffect(() => {
|
||||
setUnread(initialUnread);
|
||||
}, [initialUnread]);
|
||||
|
||||
// 挂载后静默校正一次(仅一次):覆盖 SSR 期间后端不可用、旧 Path refresh
|
||||
// 半登录等边缘情况。正常 F5 下结果与 SSR 一致,不产生可见闪动;
|
||||
// 无可读 CSRF cookie 说明必然是游客,跳过请求
|
||||
useEffect(() => {
|
||||
if (!hasAuthCookieHint()) return;
|
||||
apiMe()
|
||||
.then((res) => {
|
||||
setUser(res.user);
|
||||
if (typeof res.unread_count === "number") setUnread(res.unread_count);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const q = searchQuery.trim();
|
||||
@@ -184,7 +203,7 @@ export default function Header({
|
||||
<button onClick={toggleTheme} className={iconBtn} style={iconBtnStyle} aria-label="切换主题">
|
||||
{theme === "dark" ? <Sun size={17} /> : <Moon size={17} />}
|
||||
</button>
|
||||
{user && <NotificationBell initialUnread={initialUnread} />}
|
||||
{user && <NotificationBell initialUnread={unread} />}
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/compose" className="btn btn-primary btn-sm ml-1">
|
||||
@@ -221,7 +240,7 @@ export default function Header({
|
||||
|
||||
{/* 移动端按钮 */}
|
||||
<div className="flex items-center gap-2 lg:hidden">
|
||||
{user && <NotificationBell initialUnread={initialUnread} />}
|
||||
{user && <NotificationBell initialUnread={unread} />}
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full"
|
||||
|
||||
Reference in New Issue
Block a user