perf(session): ttl
This commit is contained in:
@@ -1,156 +1,113 @@
|
||||
"use server"
|
||||
|
||||
import { Base64 } from "js-base64"
|
||||
import { headers } from "next/headers"
|
||||
import redis from "./redis"
|
||||
import { deleteCachedSession, getOrLoadSession } from "./session-cache"
|
||||
import {
|
||||
getClientIpFromRequest,
|
||||
getSessionRedisKeyFromCookieValue,
|
||||
SESSION_COOKIE_NAME,
|
||||
} from "./session-common"
|
||||
|
||||
// 生成加密密钥
|
||||
export async function generateKey(): Promise<CryptoKey> {
|
||||
return crypto.subtle.generateKey(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateKeyFromEnv(
|
||||
envPassword: string
|
||||
): Promise<CryptoKey> {
|
||||
const encoder = new TextEncoder()
|
||||
const password = encoder.encode(envPassword)
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
password,
|
||||
{
|
||||
name: "PBKDF2",
|
||||
},
|
||||
false,
|
||||
["deriveKey"]
|
||||
)
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: new Uint8Array(16), // 随机盐值
|
||||
iterations: 100000, // 迭代次数
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
{
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
}
|
||||
|
||||
// 将ArrayBuffer转换为Uint8Array
|
||||
function arrayBufferToUint8Array(buffer: ArrayBuffer) {
|
||||
return new Uint8Array(buffer)
|
||||
}
|
||||
|
||||
// 加密函数
|
||||
export async function encrypt(data: any, key: CryptoKey): Promise<string> {
|
||||
const encodedData = new TextEncoder().encode(JSON.stringify(data))
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16)) // 16 bytes for AES block size
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
encodedData
|
||||
)
|
||||
|
||||
// 将Uint8Array转换为Base64字符串,以便存储和传输
|
||||
const encryptedBase64 = Base64.fromUint8Array(
|
||||
arrayBufferToUint8Array(encrypted)
|
||||
)
|
||||
const ivBase64 = Base64.fromUint8Array(new Uint8Array(iv)) // 使用js-base64进行编码
|
||||
|
||||
// 将IV和加密数据拼接在一起,以便解密时使用
|
||||
return `${ivBase64}::${encryptedBase64}`
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
export async function decrypt(
|
||||
encryptedData: string,
|
||||
key: CryptoKey
|
||||
): Promise<any> {
|
||||
// 将加密数据从Base64字符串转换回Uint8Array
|
||||
const [ivBase64, encryptedBase64] = encryptedData.split("::")
|
||||
|
||||
const ivArray = Buffer.from(ivBase64, "base64")
|
||||
|
||||
const encrypted = Buffer.from(encryptedBase64, "base64")
|
||||
|
||||
// 使用AES-CBC算法进行解密
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: ivArray,
|
||||
},
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
// 将ArrayBuffer转换为字符串
|
||||
return new TextDecoder().decode(decrypted)
|
||||
}
|
||||
const roundDurationMs = (durationMs: number) =>
|
||||
Math.round(durationMs * 100) / 100
|
||||
|
||||
// 校验用户信息
|
||||
export async function genAccessToken(req: any) {
|
||||
const sessionLookupStart = performance.now()
|
||||
let redisLookupMs = 0
|
||||
let sessionSource: "memory" | "shared" | "redis" | "skip" = "skip"
|
||||
|
||||
try {
|
||||
// 校验用户信息
|
||||
// const fingerprint = req.headers.get("Fingerprint")
|
||||
const sessionData = req.cookies.get("session")?.value
|
||||
const sessionData = req?.cookies?.get?.(SESSION_COOKIE_NAME)?.value
|
||||
const redisKey = getSessionRedisKeyFromCookieValue(sessionData)
|
||||
|
||||
// 无session 直接重定向到登录页面
|
||||
if (!sessionData) {
|
||||
return "客户端请求中,未包含用户标识"
|
||||
if (!redisKey) {
|
||||
return {
|
||||
ok: false as const,
|
||||
message: "客户端请求中,未包含用户标识",
|
||||
timing: {
|
||||
sessionLookupMs: roundDurationMs(
|
||||
performance.now() - sessionLookupStart
|
||||
),
|
||||
redisLookupMs: 0,
|
||||
sessionSource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let redisKey = JSON.parse(sessionData)
|
||||
const sessionResult = await getOrLoadSession(redisKey, async () => {
|
||||
const redisLookupStart = performance.now()
|
||||
const userDataString = await redis.get(redisKey)
|
||||
redisLookupMs = roundDurationMs(performance.now() - redisLookupStart)
|
||||
|
||||
const key = await generateKeyFromEnv(process.env.ENCRYPTION_KEY || "")
|
||||
const decryptedSessionData = JSON.parse(await decrypt(redisKey, key))
|
||||
const fingerprint = JSON.parse(decryptedSessionData)?.fingerprint
|
||||
if (!userDataString) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
let userData: any = await redis.get(redisKey)
|
||||
return JSON.parse(userDataString)
|
||||
})
|
||||
|
||||
userData = JSON.parse(userData)
|
||||
sessionSource = sessionResult.source
|
||||
|
||||
// redis中无用户信息 直接重定向到登录页面
|
||||
if (!userData) {
|
||||
return "未获取到用户信息"
|
||||
if (!sessionResult.data) {
|
||||
deleteCachedSession(redisKey)
|
||||
return {
|
||||
ok: false as const,
|
||||
message: "未获取到用户信息",
|
||||
timing: {
|
||||
sessionLookupMs: roundDurationMs(
|
||||
performance.now() - sessionLookupStart
|
||||
),
|
||||
redisLookupMs,
|
||||
sessionSource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const header = headers()
|
||||
let clientIP =
|
||||
(await header).get("x-forwarded-for")?.split(",")[0] ||
|
||||
req?.socket?.remoteAddress
|
||||
// ip v6地址
|
||||
if (clientIP.startsWith("::")) {
|
||||
clientIP = clientIP.slice(7)
|
||||
}
|
||||
// 登录ip与用户请求ip不匹配
|
||||
const userData = sessionResult.data
|
||||
const clientIP = getClientIpFromRequest(req)
|
||||
|
||||
if (
|
||||
clientIP !== userData?.clientIP ||
|
||||
fingerprint !== userData?.fingerprint
|
||||
) {
|
||||
return `客户端信息与缓存用户信息不一致 \n 客户端: ${clientIP}, ${fingerprint} | redis存储: ${userData?.clientIP}, ${userData?.fingerprint}`
|
||||
if (clientIP !== userData?.clientIP) {
|
||||
return {
|
||||
ok: false as const,
|
||||
message: `客户端信息与缓存用户信息不一致 \n 客户端: ${clientIP} | redis存储: ${userData?.clientIP}`,
|
||||
timing: {
|
||||
sessionLookupMs: roundDurationMs(
|
||||
performance.now() - sessionLookupStart
|
||||
),
|
||||
redisLookupMs,
|
||||
sessionSource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...userData,
|
||||
redisKey: sessionData,
|
||||
ok: true as const,
|
||||
userData: {
|
||||
...userData,
|
||||
redisKey,
|
||||
},
|
||||
timing: {
|
||||
sessionLookupMs: roundDurationMs(
|
||||
performance.now() - sessionLookupStart
|
||||
),
|
||||
redisLookupMs,
|
||||
sessionSource,
|
||||
},
|
||||
}
|
||||
} catch (err: any) {
|
||||
return `获取用户信息报错, 报错原因:${err?.message}`
|
||||
return {
|
||||
ok: false as const,
|
||||
message: `获取用户信息报错, 报错原因:${err?.message}`,
|
||||
timing: {
|
||||
sessionLookupMs: roundDurationMs(
|
||||
performance.now() - sessionLookupStart
|
||||
),
|
||||
redisLookupMs,
|
||||
sessionSource,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user