如何为React和GraphQL应用程序实施身份验证和路由保护

来自菜鸟教程
跳转至:导航、​搜索

介绍

电子邮件、Facebook、Google、Twitter、Github 都是在您的 Web 应用程序中验证用户身份的可能选项。 使用 React 和 GraphQL 构建的应用程序同样适用于此类身份验证。

在本文中,您将学习如何使用以下方法将各种身份验证提供程序添加到 GraphQL 应用程序:

  • GraphQL:API 的查询语言
  • Graphcool:GraphQL 后端即服务
  • Auth0:身份验证即服务
  • React:用于构建用户界面的 JavaScript 库

如果用户未能通过我们的身份验证服务器的身份验证,我们还将学习如何保护 React 路由不被访问。

第 1 步——准备项目

计划是有一个 React 项目设置。 在设置 React 项目的同一目录中,我们可以配置一个 Graphcool 服务器并告诉我们的项目我们希望我们的服务器如何运行。 从安装 create-react-appgraphcool-framework CLI 工具开始:

npm install -g create-react-app graphcool-framework

然后使用 React CLI 工具搭建一个新的 React 应用程序:

create-react-app do-auth

通过移动到您刚刚创建的 React 应用程序并运行 Graphcool init 命令来创建 Graphcool 服务器:

cd do-auth
graphcool-framework init server

第 2 步 - 创建 React 路由和 UI

您需要公共路由和受保护路由:

  • 主页(公开)
  • 个人资料页面(受保护)
  • 管理页面(受保护且仅供管理员使用)
  • 关于页面(公共)

创建一个 containers 文件夹并添加 home.jsprofile.jsadmin.jsabout.js 作为文件来表示这些路由中的每一个。

主页.js

import React from 'react';
import Hero from '../components/hero';

const Home = (props) => (
     <div>
        <Hero page="Home"></Hero>
        <h2>Home page</h2>
      </div>
    )
    export default Home;

关于.js

import React from 'react';
import Hero from '../components/hero';

const About = (props) => (
     <div>
        <Hero page="About"></Hero>
        <h2>About page</h2>
      </div>
    )
    export default About;

Profile.js

import React from 'react';
import Hero from '../components/hero';

const Profile = props => (
     <div>
        <Hero page="Profile"></Hero>
        <h2>Profile page</h2>
      </div>
    );
    export default Profile;

管理员.js

import React from 'react';
import Hero from '../components/hero';

const Admin = (props) => (
     <div>
        <Hero></Hero>
        <Hero page="Admin"></Hero>
      </div>
    )
    export default Admin;

每个页面导入并使用Hero显示登陆英雄信息。 创建一个 components 文件夹并使用以下内容创建一个 Hero.js 文件:

import React from 'react';
import Nav from './nav'
import './hero.css'

const Hero = ({ page }) => (
     <section className="hero is-large is-dark">
        <div className="hero-body">
        <Nav></Nav>
          <div className="container">
            <h1 className="title">DO Auth</h1>
            <h2 className="subtitle">Welcome to the Auth {page}</h2>
          </div>
        </div>
      </section>
    );
    export default Hero;

您也可以在 components 文件夹中将导航组件添加为 nav.js。 在我们这样做之前,我们需要为 React 应用程序设置路由并将页面公开为路由。

从安装 React Router 库开始:

yarn add react-router-dom

接下来,通过index.js入口文件向App提供路由器:

    //...
import { BrowserRouter } from 'react-router-dom';
import App from './App';
    //...

ReactDOM.render(
    <BrowserRouter>
        <App />
      </BrowserRouter>,
      document.getElementById('root')
    );
    //...

然后在 App 组件中配置路由:

    import React, { Component } from 'react';
    import { Switch, Route } from 'react-router-dom';

    import Profile from './containers/profile';
    import About from './containers/about';
    import Admin from './containers/admin';
    import Home from './containers/home';

    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              <Route exact path="/about" component={About} />
              <Route exact path="/profile" component={Profile} />
              <Route exact path="/admin" component={Admin} />
            </Switch>
          </div>
        );
      }
    }
    export default App;

