diff --git a/lib/config.ts b/lib/config.ts index 174fc21..bbea8cb 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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: { diff --git a/middleware.ts b/middleware.ts index 69507f8..267c843 100644 --- a/middleware.ts +++ b/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: