七、Authentication

郁子大约 5 分钟约 1508 字笔记NextJSCodevolution

(一)Authentication in Next.js

  • 任何项目都会涉及到用户,需要考虑身份和权限
    • Identity 身份:验证用户是谁 —— Authentication 认证
    • Access 权限:验证用户有什么许可 —— Authorization 授权

1.分类

  • Client-side authentication
  • Server-side authentication
  • API routes authentication

2. 如何从以上三方面保护用户数据?

  • 不需要持久化:使用 GitHub、Facebook 等认证服务,以确保用户的身份验证
  • 需要持久化:使用数据库

(二)NextAuth Setup

1.components/Navbar.js

import Link from "next/link";

function Navbar() {
  return (
    <nav className="header">
      <h1 className="logo">
        <a href="#">NextAuth</a>
      </h1>
      <ul className={`main-nav`}>
        <li>
          <Link href="/">
            <a>Home</a>
          </Link>
        </li>
        <li>
          <Link href="/dashboard">
            <a>Dashboard</a>
          </Link>
        </li>
        <li>
          <Link href="/blog">
            <a>Blog</a>
          </Link>
        </li>
        <li>
          <Link href="#">
            <a>Sign In</a>
          </Link>
        </li>
        <li>
          <Link href="#">
            <a>Sign Out</a>
          </Link>
        </li>
      </ul>
    </nav>
  );
}

export default Navbar;

2.components/Navbar.css

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
h2,
h3,
a {
  color: #34495a;
}
a {
  text-decoration: none;
}
.logo {
  margin: 0;
  font-size: 1.45em;
}
.main-nav {
  display: flex;
  margin-top: 5px;
}
.logo a,
.main-nav a {
  padding: 10px 15px;
  text-transform: uppercase;
  text-align: center;
  display: block;
}
.main-nav a {
  color: #34495a;
  font-size: 0.99em;
}
.main-nav a:hover {
  color: #718daa;
}
.header {
  display: flex;
  justify-content: space-between;
  padding-top: 0.5em;
  padding-bottom: 0.5em;
  background-color: #f4f4f4;
  -webkit-box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
  -moz-box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
  box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
}

3.pages/_app.js

import Navbar from "../components/Navbar";
import "../styles/gloabals.css";
import "../components/Navbar.css";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Navbar />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

4.pages/dashboard.js

function Dashboard() {
  return <h1>Dashboard page</h1>;
}

export default Dashboard;

5.pages/blog.js

function Blog() {
  return <h1>Blog page</h1>;
}

export default Blog;

6.setup

yarn add next-auth

1)pages/api/auth/[...nextauth].js

import NextAuth from "next-auth";
import Providers from "next-auth/providers";

export default NextAuth({
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
});

2).env.local

GITHUB_ID=XXX
GITHUB_SECRET=XXXXX

(三)Sign In and Sign Out

1.components/Navbar.js

import Link from "next/link";
import { signIn, signOut } from "next-auth/client";

function Navbar() {
  return (
    <nav className="header">
      <h1 className="logo">
        <a href="#">NextAuth</a>
      </h1>
      <ul className={`main-nav`}>
        <li>
          <Link href="/">
            <a>Home</a>
          </Link>
        </li>
        <li>
          <Link href="/dashboard">
            <a>Dashboard</a>
          </Link>
        </li>
        <li>
          <Link href="/blog">
            <a>Blog</a>
          </Link>
        </li>
        <li>
          <Link href="/api/auth/signin">
            <a
              onClick={(e) => {
                e.preventDefault();
                {
                  /* signIn(); */
                }
                signIn("github");
              }}
            >
              Sign In
            </a>
          </Link>
        </li>
        <li>
          <Link href="/api/auth/signout">
            <a
              onClick={(e) => {
                e.preventDefault();
                signOut();
              }}
            >
              Sign Out
            </a>
          </Link>
        </li>
      </ul>
    </nav>
  );
}

export default Navbar;

(四)Client-side Authentication

1.components/Navbar.js

import Link from "next/link";
import { signIn, signOut, useSession } from "next-auth/client";

function Navbar() {
  const [session, loading] = useSession();
  return (
    <nav className="header">
      <h1 className="logo">
        <a href="#">NextAuth</a>
      </h1>
      <ul className={`main-nav ${!session && loading ? "loading" : "loaded"}`}>
        <li>
          <Link href="/">
            <a>Home</a>
          </Link>
        </li>
        <li>
          <Link href="/dashboard">
            <a>Dashboard</a>
          </Link>
        </li>
        <li>
          <Link href="/blog">
            <a>Blog</a>
          </Link>
        </li>
        {!loading && !session && (
          <li>
            <Link href="/api/auth/signin">
              <a
                onClick={(e) => {
                  e.preventDefault();
                  signIn("github");
                }}
              >
                Sign In
              </a>
            </Link>
          </li>
        )}
        {session && (
          <li>
            <Link href="/api/auth/signout">
              <a
                onClick={(e) => {
                  e.preventDefault();
                  signOut();
                }}
              >
                Sign Out
              </a>
            </Link>
          </li>
        )}
      </ul>
    </nav>
  );
}

export default Navbar;

