繁体   English   中英

Next auth v4 与 Next.js 中间件

[英]Next auth v4 with Next.js middleware

我正在使用 Next.js 和下一个身份验证 v4 进行凭据身份验证。

我想要做的是在中间件中为我的 API 调用添加全局验证,以便在 API 调用 session 之前进行测试。 If the session is not null the call have to passed successfully, else if the session is null then handle an unauthorized error message and redirect to login page.

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

这是我的代码: [...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
注意:中间件文件可以单独设置在路径中,更多检查请查看执行顺序

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文件数据库: 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"
  }
}

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

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

/package.json

{
  "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"
  }
}

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

/中间件.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();
}

/services/jwt_sign_verify.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;
}

/pages/api/auth/login.ts

/* eslint-disable import/no-anonymous-default-export */
import { serialize } from "cookie";
import { sign } from "../../../services/jwt_sign_verify";

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

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

// Check-in the database for a match,
//serialize and check your data before doing any operations.
//This IF statement is for checking demo purposes only.
    if (username === "Admin" && password === "Admin") {
        const token = await sign(
            "testing", //do some magic here
            secret
        );

        const serialised = serialize("OursiteJWT", token, {
            httpOnly: true,
            secure: process.env.NODE_ENV !== "development",
            sameSite: "strict",
            maxAge: 60 * 60 * 24 * 30,
            path: "/",
        });

        res.setHeader("Set-Cookie", serialised);

        res.status(200).json({ message: "Success!" });
    } else {
        res.status(401).json({ message: "Invalid credentials!" });
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM