如何为React和GraphQL应用程序实施身份验证和路由保护
介绍
电子邮件、Facebook、Google、Twitter、Github 都是在您的 Web 应用程序中验证用户身份的可能选项。 使用 React 和 GraphQL 构建的应用程序同样适用于此类身份验证。
在本文中,您将学习如何使用以下方法将各种身份验证提供程序添加到 GraphQL 应用程序:
如果用户未能通过我们的身份验证服务器的身份验证,我们还将学习如何保护 React 路由不被访问。
第 1 步——准备项目
计划是有一个 React 项目设置。 在设置 React 项目的同一目录中,我们可以配置一个 Graphcool 服务器并告诉我们的项目我们希望我们的服务器如何运行。 从安装 create-react-app
和 graphcool-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.js
、profile.js
、admin.js
和 about.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_DOMAIN
和 AUTH0_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 哈希将令牌传回给您。 此方法读取并传递此哈希。storeAuth0Cred
和storeGraphCoolCred
会将您的凭据保存到 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 开发人员网站获取。