feat: 添加 cookie 认证支持,增强中间件的身份验证功能
This commit is contained in:
parent
17a1acfe13
commit
fd68c8a452
@ -2,7 +2,8 @@ export const config = {
|
||||
port: process.env.PORT || 3000,
|
||||
auth: {
|
||||
username: process.env.AUTH_USERNAME || 'admin',
|
||||
password: process.env.AUTH_PASSWORD || 'password'
|
||||
password: process.env.AUTH_PASSWORD || 'password',
|
||||
secret: process.env.AUTH_SECRET || 'winupdate-neo-secret-key'
|
||||
},
|
||||
// For file upload settings
|
||||
upload: {
|
||||
|
||||
119
middleware.ts
119
middleware.ts
@ -1,7 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { config } from './lib/config'
|
||||
import { config as appConfig } from './lib/config'
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const AUTH_COOKIE_NAME = 'auth_token'
|
||||
const SECRET_KEY = appConfig.auth.secret
|
||||
|
||||
// Helper to generate a random nonce
|
||||
function generateNonce() {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
|
||||
// Helper to create a signed token (simplified JWT-like structure)
|
||||
async function createToken(username: string) {
|
||||
const header = { alg: 'HS256', typ: 'JWT' }
|
||||
const payload = {
|
||||
sub: username,
|
||||
iat: Date.now(),
|
||||
exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
||||
nonce: generateNonce() // Ensure token is different every time
|
||||
}
|
||||
|
||||
const encodedHeader = btoa(JSON.stringify(header))
|
||||
const encodedPayload = btoa(JSON.stringify(payload))
|
||||
const data = `${encodedHeader}.${encodedPayload}`
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(appConfig.auth.secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
)
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
key,
|
||||
new TextEncoder().encode(data)
|
||||
)
|
||||
|
||||
// Convert signature to base64
|
||||
const signatureArray = Array.from(new Uint8Array(signature))
|
||||
const encodedSignature = btoa(String.fromCharCode.apply(null, signatureArray))
|
||||
|
||||
return `${data}.${encodedSignature}`
|
||||
}
|
||||
|
||||
// Helper to verify token
|
||||
async function verifyToken(token: string) {
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) return false
|
||||
|
||||
const [encodedHeader, encodedPayload, encodedSignature] = parts
|
||||
const data = `${encodedHeader}.${encodedPayload}`
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(SECRET_KEY),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
)
|
||||
|
||||
const signature = new Uint8Array(
|
||||
atob(encodedSignature).split('').map(c => c.charCodeAt(0))
|
||||
)
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
'HMAC',
|
||||
key,
|
||||
signature,
|
||||
new TextEncoder().encode(data)
|
||||
)
|
||||
|
||||
if (!isValid) return false
|
||||
|
||||
const payload = JSON.parse(atob(encodedPayload))
|
||||
if (payload.exp < Date.now()) return false
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
// Skip auth for certain paths (same logic as original Express app)
|
||||
@ -21,7 +103,16 @@ export function middleware(req: NextRequest) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// For all other paths, require authentication
|
||||
// 1. Try Cookie Authentication
|
||||
const authCookie = req.cookies.get(AUTH_COOKIE_NAME)
|
||||
if (authCookie) {
|
||||
const isValid = await verifyToken(authCookie.value)
|
||||
if (isValid) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to Basic Auth
|
||||
const authHeader = req.headers.get('authorization')
|
||||
|
||||
if (!authHeader) {
|
||||
@ -34,11 +125,25 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString()
|
||||
// Use atob for Edge compatibility
|
||||
const authValue = authHeader.split(' ')[1]
|
||||
const auth = atob(authValue)
|
||||
const [username, password] = auth.split(':')
|
||||
|
||||
if (username === config.auth.username && password === config.auth.password) {
|
||||
return NextResponse.next()
|
||||
if (username === appConfig.auth.username && password === appConfig.auth.password) {
|
||||
const response = NextResponse.next()
|
||||
|
||||
// Set auth cookie on successful Basic Auth
|
||||
const token = await createToken(username)
|
||||
response.cookies.set(AUTH_COOKIE_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24, // 1 day
|
||||
path: '/'
|
||||
})
|
||||
|
||||
return response
|
||||
} else {
|
||||
return new NextResponse('Authentication failed', {
|
||||
status: 401,
|
||||
@ -58,7 +163,7 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
// 配置中间件匹配的路径
|
||||
export const config_middleware = {
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user