回到导航组件 (nav.js),我们想使用 react-router-dom 中的 Link 组件来提供导航:

    import React from 'react';
    import { Link } from 'react-router-dom'
    import './nav.css'
    const Nav = () => {
      return (
        <nav className="navbar">
          <div className="navbar-brand">
            <Link className="navbar-item" to="/">
              <strong>Auth Page</strong>
            </Link>
          </div>
          <div className="navbar-menu">
            <div className="navbar-end">
              <Link to="/about" className="navbar-item">
                About
              </Link>
              <Link to="/profile" className="navbar-item">
                Profile
              </Link>
              <div className="navbar-item join">
                Join
              </div>
            </div>
          </div>
        </nav>
      );
    };
    export default Nav;

接下来,创建一个 Graphcool 服务器

graphcool-framework deploy

第 3 步 — 为服务器身份验证配置 Auth0

让我们暂时离开客户端应用程序,回到我们之前创建的 Graphcool 服务器。 Graphcool 有一个无服务器功能概念,允许您扩展服务器的功能。 此功能可用于实现许多 3rd 方集成,包括身份验证。

此类集成的某些功能已为您预先打包,因此您不必从头开始创建它们。 您只需要安装模板,取消注释某些配置和类型,然后根据需要更新或调整代码。

让我们添加 Auth0 模板。 确保您在 server 文件夹中并运行以下命令:

graphcool-framework add-template graphcool/templates/auth/auth0

这将在 server/src 中创建一个 auth0 文件夹。 此文件夹包含触发此函数的函数逻辑、类型和突变定义。

接下来,您需要创建一个 Auth0 API,然后将该 API 的配置添加到您的服务器。 首先创建 一个帐户 ,然后从您的 API 仪表板创建一个新 API。 您可以随意命名 API。 提供对所有现有 API 唯一的标识符。

取消注释 server/src/graphcool.yml 中的模板配置并将其更新为如下所示:

    authenticate:
        handler:
          code:
            src: ./src/auth0/auth0Authentication.js
            environment:
              AUTH0_DOMAIN: [YOUR AUTH0 DOMAIN]
              AUTH0_API_IDENTIFIER: [YOUR AUTH0 IDENTIFIER]
        type: resolver
        schema: ./src/auth0/auth0Authentication.graphql

AUTH0_DOMAINAUTH0_API_IDENTIFIER 将作为环境变量在您的函数中的 process.env 上公开。

模板命令还会在 server/src/types.graphql 中生成一个类型。 它默认被注释掉。 您需要取消注释以下内容:

    type User @model {
      # Required system field:
      id: ID! @isUnique # read-only (managed by Graphcool)
      # Optional system fields (remove if not needed):
      createdAt: DateTime! # read-only (managed by Graphcool)
      updatedAt: DateTime! # read-only (managed by Graphcool)
      email: String
      auth0UserId: String @isUnique
    }

您需要删除创建服务器时生成的 User 类型,以便此 auth User 类型可以替换它。

接下来,您需要对身份验证逻辑进行一些调整。 找到以下代码块:

    jwt.verify(
            token,
            signingKey,
            {
              algorithms: ['RS256'],
              audience: process.env.AUTH0_API_IDENTIFIER,
              ignoreExpiration: false,
              issuer: `https://${process.env.AUTH0_DOMAIN}/`
            },
            (err, decoded) => {
              if (err) throw new Error(err)
              return resolve(decoded)
            }
          )

并将 verify 方法中的 audience 属性更新为 aud

    jwt.verify(
            token,
            signingKey,
            {
              algorithms: ['RS256'],
              aud: process.env.AUTH0_API_IDENTIFIER,
              ignoreExpiration: false,
              issuer: `https://${process.env.AUTH0_DOMAIN}/`
            },
            (err, decoded) => {
              if (err) throw new Error(err)
              return resolve(decoded)
            }
          )

最后,身份验证令牌将始终对电子邮件进行编码,因此无需执行以下操作即可获取电子邮件:

    let email = null
    if (decodedToken.scope.includes('email')) {
      email = await fetchAuth0Email(accessToken)
    }

我们可以马上收到来自decodedToken的邮件:

const email = decodedToken.email

这使得 fetchAuth0Email 功能无用,因此您可以将其删除。

通过运行以下命令将服务器部署到 Graphcool:

graphcool-framework

如果这是您第一次使用 Graphcool,您应该被带到一个页面以创建一个 Graphcool 帐户。

第 4 步 — 为客户端身份验证配置 Auth0

您的服务器设置为接收令牌以进行身份验证。 您可以通过运行以下命令在 Graphcool 操场上进行测试:

graphcool-framework playground

我们为突变提供一个 Auth0 令牌,并从 Graphcool 获得一个 节点令牌 。 让我们看看如何从 Auth0 获取令牌。

就像创建 API 一样,您还需要为项目创建客户端。 客户端用于从浏览器触发身份验证。 在 Auth0 仪表板导航中,单击 clients 并创建一个新客户端。

应用程序类型应设置为 单页 Web 应用程序 ,这就是路由的 React 应用程序。

启动身份验证后,它会重定向到您的 Auth0 域以验证用户。 验证用户后,它需要将用户重定向回您的应用程序。 回调 URL 是重定向后返回的位置。 进入刚刚创建的客户端的Settings选项卡,设置回调URL:

您现在已完成在 Auth0 仪表板上设置客户端配置。 我们要做的下一件事是在我们的 React 应用程序中创建一个服务,该服务公开一些方法。 这些方法将处理实用程序任务,例如触发身份验证、处理来自 Auth0 的响应、注销等。

首先,安装 Auth0 JS 库:

yarn add auth0-js

然后在src中创建一个services文件夹。 在新文件夹中添加一个 auth.js,内容如下:

    import auth0 from 'auth0-js';

    export default class Auth {
      auth0 = new auth0.WebAuth({
        domain: '[Auth0 Domain]',
        clientID: '[Auth0 Client ID]',
        redirectUri: 'http://localhost:3000/callback',
        audience: '[Auth0 Client Audience]',
        responseType: 'token id_token',
        scope: 'openid profile email'
      });
      handleAuthentication(cb) {
        this.auth0.parseHash({hash: window.location.hash}, (err, authResult) => {
          if (authResult && authResult.accessToken && authResult.idToken) {
            this.auth0.client.userInfo(authResult.accessToken, (err, profile) => {
              this.storeAuth0Cred(authResult, profile);
              cb(false, {...authResult, ...profile})
            });
          } else if (err) {
            console.log(err);
            cb(true, err)
          }
        });
      }
      storeAuth0Cred(authResult, profile) {
        // Set the time that the access token will expire at
        let expiresAt = JSON.stringify(
          authResult.expiresIn * 1000 + new Date().getTime()
        );
        localStorage.setItem('do_auth_access_token', authResult.accessToken);
        localStorage.setItem('do_auth_id_token', authResult.idToken);
        localStorage.setItem('do_auth_expires_at', expiresAt);
        localStorage.setItem('do_auth_profile', JSON.stringify(profile));
      }
      storeGraphCoolCred(authResult) {
        localStorage.setItem('do_auth_gcool_token', authResult.token);
        localStorage.setItem('do_auth_gcool_id', authResult.id);
      }
      login() {
        this.auth0.authorize();
      }
      logout(history) {
        // Clear access token and ID token from local storage
        localStorage.removeItem('do_auth_access_token');
        localStorage.removeItem('do_auth_id_token');
        localStorage.removeItem('do_auth_expires_at');
        localStorage.removeItem('do_auth_profile');
        localStorage.removeItem('do_auth_gcool_token');
        localStorage.removeItem('do_auth_gcool_id');
        // navigate to the home route
        history.replace('/');
      }
      isAuthenticated() {
        // Check whether the current time is past the
        // access token's expiry time
        const expiresAt = JSON.parse(localStorage.getItem('do_auth_expires_at'));
        return new Date().getTime() < expiresAt;
      }
      getProfile() {
        return JSON.parse(localStorage.getItem('do_auth_profile'));
      }
    }

下面是这段代码的作用:

  • 首先,这将创建一个 Auth0 SDK 实例并使用您的 Auth0 客户端凭据对其进行配置。 然后将该实例存储在实例变量 auth0 中。
  • handleAuthentication 将在身份验证完成时由您的组件之一调用。 Auth0 将通过 URL 哈希将令牌传回给您。 此方法读取并传递此哈希。
  • storeAuth0CredstoreGraphCoolCred 会将您的凭据保存到 localStorage 以供将来使用。
  • 您可以调用 isAuthenticated 来检查存储在 localStorage 中的令牌是否仍然有效。
  • getProfile 返回用户配置文件的 JSON 有效负载。

如果用户通过身份验证,您还希望将用户重定向到您的个人资料页面,或者如果他们没有通过,则将他们发送回主页(这是默认页面)。 回调页面是最好的选择。 首先,在 App.js 中的路由中添加另一条路由:

    //...
    import Home from './containers/home';
    import Callback from './containers/callback'
    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              {/* Callback route */}
              <Route exact path="/callback" component={Callback} />
              ...
            </Switch>
          </div>
        );
      }
    }
    export default App;

/callback 使用需要在 src/container/Callback.js 中创建的 Callback 组件:

    import React from 'react';
    import { graphql } from 'react-apollo';
    import gql from 'graphql-tag';
    import Auth from '../services/auth'
    const auth = new Auth();
    class Callback extends React.Component {
      componentDidMount() {
        auth.handleAuthentication(async (err, authResult) => {
          // Failed. Send back home
          if (err) this.props.history.push('/');
          // Send mutation to Graphcool with idToken
          // as the accessToken
          const result = await this.props.authMutation({
            variables: {
              accessToken: authResult.idToken
            }
          });
          // Save response to localStorage
          auth.storeGraphCoolCred(result.data.authenticateUser);
          // Redirect to profile page
          this.props.history.push('/profile');
        });
      }
      render() {
        // Show a loading text while the app validates the user
        return <div>Loading...</div>;
      }
    }

    // Mutation query
    const AUTH_MUTATION = gql`
      mutation authMutation($accessToken: String!) {
        authenticateUser(accessToken: $accessToken) {
          id
          token
        }
      }
    `;

这使用 Auth 公开的 handleAuthentication 方法向 Graphcool 服务器发送突变。 您尚未建立与服务器的连接,但一旦您尝试验证用户身份,此回调页面将向 Graphcool 服务器发送写入突变,告诉服务器该用户存在并且允许访问资源。

第 5 步 — 设置 Apollo 并连接到服务器

在回调组件中,您使用了 graphql(尚未安装)将组件连接到突变。 这并不意味着已经连接到 Graphcool 服务器。 您需要使用 Apollo 设置此连接,然后在应用程序的顶层提供 Apollo 实例。

从安装所需的依赖项开始:

yarn add apollo-client-preset react-apollo graphql-tag graphql

更新src/index.js入口文件:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter } from 'react-router-dom';
    import './index.css';
    import App from './App';
    import registerServiceWorker from './registerServiceWorker';

    // Import modules
    import { ApolloProvider } from 'react-apollo';
    import { ApolloClient } from 'apollo-client';
    import { HttpLink } from 'apollo-link-http';
    import { InMemoryCache } from 'apollo-cache-inmemory';

    // Create connection link
    const httpLink = new HttpLink({ uri: '[SIMPLE API URL]' });

    // Configure client with link
    const client = new ApolloClient({
      link: httpLink,
      cache: new InMemoryCache()
    });

    // Render App component with Apollo provider
    ReactDOM.render(
      <BrowserRouter>
        <ApolloProvider client={client}>
          <App />
        </ApolloProvider>
      </BrowserRouter>,
      document.getElementById('root')
    );

    registerServiceWorker();

首先,这会导入所有依赖项。 然后这使用 HttpLink 创建了一个链接。 传递的参数是一个带有 URI 的对象。 您可以通过在服务器文件夹上运行以下命令来获取服务器的 URI:

graphcool-framework list

使用 Simple URI 替换上面代码中的占位符。

接下来,您使用此链接和缓存创建并配置了一个 Apollo 客户端实例。 这个创建的客户端实例现在作为 prop 传递给包装了 App 组件的 Apollo 提供程序。

第 6 步 — 测试身份验证流程

现在一切都完好无损,您可以在导航栏上的按钮上添加一个事件来触发身份验证过程:

    //...
    import Auth from '../services/auth'
    const auth = new Auth();
    const Nav = () => {
      return (
        <nav className="navbar">
          ...
          <div className="navbar-menu">
            <div className="navbar-end">
              ...
              <div className="navbar-item join" onClick={() => {auth.login()}}>
                Join
              </div>
            </div>
          </div>
        </nav>
      );
    };
    export default Nav;

单击加入按钮时,我们会触发 auth.login 方法,该方法会将我们重定向到我们的 Auth0 域进行身份验证。

用户登录后,Auth0 会询问用户是否想访问他们的个人资料信息。

验证后,如果验证成功,请观看页面返回 /callback/profile

您还可以通过转到 Graphcool 仪表板中的数据视图并打开用户表来确认用户已创建。

第 7 步 - 添加条件按钮

接下来,您将希望在用户通过身份验证时隐藏 login 按钮,而是显示注销按钮。 回到 nav.js 用这个条件逻辑替换 Join 按钮元素:

     {auth.isAuthenticated() ? (
        <div
          className="navbar-item join"
          onClick={() => {
            auth.logout();
          }}
        >
          Logout
        </div>
      ) : (
        <div
          className="navbar-item join"
          onClick={() => {
            auth.login();
          }}
        >
          Join
        </div>
      )}

Auth 服务公开了一个名为 isAuthenticated 的方法来检查令牌是否存储在 localStorage 中并且未过期。 然后您的用户将登录。

第 8 步 — 显示用户资料

您还可以使用 Auth 服务来检索已登录的用户配置文件。 此配置文件已在 localStorage 中可用:

    //...
    import Auth from '../services/auth';
    const auth = new Auth();
    const Profile = props => (
      <div>
        //...
        <h2 className="title">Nickname: {auth.getProfile().nickname}</h2>
      </div>
    );
    export default Profile;

然后昵称将打印在浏览器中。

第 9 步 - 保护 Graphcool 端点

此时,经过身份验证的用户仍然可以访问受限后端,因为您还没有告诉服务器有关令牌的信息。

您可以在我们的请求标头中将令牌作为 Bearer 令牌发送。 使用以下内容更新 index.js

    //...
    import { ApolloLink } from 'apollo-client-preset'
    const httpLink = new HttpLink({ uri: '[SIMPLE URL]' });

    const middlewareAuthLink = new ApolloLink((operation, forward) => {
      const token = localStorage.getItem('do_auth_gcool_token')
      const authorizationHeader = token ? `Bearer ${token}` : null
      operation.setContext({
        headers: {
          authorization: authorizationHeader
        }
      })
      return forward(operation)
    })
    const httpLinkWithAuthToken = middlewareAuthLink.concat(httpLink)

不是仅使用 HTTP 链接创建 Apollo 客户端,而是将其更新为使用刚刚创建的中间件,该中间件将令牌添加到我们发出的所有服务器请求中:

    const client = new ApolloClient({
      link: httpLinkWithAuthToken,
      cache: new InMemoryCache()
    })

然后,您可以使用服务器端的令牌来验证请求。

第 10 步 — 保护路由

尽管您通过保护项目中最重要的部分(即服务器及其数据)做得很好,但让用户停留在没有内容的路线上是没有意义的。 需要保护 /profile 路由在用户未通过身份验证时不被访问。

如果用户进入个人资料但未通过身份验证,则更新 App.js 以重定向到主页:

    //...
    import { Switch, Route, Redirect } from 'react-router-dom';
    //...
    import Auth from './services/auth';
    const auth = new Auth();
    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              <Route exact path="/callback" component={Callback} />
              <Route exact path="/about" component={About} />
              <Route
                exact
                path="/profile"
                render={props =>
                  auth.isAuthenticated() ? (
                    <Profile />
                  ) : (
                    <Redirect
                      to={{
                        pathname: '/'
                      }}
                    />
                  )
                }
              />
            </Switch>
          </div>
        );
      }
    }
    export default App;

您仍在使用 auth.isAuthenticated 来检查身份验证。 如果返回 true,则 /profile 获胜,否则 / 获胜。

结论

在本教程中,您在 GraphQL 项目中使用 Auth0 对用户进行了身份验证。 您可以做的是转到您的 Auth0 仪表板并添加更多社交身份验证选项,例如 Twitter。 系统将要求您提供 Twitter 开发人员凭据,该凭据可从 Twitter 开发人员网站获取。