157 lines
3.9 KiB
TypeScript
157 lines
3.9 KiB
TypeScript
"use server"
|
||
|
||
import { Base64 } from "js-base64"
|
||
import { headers } from "next/headers"
|
||
import redis from "./redis"
|
||
|
||
// 生成加密密钥
|
||
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)
|
||
}
|
||
|
||
// 校验用户信息
|
||
export async function genAccessToken(req: any) {
|
||
try {
|
||
// 校验用户信息
|
||
// const fingerprint = req.headers.get("Fingerprint")
|
||
const sessionData = req.cookies.get("session")?.value
|
||
|
||
// 无session 直接重定向到登录页面
|
||
if (!sessionData) {
|
||
return "客户端请求中,未包含用户标识"
|
||
}
|
||
|
||
let redisKey = JSON.parse(sessionData)
|
||
|
||
const key = await generateKeyFromEnv(process.env.ENCRYPTION_KEY || "")
|
||
const decryptedSessionData = JSON.parse(await decrypt(redisKey, key))
|
||
const fingerprint = JSON.parse(decryptedSessionData)?.fingerprint
|
||
|
||
// 获取用户信息
|
||
let userData: any = await redis.get(redisKey)
|
||
|
||
userData = JSON.parse(userData)
|
||
|
||
// redis中无用户信息 直接重定向到登录页面
|
||
if (!userData) {
|
||
return "未获取到用户信息"
|
||
}
|
||
|
||
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不匹配
|
||
|
||
if (
|
||
clientIP !== userData?.clientIP ||
|
||
fingerprint !== userData?.fingerprint
|
||
) {
|
||
return `客户端信息与缓存用户信息不一致 \n 客户端: ${clientIP}, ${fingerprint} | redis存储: ${userData?.clientIP}, ${userData?.fingerprint}`
|
||
}
|
||
|
||
return {
|
||
...userData,
|
||
redisKey: sessionData,
|
||
}
|
||
} catch (err: any) {
|
||
return `获取用户信息报错, 报错原因:${err?.message}`
|
||
}
|
||
}
|