给所有请求都加上jwt,隐藏生成jwt的secret(放到.env中),隐藏app-secret(放在pm2运行配置文件中,后续直接读取环境配置即可)

This commit is contained in:
2025-10-17 15:28:22 +08:00
parent 9ec6d30573
commit 59706b70d0
70 changed files with 2279 additions and 688 deletions
+98 -43
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { useSearchParams, Form } from "@remix-run/react";
import { useActionData, useLoaderData, Form } from "@remix-run/react";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { OAUTH_CONFIG } from "~/config/api-config";
@@ -32,11 +32,30 @@ export async function loader({ request }: LoaderFunctionArgs) {
const redirectTo = url.searchParams.get("redirect") || "/";
const session = await getSession(request);
// 读取 flash 消息(来自 callback 的错误)
const loginError = session.get("loginError");
session.set("redirectTo", redirectTo);
// 提交 session 以清除 flash 消息
if (loginError) {
const { sessionStorage } = await import("~/api/login/auth.server");
return Response.json({
isAuthenticated: false,
redirectTo,
flashError: loginError
}, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session)
}
});
}
return Response.json({
isAuthenticated: false,
redirectTo
redirectTo,
flashError: null
});
}
@@ -60,10 +79,17 @@ export async function action({ request }: ActionFunctionArgs) {
// 登录成功,直接返回重定向响应
return response;
} else {
// 登录失败,解析错误信息并重定向到登录页面
// 登录失败,返回错误信息(不再使用URL参数)
const errorData = await response.json();
const errorMsg = errorData.error || "登录失败";
return redirect(`/login?error=${encodeURIComponent(errorMsg)}`);
return Response.json({
success: false,
error: errorData.error || "登录失败",
retryCount: errorData.retryCount || 0,
isLocked: errorData.isLocked || false,
remainingAttempts: errorData.remainingAttempts || 5
}, {
status: response.status
});
}
}
@@ -71,40 +97,19 @@ export async function action({ request }: ActionFunctionArgs) {
}
export default function Login() {
const [searchParams] = useSearchParams();
const error = searchParams.get("error");
const actionData = useActionData<typeof action>();
const loaderData = useLoaderData<typeof loader>();
const [isFlipped, setIsFlipped] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
// 获取错误消息的友好描述
const getErrorMessage = (error: string | null) => {
if (!error) return null;
switch (error) {
case "missing_code":
return "登录过程中缺少授权码,请重新登录";
case "invalid_state":
return "登录状态验证失败,请重新登录";
case "token_error":
return "获取访问令牌失败,请重新登录";
case "userinfo_error":
return "获取用户信息失败,请重新登录";
case "callback_error":
return "登录回调处理失败,请重新登录";
case "用户名和密码不能为空":
case "用户名和密码不能为空,请重新输入":
return "用户名和密码不能为空,请重新输入";
case "登录失败,请检查用户名和密码":
case "用户名或密码错误,请重新输入":
return "用户名或密码错误,请重新输入";
case "登录请求失败,请稍后重试":
case "网络连接失败,请稍后重试":
return "网络连接失败,请稍后重试";
default:
return decodeURIComponent(error);
}
};
// 从 actionData 或 loaderData 中获取错误信息
// actionData 的错误优先(来自密码登录)
// loaderData.flashError 次之(来自 OAuth 回调)
const error = actionData?.error || loaderData?.flashError;
const isLocked = actionData?.isLocked || false;
const retryCount = actionData?.retryCount || 0;
const remainingAttempts = actionData?.remainingAttempts || 5;
// 处理OAuth2.0登录
const handleOAuthLogin = () => {
@@ -143,6 +148,13 @@ export default function Login() {
// 处理账号密码登录表单提交
const handlePasswordLoginSubmit = (e: React.FormEvent) => {
// 检查账户是否被锁定
if (isLocked) {
e.preventDefault();
toastService.error("账户已被锁定,请联系管理员");
return;
}
// 客户端验证
if (!username.trim()) {
e.preventDefault();
@@ -180,9 +192,22 @@ export default function Login() {
<h2 className="login-subtitle"></h2>
{error && (
<div className="error-message-container">
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
<div className="error-text">{getErrorMessage(error)}</div>
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
<div className="error-icon">
{isLocked ? (
<i className="ri-lock-line"></i>
) : (
<i className="ri-error-warning-line"></i>
)}
</div>
<div className="error-text">
{error}
{!isLocked && retryCount > 0 && (
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
{remainingAttempts}
</div>
)}
</div>
</div>
)}
@@ -235,9 +260,22 @@ export default function Login() {
<h2 className="login-subtitle"></h2>
{error && (
<div className="error-message-container">
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
<div className="error-text">{getErrorMessage(error)}</div>
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
<div className="error-icon">
{isLocked ? (
<i className="ri-lock-line"></i>
) : (
<i className="ri-error-warning-line"></i>
)}
</div>
<div className="error-text">
{error}
{!isLocked && retryCount > 0 && (
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
{remainingAttempts}
</div>
)}
</div>
</div>
)}
@@ -275,10 +313,27 @@ export default function Login() {
<button
type="submit"
className="admin-login-button"
disabled={isLocked}
style={isLocked ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
>
<i className="ri-login-box-line"></i>
<i className={isLocked ? "ri-lock-line" : "ri-login-box-line"}></i>
{isLocked ? "账户已锁定" : "登录"}
</button>
{isLocked && (
<div style={{
marginTop: '12px',
padding: '8px',
background: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
fontSize: '0.9em',
textAlign: 'center',
color: '#856404'
}}>
<i className="ri-information-line"></i>
</div>
)}
</Form>
<div className="back-to-oauth">