React + NextJS - 受保护的路由

18

目标:如果已登录用户尝试手动访问/auth/signin,则希望将其重定向到主页。

登录页面/组件:

const Signin = ({ currentUser }) => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const { doRequest, errors } = useRequest({
        url: '/api/users/signin',
        method: 'post',
        body: {
            email, password
        },
        onSuccess: () => Router.push('/')
    });

    useEffect(() => {
        const loggedUser = () => {
            if (currentUser) {
                Router.push('/');
            }
        };
        loggedUser();
    }, []);

自定义 _app 组件:

const AppComponent = ({ Component, pageProps, currentUser }) => {
    return (
        <div>
            <Header currentUser={currentUser} />
            <Component {...pageProps} currentUser={currentUser} />
        </div>

    )
};

AppComponent.getInitialProps = async (appContext) => {
    const client = buildClient(appContext.ctx);
    const { data } = await client.get('/api/users/currentuser');
    let pageProps = {};
    if (appContext.Component.getInitialProps) {
        pageProps = await appContext.Component.getInitialProps(appContext.ctx);
    }
    return {
        pageProps,
        ...data
    }
};

export default AppComponent;

问题

我尝试过这个方法,但这会导致轻微的延迟,因为它不会在服务器端渲染。 所谓的延迟是指:在重定向之前,它会显示我不想要显示的页面大约一秒钟左右。

我可以使用加载符号或一堆if else条件,但那只是一个变通方法,什么是处理这个问题的最佳方法/做法


我想出了另一个解决方案:

  • 我建立了一个重定向钩子:
import Router from 'next/router';
export default (ctx, target) => {
    if (ctx.res) {
        // server 
        ctx.res.writeHead(303, { Location: target });
        ctx.res.end();
    } else {
        // client
        Router.push(target);
    }
}
  • 然后我创建了两个HOC(用于已登录和未登录用户)用于保护路由:
import React from 'react';
import redirect from './redirect';
const withAuth = (Component) => {
    return class AuthComponent extends React.Component {
        static async getInitialProps(ctx, { currentUser }) {
            if (!currentUser) {
                return redirect(ctx, "/");
            }
        }
        render() {
            return <Component {...this.props} />
        }
    }
}
export default withAuth;
  • 然后我用它来包裹组件,以保护页面:
  • 然后我用它来包裹组件,以保护页面:
  • export default withAuth(NewTicket);
    

    有没有更好的方法来处理这个问题?非常感谢帮助。


    请查看此链接:https://medium.com/@eslamifard.ali/how-to-simply-create-a-private-route-in-next-js-38cab204a99c - Ali Eslamifard
    8个回答

    13

    答案

    我真的建议看一下示例,看看NextJS建议如何处理这个问题。资源非常好!

    https://github.com/vercel/next.js/tree/master/examples

    例如,您可以使用开源认证选项next-auth
    这里是示例。https://github.com/vercel/next.js/tree/master/examples/with-next-auth
    // _app.js
    import { Provider } from 'next-auth/client'
    import '../styles.css'
    
    const App = ({ Component, pageProps }) => {
      const { session } = pageProps
      return (
        <Provider options={{ site: process.env.SITE }} session={session}>
          <Component {...pageProps} />
        </Provider>
      )
    }
    
    export default App
    

    // /pages/api/auth/[...nextauth].js
    import NextAuth from 'next-auth'
    import Providers from 'next-auth/providers'
    
    const options = {
      site: process.env.VERCEL_URL,
      providers: [
        Providers.Email({
          // SMTP connection string or nodemailer configuration object https://nodemailer.com/
          server: process.env.EMAIL_SERVER,
          // Email services often only allow sending email from a valid/verified address
          from: process.env.EMAIL_FROM,
        }),
        // When configuring oAuth providers make sure you enabling requesting
        // permission to get the users email address (required to sign in)
        Providers.Google({
          clientId: process.env.GOOGLE_ID,
          clientSecret: process.env.GOOGLE_SECRET,
        }),
        Providers.Facebook({
          clientId: process.env.FACEBOOK_ID,
          clientSecret: process.env.FACEBOOK_SECRET,
        }),
        Providers.Twitter({
          clientId: process.env.TWITTER_ID,
          clientSecret: process.env.TWITTER_SECRET,
        }),
        Providers.GitHub({
          clientId: process.env.GITHUB_ID,
          clientSecret: process.env.GITHUB_SECRET,
        }),
      ],
      // The 'database' option should be a connection string or TypeORM
      // configuration object https://typeorm.io/#/connection-options
      //
      // Notes:
      // * You need to install an appropriate node_module for your database!
      // * The email sign in provider requires a database but OAuth providers do not
      database: process.env.DATABASE_URL,
    
      session: {
        // Use JSON Web Tokens for session instead of database sessions.
        // This option can be used with or without a database for users/accounts.
        // Note: `jwt` is automatically set to `true` if no database is specified.
        // jwt: false,
        // Seconds - How long until an idle session expires and is no longer valid.
        // maxAge: 30 * 24 * 60 * 60, // 30 days
        // Seconds - Throttle how frequently to write to database to extend a session.
        // Use it to limit write operations. Set to 0 to always update the database.
        // Note: This option is ignored if using JSON Web Tokens
        // updateAge: 24 * 60 * 60, // 24 hours
        // Easily add custom properties to response from `/api/auth/session`.
        // Note: This should not return any sensitive information.
        /*
        get: async (session) => {
          session.customSessionProperty = "ABC123"
          return session
        }
        */
      },
    
      // JSON Web Token options
      jwt: {
        // secret: 'my-secret-123', // Recommended (but auto-generated if not specified)
        // Custom encode/decode functions for signing + encryption can be specified.
        // if you want to override what is in the JWT or how it is signed.
        // encode: async ({ secret, key, token, maxAge }) => {},
        // decode: async ({ secret, key, token, maxAge }) => {},
        // Easily add custom to the JWT. It is updated every time it is accessed.
        // This is encrypted and signed by default and may contain sensitive information
        // as long as a reasonable secret is defined.
        /*
        set: async (token) => {
          token.customJwtProperty = "ABC123"
          return token
        }
        */
      },
    
      // Control which users / accounts can sign in
      // You can use this option in conjunction with OAuth and JWT to control which
      // accounts can sign in without having to use a database.
      allowSignin: async (user, account) => {
        // Return true if user / account is allowed to sign in.
        // Return false to display an access denied message.
        return true
      },
    
      // You can define custom pages to override the built-in pages
      // The routes shown here are the default URLs that will be used.
      pages: {
        // signin: '/api/auth/signin',  // Displays signin buttons
        // signout: '/api/auth/signout', // Displays form with sign out button
        // error: '/api/auth/error', // Error code passed in query string as ?error=
        // verifyRequest: '/api/auth/verify-request', // Used for check email page
        // newUser: null // If set, new users will be directed here on first sign in
      },
    
      // Additional options
      // secret: 'abcdef123456789' // Recommended (but auto-generated if not specified)
      // debug: true, // Use this option to enable debug messages in the console
    }
    
    const Auth = (req, res) => NextAuth(req, res, options)
    
    export default Auth
    

    所以上述选项肯定是服务器端渲染应用程序,因为我们使用/auth路径进行身份验证。如果您想要无服务器解决方案,则可能需要将/api路径中的所有内容提取到lambda(AWS Lambda)+网关API(AWS API Gateway)中。那里你只需要一个连接到该api的自定义钩子。当然,您也可以以不同的方式实现这一点。
    这是另一个使用firebase的身份验证示例。

    https://github.com/vercel/next.js/tree/master/examples/with-firebase-authentication

    还有一个使用Passport.js的例子

    https://github.com/vercel/next.js/tree/master/examples/with-passport

    你也问到了加载行为,这个例子可能会对你有所帮助

    https://github.com/vercel/next.js/tree/master/examples/with-loading

    观点

    自定义_app组件通常是顶级包装器(不完全是最顶层的_document适用于该描述)。

    现实情况下,我会在_app下创建一个登录组件。通常我会在布局组件中实现该模式,或者像上面的示例一样使用api路径或实用程序函数。


    2
    我通常也这样做,使用 withAuth HOC 导出布局组件。 - Nico
    1
    如果您的登录是一个模态框,如何在next-auth中使用它?因为我正在使用next-auth的signIn函数,它将我推向credential-signIn页面? - Mark Sparrow

    6

    下面是使用自定义“hook”与getServerSideProps的示例。

    我正在使用react-query,但您可以使用任何数据获取工具。

    // /pages/login.jsx
    
    import SessionForm from '../components/SessionForm'
    import { useSessionCondition } from '../hooks/useSessionCondition'
    
    export const getServerSideProps = useSessionCondition(false, '/app')
    
    const Login = () => {
        return (
            <SessionForm isLogin/>
        )
    }
    
    export default Login
    
    

    // /hooks/useSessionCondition.js
    
    import { QueryClient } from "react-query";
    import { dehydrate } from 'react-query/hydration'
    import { refreshToken } from '../utils/user_auth';
    
    export const useSessionCondition = (
        sessionCondition = true, // whether the user should be logged in or not
        redirect = '/' // where to redirect if the condition is not met
    ) => {
    
        return async function ({ req, res }) {
            const client = new QueryClient()
            await client.prefetchQuery('session', () => refreshToken({ headers: req.headers }))
            const data = client.getQueryData('session')
    
            if (!data === sessionCondition) {
                return {
                    redirect: {
                        destination: redirect,
                        permanent: false,
                    },
                }
            }
    
            return {
                props: {
                    dehydratedState: JSON.parse(JSON.stringify(dehydrate(client)))
                },
            }
        }
    
    }
    

    3
    export const getServerSideProps = wrapper.getServerSideProps(
      (store) =>
        async ({ req, params }) => {
          const session = await getSession({ req });
    
          if (!session) {
            return {
              redirect: {
                destination: '/',
                permanent: false,
              },
            };
          }
        }
    );
    

    在 Next 9++ 中,您可以这样做:只需检查会话,如果没有会话,我们就可以返回一个重定向,并将目的地设置为路由用户到终点!


    3

    将Next.js升级到9.3+版本,使用getServerSideProps替代getInitialProps。与getInitialProps不同,getServerSideProps仅在服务器端运行,永远都是在服务器端运行。如果验证失败,请从getServerSideProps进行重定向。


    2

    为了扩展@Nico在评论中所说的内容,这是我如何设置它的方式:Layout.tsx文件

    // ...
    import withAuth from "../utils/withAuth";
    
    interface Props {
      children?: ReactNode;
      title?: string;
    }
    
    const Layout = ({
      children,
      title = "This is the default title",
    }: Props): JSX.Element => (
      <>
        {children}
      </>
    );
    
    export default withAuth(Layout);
    

    还有withAuth.js文件

    import { getSession } from "next-auth/client";
    
    export default function withAuth(Component) {
      const withAuth = (props) => {
        return <Component {...props} />;
      };
    
      withAuth.getServerSideProps = async (ctx) => {
        return { session: await getSession(ctx) };
      };
    
      return withAuth;
    }
    

    如果认证来自后端会怎样? - Alan Yong

    1

    我曾经遇到过同样的问题,我采用了不闪烁内容的客户端解决方案,具体操作如下,请指出任何错误。

    我使用 useRouter

    //@/utils/ProtectedRoute
    import { useRouter } from "next/router";
    import { useState, useEffect } from "react";
    export const ProtectedRoute = ({ user = false, children }) => {
      const [login, setLogin] = useState(false);
      const router = useRouter();
    
      useEffect(() => {
        login && router.push("/account/login");//or router.replace("/account/login");
      }, [login]);
      useEffect(() => {
        !user && setLogin(true);
      }, []);
    
      return (
         <>
          {user ? (
            children
           ) : (
            <div>
              <h4>
                You are not Authorized.{" "}
                <Link href="/account/login">
                  <a>Please log in</a>
                </Link>
              </h4>
            </div>
          )}
        </>
      };
    )
    

    当我想要保护一个路由时,我使用以下语法:
    import { ProtectedRoute } from "@/utils/ProtectedRoute";
    const ProtectedPage = () => {
      const user = false;
      return (
        <ProtectedRoute user={user}>
          <h1>Protected Content</h1>
        </ProtectedRoute>
      );
    };
    
    export default ProtectedPage;
    

    1
    // Authentication.js
    
    import { useRouter } from "next/router";
    import React, { useEffect } from "react";
    
    function Authentication(props) {
      let userDetails;
      const router = useRouter();
      useEffect(() => {
        if (typeof window !== undefined) {
          userDetails=useSession
          if (!userDetails) {
            const path = router.pathname;
            switch (path) {
              case "/":
                break;
              case "/about":
                break;
              case "/contact-us":
                break;
              default:
                router.push("/");
            }
          } else if (userDetails) {
            if (router.pathname == "/") {
              router.push("/home");
            }
          }
        }
      }, []);
      return <>{props.children}</>;
    }
    
    export default Authentication;
    
    

    现在将以下代码添加到您的_app.js文件中。
     <DefaultLayout>
          <Authentication>
            <Component {...pageProps} />
          </Authentication>
     </DefaultLayout>
    

    现在一切都应该可以正常工作了,如果你想添加加载,则可以这样做。


    0
    Vercel最近推出了Next.js中间件。 Next.js中间件允许你在HTTP请求处理之前运行代码。
    要将你的中间件逻辑添加到你的应用程序中,请在你的Next.js项目的根目录中添加一个middleware.jsmiddleware.ts文件。
    export async function middleware(req: NextRequest) {
      const token = req.headers.get('token') // get token from request header
      const userIsAuthenticated = true // TODO: check if user is authenticated
    
      if (!userIsAuthenticated) {
        const signinUrl = new URL('/signin', req.url)
        return NextResponse.redirect(signinUrl)
      }
    
      return NextResponse.next()
    }
    
    // Here you can specify all the paths for which this middleware function should run
    // Supports both a single string value or an array of matchers
    export const config = {
      matcher: ['/api/auth/:path*'],
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接