优化登录逻辑的实现,将认证请求和token验证的处理分成两个逻辑文件。新增交叉评查任务列表的页面(尚未对接真实数据)。
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 认证服务核心实现
|
||||
*
|
||||
* 这个文件包含了用户认证、会话管理、Token 刷新等核心功能的具体实现。
|
||||
*
|
||||
* 主要功能:
|
||||
* - 基于 Cookie 的会话管理
|
||||
* - OAuth2.0 Token 自动刷新
|
||||
* - 用户登录状态检查
|
||||
* - 会话创建和销毁
|
||||
*
|
||||
* 技术栈:
|
||||
* - Remix Session Storage (Cookie-based)
|
||||
* - OAuth2.0 Token Management
|
||||
* - TypeScript 类型安全
|
||||
*
|
||||
*/
|
||||
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { tokenManager } from "./token-manager.server";
|
||||
|
||||
/**
|
||||
* 用户角色类型定义
|
||||
*
|
||||
* @typedef {string} UserRole
|
||||
* @property {'common'} common - 普通用户,有基本的系统访问权限
|
||||
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
|
||||
*/
|
||||
export type UserRole = 'common' | 'developer';
|
||||
|
||||
/**
|
||||
* 会话存储配置
|
||||
*
|
||||
* 使用 Remix 的 Cookie Session Storage 来管理用户会话。
|
||||
* Cookie 存储方式的优势:
|
||||
* - 服务器端控制,安全性高
|
||||
* - 自动处理过期时间
|
||||
* - 支持 HttpOnly,防止 XSS 攻击
|
||||
*
|
||||
* 配置说明:
|
||||
* - name: Cookie 名称,用于标识会话
|
||||
* - httpOnly: 防止客户端 JavaScript 访问,提高安全性
|
||||
* - sameSite: CSRF 保护
|
||||
* - secrets: 用于签名和加密 Cookie 内容
|
||||
* - maxAge: 会话有效期,与 OAuth Token 过期时间保持一致
|
||||
*/
|
||||
export const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__lgsession", // 登录会话 Cookie 名称
|
||||
httpOnly: true, // 仅服务器端可访问,防止 XSS
|
||||
path: "/", // Cookie 作用域为整个应用
|
||||
sameSite: "lax", // CSRF 保护,允许顶级导航
|
||||
secrets: ["s3cr3t"], // TODO: 应该从环境变量读取
|
||||
maxAge: 60 * 60 * 2, // 2小时,与 OAuth Token 同步
|
||||
secure: false, // 开发环境中禁用 HTTPS 要求
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取会话对象
|
||||
*
|
||||
* 从请求中提取 Cookie 并获取对应的会话对象。
|
||||
* 这是一个底层函数,通常不直接使用,而是通过 getUserSession 使用。
|
||||
*
|
||||
* @param request - Remix Request 对象,包含 HTTP 请求信息
|
||||
* @returns 会话对象,可以用来读取和设置会话数据
|
||||
*/
|
||||
export async function getSession(request: Request) {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
return sessionStorage.getSession(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户登录状态(带自动Token刷新)
|
||||
*
|
||||
* 这是认证系统的核心函数,负责:
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 验证 OAuth Token 是否有效
|
||||
* 3. 自动刷新即将过期的 Token
|
||||
* 4. 返回用户信息和认证状态
|
||||
*
|
||||
* Token 刷新机制:
|
||||
* - 检查 access_token 是否即将过期(距离过期时间 < 5 分钟)
|
||||
* - 如果即将过期,使用 refresh_token 自动获取新的 access_token
|
||||
* - 如果刷新失败,则认为用户未登录
|
||||
*
|
||||
* @param request - Remix Request 对象
|
||||
* @returns 包含以下字段的对象:
|
||||
* - isAuthenticated: 是否已认证
|
||||
* - userRole: 用户角色 ('common' | 'developer')
|
||||
* - accessToken: OAuth 访问令牌
|
||||
* - refreshToken: OAuth 刷新令牌
|
||||
* - userInfo: 用户详细信息
|
||||
* - isTokenExpired: Token 是否已过期
|
||||
* - refreshedSession: 如果刷新了 Token,返回更新后的会话对象
|
||||
*/
|
||||
export async function getUserSession(request: Request) {
|
||||
const session = await getSession(request);
|
||||
const isAuthenticated = session.get("isAuthenticated") === true;
|
||||
const userRole = session.get("userRole") || 'common' as UserRole;
|
||||
let accessToken = session.get("accessToken");
|
||||
const refreshToken = session.get("refreshToken");
|
||||
let tokenIssuedAt = session.get("tokenIssuedAt");
|
||||
let tokenExpiresIn = session.get("tokenExpiresIn");
|
||||
const userInfo = session.get("userInfo");
|
||||
|
||||
let isTokenExpired = false;
|
||||
let refreshedSession = null;
|
||||
|
||||
// 如果有token信息,检查是否需要刷新
|
||||
if (accessToken && refreshToken && tokenIssuedAt && tokenExpiresIn) {
|
||||
try {
|
||||
const tokenInfo = {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenIssuedAt,
|
||||
tokenExpiresIn
|
||||
};
|
||||
|
||||
// 检查并自动刷新token
|
||||
const refreshResult = await tokenManager.checkAndRefreshToken(tokenInfo);
|
||||
|
||||
if (refreshResult.success && refreshResult.newTokenInfo) {
|
||||
const newToken = refreshResult.newTokenInfo;
|
||||
|
||||
// 如果token被刷新了,更新session
|
||||
if (newToken.accessToken !== accessToken) {
|
||||
console.log("Token已刷新,更新session");
|
||||
|
||||
session.set("accessToken", newToken.accessToken);
|
||||
session.set("refreshToken", newToken.refreshToken);
|
||||
session.set("tokenIssuedAt", newToken.tokenIssuedAt);
|
||||
session.set("tokenExpiresIn", newToken.tokenExpiresIn);
|
||||
|
||||
// 更新本地变量
|
||||
accessToken = newToken.accessToken;
|
||||
tokenIssuedAt = newToken.tokenIssuedAt;
|
||||
tokenExpiresIn = newToken.tokenExpiresIn;
|
||||
|
||||
refreshedSession = session;
|
||||
}
|
||||
|
||||
isTokenExpired = false;
|
||||
} else {
|
||||
console.error("Token刷新失败:", refreshResult.error);
|
||||
isTokenExpired = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Token验证过程中出错:", error);
|
||||
isTokenExpired = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated: isAuthenticated && !isTokenExpired,
|
||||
userRole,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
userInfo,
|
||||
isTokenExpired,
|
||||
refreshedSession // 如果刷新了token,返回更新后的session
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户登录会话
|
||||
*
|
||||
* 在用户成功登录后调用此函数来创建会话并设置 Cookie。
|
||||
* 这个函数通常在以下场景中使用:
|
||||
* - OAuth2.0 登录成功后
|
||||
* - 临时管理员登录
|
||||
* - 其他认证方式成功后
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 创建新的会话对象
|
||||
* 2. 设置认证状态和用户角色
|
||||
* 3. 生成签名的 Cookie
|
||||
* 4. 返回重定向响应并设置 Cookie
|
||||
*
|
||||
* @param isAuthenticated - 是否已认证,通常为 true
|
||||
* @param userRole - 用户角色,决定用户的权限级别
|
||||
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
|
||||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
||||
*/
|
||||
export async function createUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", isAuthenticated);
|
||||
session.set("userRole", userRole);
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
console.log("创建会话 - 设置Cookie:", !!cookie);
|
||||
console.log("创建会话 - 用户角色:", userRole);
|
||||
console.log("创建会话 - 重定向到:", redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
headers: {
|
||||
Location: redirectTo, // 重定向目标 URL
|
||||
"Set-Cookie": cookie, // 设置会话 Cookie
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁会话(用户登出)
|
||||
*
|
||||
* 当用户主动登出或会话失效时调用此函数。
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 获取当前用户的会话
|
||||
* 2. 销毁会话数据(清除所有存储的信息)
|
||||
* 3. 清除客户端的会话 Cookie
|
||||
* 4. 重定向到登录页面
|
||||
*
|
||||
* 注意事项:
|
||||
* - 这个函数只处理本地会话,不会调用 IDaaS 的单点登出
|
||||
* - 如果需要全局登出,应该额外调用 IDaaS 的 SLO 接口
|
||||
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
|
||||
*
|
||||
* @param request - Remix Request 对象,用于获取当前会话
|
||||
* @returns HTTP 302 重定向响应,清除 Cookie 并跳转到登录页
|
||||
*/
|
||||
export async function logout(request: Request) {
|
||||
const session = await getSession(request);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
headers: {
|
||||
Location: "/login", // 重定向到登录页面
|
||||
"Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user