feat(project): init
This commit is contained in:
21
components/login/libs/common.ts
Normal file
21
components/login/libs/common.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const validatePhoneNumber = (str: string) => {
|
||||
const reg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/
|
||||
return reg.test(str)
|
||||
}
|
||||
|
||||
// 获取url中的某个参数值
|
||||
export const getQueryVariable = (variable: string) => {
|
||||
const query = window.location.search.substring(1)
|
||||
const vars = query.split("&")
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split("=")
|
||||
if (pair[0] === variable) {
|
||||
return pair[1]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const APP = "basis"
|
||||
export const PLATFORM = "basis"
|
||||
export const TENANT = "cowarobot"
|
||||
93
components/login/libs/cookie.ts
Normal file
93
components/login/libs/cookie.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { ResultData } from "@/api/typing"
|
||||
import redis from "@/components/login/libs/redis"
|
||||
import { encrypt, generateKeyFromEnv } from "@/components/login/libs/session"
|
||||
import { cookies } from "next/headers"
|
||||
import { Login } from "@/components/login/api/types"
|
||||
|
||||
export const handleLoginData = async (props: {
|
||||
clientIP: string
|
||||
fingerprint: string
|
||||
data: ResultData<Login.ResLogin>
|
||||
}) => {
|
||||
const { clientIP, fingerprint, data } = props
|
||||
const sessionKey = {
|
||||
clientIP,
|
||||
fingerprint: fingerprint,
|
||||
}
|
||||
const cookieStore = await cookies()
|
||||
|
||||
const oldSession = cookieStore.get("session")?.value || ""
|
||||
if (oldSession) {
|
||||
redis.del(JSON.parse(oldSession) as any)
|
||||
}
|
||||
|
||||
const key = await generateKeyFromEnv(process.env.ENCRYPTION_KEY || "")
|
||||
const encryptedSessionData = await encrypt(JSON.stringify(sessionKey), key)
|
||||
|
||||
// save redis
|
||||
await redis.set(
|
||||
encryptedSessionData,
|
||||
JSON.stringify({
|
||||
clientIP,
|
||||
fingerprint: fingerprint,
|
||||
user_name: data?.message?.user_info?.user_name,
|
||||
access_token: data.message.access_token,
|
||||
refresh_token: data.message.refresh_token,
|
||||
}),
|
||||
"EX",
|
||||
172800
|
||||
)
|
||||
|
||||
// set cookie
|
||||
cookieStore.set("session", JSON.stringify(encryptedSessionData), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NEXT_PUBLIC_COOKIE_ENV === "production",
|
||||
maxAge: 60 * 60 * 24 * 30, // 30天
|
||||
path: "/",
|
||||
})
|
||||
return {
|
||||
...data,
|
||||
message: {
|
||||
...data.message,
|
||||
access_token: "",
|
||||
refresh_token: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const handleRefreshToken = async (props: {
|
||||
data: ResultData<Login.ResRefreshKey>
|
||||
userData: any
|
||||
}) => {
|
||||
const { userData, data } = props
|
||||
const { redisKey, ...rest } = userData
|
||||
await redis.set(
|
||||
JSON.parse(redisKey),
|
||||
JSON.stringify({
|
||||
...rest,
|
||||
access_token: data?.message?.access,
|
||||
refresh_token: data?.message?.refresh,
|
||||
}),
|
||||
"EX",
|
||||
172800
|
||||
)
|
||||
return {
|
||||
...data,
|
||||
message: {
|
||||
...data.message,
|
||||
access_token: "",
|
||||
refresh_token: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const handleDeleteCookie = async () => {
|
||||
const cookieStore = await cookies()
|
||||
const sessionData = cookieStore.get("session")?.value || ""
|
||||
|
||||
// 删cookie和redis中用户
|
||||
if (sessionData) {
|
||||
await redis.del(JSON.parse(sessionData) as any)
|
||||
cookieStore.delete("session")
|
||||
}
|
||||
}
|
||||
23
components/login/libs/redis.ts
Normal file
23
components/login/libs/redis.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Redis from "ioredis"
|
||||
|
||||
const redisConfig = {
|
||||
port: process.env.NEXT_PUBLIC_REDIS_PORT
|
||||
? Number(process.env.NEXT_PUBLIC_REDIS_PORT)
|
||||
: 6379, // Redis port
|
||||
host: process.env.NEXT_PUBLIC_REDIS_URL || "", // Redis host
|
||||
username: process.env.NEXT_PUBLIC_REDIS_USERNAME || "", // needs Redis >= 6
|
||||
password: process.env.NEXT_PUBLIC_REDIS_PASSWORD || "",
|
||||
db: process.env.NEXT_PUBLIC_REDIS_DB
|
||||
? Number(process.env.NEXT_PUBLIC_REDIS_DB)
|
||||
: 0, // Defaults to 0
|
||||
}
|
||||
|
||||
const redis = new Redis({
|
||||
port: redisConfig.port, // Redis port
|
||||
host: redisConfig.host, // Redis host
|
||||
username: redisConfig.username, // needs Redis >= 6
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db, // Defaults to 0
|
||||
})
|
||||
|
||||
export default redis
|
||||
156
components/login/libs/session.ts
Normal file
156
components/login/libs/session.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
"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}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user