使用 Next.js 中间件的 Next auth v4

sok*_*ida 7 javascript authorization next.js next-auth

我使用 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",
    },
})
Run Code Online (Sandbox Code Playgroud)

我的_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()
}
Run Code Online (Sandbox Code Playgroud)

Phi*_* D. 16

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

我通过以下方式使其发挥作用:

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

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

export default withAuth({
  callbacks: {
    authorized: ({ token }) => token?.userRole === "admin",
  },
})
Run Code Online (Sandbox Code Playgroud)

文件: 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};
      }
    },
  },
})
Run Code Online (Sandbox Code Playgroud)

文件: 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" });
  }
}
Run Code Online (Sandbox Code Playgroud)

对于//YOUR OWN DATABASEFILE : 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
    }
  }
}

Run Code Online (Sandbox Code Playgroud)

文件: .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"
Run Code Online (Sandbox Code Playgroud)

文件: package.json

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"
Run Code Online (Sandbox Code Playgroud)

NextJS 12.2.0 中间件
的编辑:01/07/2022 正如我所提到的,我将跟进TypeScript的编辑,并且与 NextJS 12.2.0 版本完美契合。
我还想提一下:

根据与 GitHub 中用户的讨论,显然jose库在中间件中运行 Edge 函数时效果更好,而jsonwebtoken则不然。这是基于SO问题
这些文件应如下所示:

/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"
  }
}
Run Code Online (Sandbox Code Playgroud)

/pages/_middleware已移动到/middleware,基本上在根文件夹中,其中将包含以下内容:

/中间件.ts

{
  "name": "xyz123",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@emotion/react": "^11.9.3",
    "@emotion/styled": "^11.9.3",
    "@mui/material": "^5.8.6",
    "@prisma/client": "^4.0.0",
    "axios": "^0.27.2",
    "jose": "^4.8.3",
    "next": "12.2.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hook-form": "^7.33.0"
  },
  "devDependencies": {
    "@types/node": "18.0.0",
    "@types/react": "18.0.14",
    "@types/react-dom": "18.0.5",
    "eslint": "8.18.0",
    "eslint-config-next": "12.2.0",
    "prisma": "^4.0.0",
    "typescript": "4.7.4"
  }
}

Run Code Online (Sandbox Code Playgroud)

/services/jwt_sign_verify.ts

import { NextResponse } from "next/server";
import type { NextRequest } from 'next/server'
import { verify } from "./services/jwt_sign_verify";

const secret = process.env.SECRET || "secret";

export default async function middleware(req: NextRequest) {
  const jwt = req.cookies.get("OutsiteJWT");
  const url = req.url;
  const {pathname} = req.nextUrl;

  if (pathname.startsWith("/dashboard")) {
    if (jwt === undefined) {
      req.nextUrl.pathname = "/login";
      return NextResponse.redirect(req.nextUrl);
    }

    try {
      await verify(jwt, secret);
      return NextResponse.next();
    } catch (error) {
      req.nextUrl.pathname = "/login";
      return NextResponse.redirect(req.nextUrl);
    }
  }

  return NextResponse.next();
}
Run Code Online (Sandbox Code Playgroud)

/pages/api/auth/login.ts

import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
import { Token } from "@typescript-eslint/types/dist/generated/ast-spec";

export async function sign(payload: string, secret: string): Promise<string> {
    const iat = Math.floor(Date.now() / 1000);
    const exp = iat + 60 * 60; // one hour

    return new SignJWT({ payload })
        .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
        .setExpirationTime(exp)
        .setIssuedAt(iat)
        .setNotBefore(iat)
        .sign(new TextEncoder().encode(secret));
}

export async function verify(token: string, secret: string): Promise<JWTPayload> {
    const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
    // run some checks on the returned payload, perhaps you expect some specific values

    // if its all good, return it, or perhaps just return a boolean
    return payload;
}
Run Code Online (Sandbox Code Playgroud)