带有 Next.js 中间件的下一个 auth v4

Next auth v4 with Next.js middleware

我正在使用 Next.js 和 next auth v4 以使用凭据进行身份验证。

我想做的是在中间件中为我的 API 调用添加一个全局验证,以便在 API 调用会话之前进行测试。如果会话不为空,调用必须成功通过,否则如果会话为空,则处理未经授权的错误消息并重定向到登录页面。

我还想为登录页面和其他不需要检查身份验证的页面添加受保护的路由和不受保护的路由。

这是我的代码: [...nextauth].js

import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials";
import api from './api'

export default NextAuth({
    providers: [
        CredentialsProvider({
          name: "Credentials",
          async authorize(credentials, req) {
            const {username,password} = credentials    
            const user = await api.auth({
                username,
                password,
            })

            if (user) {
              return user
            } else {
              return null
              
            }
          }
        })
    ],
    callbacks: {
        async jwt({ token, user, account }) {
            let success = user?.id > 0
            if (account && success) {
                return {
                ...token,
                user : user ,
                accessToken: user.id            
              };
            }
            return token;
        },
    
        async session({ session, token }) {   
          session.user = token;  
          return session;
        },
      },
    secret: "test",
    jwt: {
        secret: "test",
        encryption: true,
    }, 
    pages: {
        signIn: "/Login",
    },
})

我的_middleware.js

import { getSession } from "next-auth/react"
import { NextResponse } from "next/server"

/** @param {import("next/server").NextRequest} req */

export async function middleware(req) {
  // return early if url isn't supposed to be protected
   // Doesn't work here 
  if (req.url.includes("/Login")) {
    return NextResponse.next()
  }

  const session = await getSession({req})
  // You could also check for any property on the session object,
  // like role === "admin" or name === "John Doe", etc.
  if (!session) return NextResponse.redirect("/Login")

  // If user is authenticated, continue.
  return NextResponse.next()
}

我想提一下,这些技术可以根据情况进行改进,也可以迁移到 TypeScript,我将在以后的编辑中跟进,希望这可能有所帮助。

我通过以下方式让它工作:

文件: pages/admin/_middleware.js
注意:中间件文件可以在路径中单独设置,更多检查请查看execution order

import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token }) => token?.userRole === "admin",
  },
})

文件: api/auth/[...nextauth].js

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        const res = await fetch("http://localhost:3000/api/auth/getuser", {
          method: 'POST',
          body: JSON.stringify(credentials),
          headers: { "Content-Type": "application/json" }
        })
        const user = await res.json()

        // If no error and we have user data, return it
        if (res.ok && user) {
          return user;
        }
        return null
      }
    })
  ],
  secret: process.env.JWT_SECRET,
  callbacks: {
    async jwt({token, user, account}) {
      if (token || user) {
        token.userRole = "admin";
        return {...token};
      }
    },
  },
})

文件: api/auth/getuser.js

//YOUR OWN DATABASE
import { sql_query } from '@project/utils/db';

export default async function handler(req,res) {
  let username = req.body.username;
  let password = req.body.password;

  let isJSON = req.headers['content-type'] == "application/json";
  let isPOST = req.method === "POST";

  let fieldsExisting = password && username;

  if (isPOST && isJSON && fieldsExisting) {
    const { createHmac } = await import('crypto');

//This will require to have password field in database set as md5
//you can also have it as simple STRING, depends on preferences
    const hash = createHmac('md5', password ).digest('hex'); 

//YOUR OWN DATABASE
    const query = `SELECT * FROM users WHERE email='${username}' AND password='${hash}' LIMIT 1;`;

    let results = await sql_query(query);
    if (results == undefined) {
      res.status(404).json({ "error": "Not found" });
    } else {
      res.status(200).json({ "username": results[0].nume });
    }
  } else {
    res.status(500).json({ "error": "Invalid request type" });
  }
}

//YOUR OWN DATABASE 用于 FILE: utils/db:

import mysql from "serverless-mysql";

export const db = mysql({
  config: {
    host: process.env.MYSQL_HOST,
    database: process.env.MYSQL_DATABASE,
    user: process.env.MYSQL_USERNAME,
    password: process.env.MYSQL_PASSWORD,
  },
});

export async function sql_query(query_string values = []) {
  try {
    const results = await db.query(query_string, values);
    await db.end();
    return results;
  } catch (e) {
    if (typeof e === "string") {
      e.toUpperCase() // works, `e` narrowed to string
    } else if (e instanceof Error) {
      e.message // works, `e` narrowed to Error
    }
  }
}

文件: .env -- 注意: CHANGE .env variables with your own

NEXTAUTH_URL=http://localhost:3000
MYSQL_HOST="0.0.0.0"
MYSQL_DATABASE="randomNAME"
MYSQL_USERNAME="randomNAME"
MYSQL_PASSWORD="randomPASS"
NEXTAUTH_SECRET="49dc52e6bf2abe5ef6e2bb5b0f1ee2d765b922ae6cc8b95d39dc06c21c848f8c"
JWT_SECRET="49dc52e6bf2abe5ef6e2bb5b0f1ee2d765b922ae6cc8b95d39dc06c21c848f8c"

文件: package.json

{
  "name": "MyAwesomeName",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.0.9",
    "next-auth": "^4.2.0",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "serverless-mysql": "^1.5.4",
    "swr": "^0.4.2"
  },
  "devDependencies": {
    "@types/node": "17.0.12",
    "@types/react": "17.0.38",
    "eslint": "8.7.0",
    "eslint-config-next": "12.0.9",
    "typescript": "4.5.5"
  }
}