Files
jiang13-bbs/frontend/app/login/page.tsx
freefire 055688a6cf refactor: 完成前后端认证体系重构与安全增强
本提交重构了全栈的认证与安全体系,包含以下核心变更:
1. **后端安全增强**:
   - 使用常量时间比较修复CSRF校验时序漏洞
   - 重构refresh token存储为哈希+密文,支持轮转宽限期与盗用检测
   - 新增cookie前缀自动配置,支持__Host-前缀强化生产环境安全
   - 优化登出逻辑为单设备登出,合并用户信息与未读通知接口
   - 增加数据库迁移脚本,兼容旧版refresh token数据升级
   - 新增定时清理过期refresh token任务

2. **前端安全与体验优化**:
   - 新增cookie工具库,统一管理认证cookie名与转发逻辑
   - 修复开放重定向漏洞,实现安全跳转校验
   - 重构SSR请求逻辑,仅转发必要的access token cookie
   - 优化middleware轮转逻辑,修复并发请求去重与边缘部署兼容性
   - 修复登录态静默校正逻辑,兼容旧版cookie路径与名称
   - 新增环境变量示例配置文件

3. **工程化改进**:
   修复Next.js类型文件导入路径,统一前后端配置与命名规范
2026-09-12 15:23:13 +08:00

101 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
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 = safeRedirect(searchParams.get("redirect"));
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await apiLogin(username, password);
// 登录成功后后端通过 Set-Cookie 写入 HttpOnly JWT,前端无需存储 token
if (res.user) {
// root layout 在客户端导航中持久化,push 后再 refresh 让其重新取登录态
router.push(redirect);
router.refresh();
} else {
setError(res.error || "登录失败");
}
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto pt-8 sm:pt-14">
<div className="mb-7 text-center">
<span
className="inline-flex w-14 h-14 rounded-2xl items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<MessagesSquare size={26} />
</span>
<h1 className="text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
欢迎回来
</h1>
<p className="text-sm mt-2" style={{ color: "var(--ink-2)" }}>
登录后发帖、评论与点赞
</p>
</div>
<form onSubmit={handleSubmit} className="panel p-6 sm:p-7 space-y-4">
{error && <div className="alert-error">{error}</div>}
<div>
<label className="field-label">用户名</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoFocus
className="field"
placeholder="testuser"
/>
</div>
<div>
<label className="field-label">密码</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="field"
placeholder="••••••"
/>
</div>
<button type="submit" disabled={loading} className="btn btn-primary w-full justify-center mt-2">
{loading ? "登录中..." : "登录"}
</button>
</form>
<p className="meta text-center mt-5">
还没有账号?
<Link href="/register" className="ml-1 font-semibold" style={{ color: "var(--accent)" }}>
立即注册
</Link>
</p>
</div>
);
}