2.components/Navbar.css

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
h2,
h3,
a {
  color: #34495a;
}
a {
  text-decoration: none;
}
.logo {
  margin: 0;
  font-size: 1.45em;
}
.main-nav {
  display: flex;
  margin-top: 5px;
}
.logo a,
.main-nav a {
  padding: 10px 15px;
  text-transform: uppercase;
  text-align: center;
  display: block;
}
.main-nav a {
  color: #34495a;
  font-size: 0.99em;
}
.main-nav a:hover {
  color: #718daa;
}
.header {
  display: flex;
  justify-content: space-between;
  padding-top: 0.5em;
  padding-bottom: 0.5em;
  background-color: #f4f4f4;
  -webkit-box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
  -moz-box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
  box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.75);
}
.loading {
  opacity: 0;
  transition: all 0.2s ease-in;
}
.loaded {
  opacity: 1;
  transition: all 0.2s ease-in;
}

(五)Securing Pages Client-side

1.pages/dashboard.js

  • useSession 返回的 loadingfalse => true,但是 sessionnull => undefined
    • 无法利用此数据判断用户是否有权限访问当前页面
  • 使用 getSession 判断用户是否登录
// import { useSession } from "next-auth/client";

import { useState, useEffect } from "react";
import { getSession, signIn } from "next-auth/client";

function Dashboard() {
  // const [session, loading] = useSession();
  // console.log({ session, loading });

  const [loading, setLoading] = useState(true);
  useEffect(() => {
    const securePage = async () => {
      const session = await getSession();
      if (!session) {
        signIn();
      } else {
        setLoading(false);
      }
    };
    securePage();
  }, []);

  if (loading) {
    return <h2>Loading...</h2>;
  }

  return <h1>Dashboard page</h1>;
}

export default Dashboard;

(六)NextAuth Provider

  • Provider 允许通过 useSession 跨组件使用 session
  • 推荐,因为页面刷新时 useSession 会自动检查用户是否有 session

1.pages/index.js

import { useSession } from "next-auth/client";

export default function Home() {
  const [session, loading] = useSession();
  console.log({ session, loading });
  <main>{session ? `${session.user.name}, ` : ""} Welcome to NextJS</main>;
}

2.pages/_app.js

import { Provider } from "next-auth/client";
import Navbar from "../components/Navbar";
import "../styles/gloabals.css";
import "../components/Navbar.css";

function MyApp({ Component, pageProps }) {
  return (
    <Provider>
      <Navbar />
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;

(七)Server-side Authentication

  • 无需从 props 中解构 session ,也无需判断 loading
  • 因为 SSR 的 session 一定会被加载,且可以全局访问

1.pages/blog.js

import { getSession, useSession } from "next-auth/client";

function Blog({ data }) {
  const [session] = useSession();
  console.log({ session });
  return <h1>Blog page - {data}</h1>;
}

export async function getServerSideProps(context) {
  const session = await getSession(context);
  return {
    props: {
      session,
      data: session ? "List of 100 personalized blogs" : "List of free blogs",
    },
  };
}

export default Blog;

2.pages/_app.js

import { Provider } from "next-auth/client";
import Navbar from "../components/Navbar";
import "../styles/gloabals.css";
import "../components/Navbar.css";

function MyApp({ Component, pageProps }) {
  return (
    <Provider session={pageProps.session}>
      <Navbar />
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;

(八) Securing Pages Server-side

  • 用户通过 url 直接访问无权限访问的页面时,跳转回首页

1.pages/blog.js

import { getSession, useSession } from "next-auth/client";

function Blog({ data }) {
  const [session] = useSession();
  console.log({ session });
  return <h1>Blog page - {data}</h1>;
}

export async function getServerSideProps(context) {
  const session = await getSession(context);

  if (!session) {
    return {
      redirect: {
        destination: "/api/auth/signin?calbackUrl=http://localhost:3000/blog",
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      data: session ? "List of 100 personalized blogs" : "List of free blogs",
    },
  };
}

export default Blog;

(九) Securing API Routes

1.pages/api/test-session.js

import { getSession } from "next-auth/client";

const handler = async (req, res) => {
  const session = await getSession({ req });
  if (!session) {
    res.status(401).json({ error: "Unauthenticated user" });
  } else {
    res.status(200).json({ message: "Success", session });
  }
};

export default handler;

(十) Connecting to a Database

yarn add mongodb

1..env.local

GITHUB_ID=XXX
GITHUB_SECRET=XXXXX

DB_USER=XXX
DB_PASSWORD=XXXXX
DB_URL=XXXXXXX

2.pages/api/auth/[...nextauth].js

import NextAuth from "next-auth";
import Providers from "next-auth/providers";

export default NextAuth({
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  database: process.env.DB_URL,
  session: {
    jwt: true,
  },
  jwt: {
    secret: "xxxxx",
  },
});

(十一)Callbacks

1.pages/api/auth/[...nextauth].js

import NextAuth from "next-auth";
import Providers from "next-auth/providers";

export default NextAuth({
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  database: process.env.DB_URL,
  session: {
    jwt: true,
  },
  jwt: {
    secret: "xxxxx",
  },
  callbacks: {
    // token创建和更新时自动调用,可在此自定义格式
    async jwt(token, user) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session(session, token) {
      session.user.id = token.id;
      return session;
    },
  },
});

(十二)Summary

  • 使用 useSession 钩子函数进行客户端身份验证
  • 使用 getSession 函数从客户端层面限制访问页面
  • 当在 getServerSideProps 中使用 getSession 函数进行服务端身份验证时, next-authProvider 可以提高性能,减少网络调用并避免页面闪烁
  • 通过从 getServerSideProps 中重定向来从服务端层面限制访问页面
  • 使用 getSession 函数可用于验证 API 路由权限
  • Callbacks 允许自定义 session 对象

(十三)Deploying Next.js Apps to Vercel

上次编辑于: