如何为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 开发人员网站获取。