jwt跨域身份验证
In this tutorial I’ll explain how to handle a login mechanism for a GraphQL API using Apollo.
在本教程中,我将解释如何使用Apollo处理GraphQL API的登录机制。
We’ll create a private area that depending on your user login will display different information.
我们将创建一个私人区域,该区域将根据您的用户登录名显示不同的信息。
In detail, these are the steps:
详细来说,这些步骤是:
Store the JWT in a cookie
将JWT存储在cookie中
The code for this tutorial is available on GitHub at https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt
本教程的代码可在GitHub上找到, 网址为https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt
Let’s start.
开始吧。
Let’s create the client side part using create-react-app
, run npx create-react-app client
in an empty folder.
让我们使用create-react-app
创建客户端部分,在一个空文件夹中运行npx create-react-app client
。
Then call cd client
and npm install
all the things we’ll need so that we don’t need to go back later:
然后调用cd client
,然后npm install
我们需要的所有东西,这样我们以后就不必再回去了:
npm install apollo-client apollo-boost apollo-link-http apollo-cache-inmemory react-apollo apollo-link-context @reach/router js-cookie graphql-tag
Let’s start by creating the login form.
让我们从创建登录表单开始。
Create a Form.js
file in the src
folder, and add this content into it:
在src
文件夹中创建一个Form.js
文件,并将以下内容添加到其中:
import React, { useState } from 'react'
import { navigate } from '@reach/router'
const url = 'http://localhost:3000/login'
const Form = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const submitForm = event => {
event.preventDefault()
const options = {
method: 'post',
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: `email=${email}&password=${password}`
}
fetch(url, options)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
alert('Email not found, please retry')
}
if (response.status === 401) {
alert('Email and password do not match, please retry')
}
}
return response
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.cookie = 'token=' + data.token
navigate('/private-area')
}
})
}
return (
<div>
<form onSubmit={submitForm}>
<p>Email: <input type="text" onChange={event => setEmail(event.target.value)} /></p>
<p>Password: <input type="password" onChange={event => setPassword(event.target.value)} /></p>
<p><button type="submit">Login</button></p>
</form>
</div>
)
}
export default Form
Here I assume the server will run on localhost
, on the HTTP protocol, on port 3000
.
在这里,我假设服务器将在HTTP协议的端口3000
上的localhost
上运行。
I use React Hooks, and the Reach Router. There’s no Apollo code here. Just a form and some code to register a new cookie when we get successfully authenticated.
我使用React Hooks和Reach Router 。 这里没有阿波罗代码。 当我们成功通过身份验证时,只需一个表格和一些代码即可注册一个新的cookie。
Using the Fetch API, when the form is sent by the user I contact the server on the /login
REST endpoint with a POST request.
使用Fetch API,当用户发送表单时,我通过POST请求与/login
REST端点上的服务器联系。
When the server will confirm we are logged in, it will store the JWT token into a cookie, and it will navigate to the /private-area
URL, which we haven’t built yet.
当服务器确认我们已登录时,它将把JWT令牌存储到cookie中,并将导航到/private-area
URL(我们尚未构建)。
Let’s edit the index.js
file of the app to use this component:
让我们编辑应用程序的index.js
文件以使用此组件:
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'
ReactDOM.render(
<Router>
<Form path="/" />
</Router>
document.getElementById('root')
)
Let’s switch server-side.
让我们切换服务器端。
Create a server
folder and run npm init -y
to create a ready-to-go package.json
file.
创建一个server
文件夹,然后运行npm init -y
创建一个随时可用的package.json
文件。
Now run
现在运行
npm install express apollo-server-express cors bcrypt jsonwebtoken
Next, create an app.js file.
接下来,创建一个app.js文件。
In here, we’re going to first handle the login process.
在这里,我们将首先处理登录过程。
Let’s create some dummy data. One user:
让我们创建一些虚拟数据。 一个用户:
const users = [{
id: 1,
name: 'Test user',
email: 'your@email.com',
password: '$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W' // = ssseeeecrreeet
}]
and some TODO items:
和一些待办事项:
const todos = [
{
id: 1,
user: 1,
name: 'Do something'
},
{
id: 2,
user: 1,
name: 'Do something else'
},
{
id: 3,
user: 2,
name: 'Remember the milk'
}
]
The first 2 of them are assigned to the user we just defined. The third item belongs to another user. Our goal is to log in the user, and show only the TODO items belonging to them.
其中的前2个已分配给我们刚刚定义的用户。 第三项属于另一个用户。 我们的目标是登录用户,并仅显示属于他们的TODO项目。
The password hash, for the sake of the example, was generated by me manually using bcrypt.hash()
and corresponds to the ssseeeecrreeet
string. More info on bcrypt here. In practice you’ll store users and todos in a database, and password hashes are created automatically when users register.
就本例而言,密码哈希是由我使用bcrypt.hash()
手动生成的,并且对应于ssseeeecrreeet
字符串。 关于bcrypt的更多信息在这里 。 实际上,您会将用户和待办事项存储在数据库中,并且在用户注册时会自动创建密码哈希。
Now, I want to handle the login process.
现在,我要处理登录过程。
I load a bunch of libraries we’re going to use, and initialize Express to use CORS, so we can use it from our client app (as it’s on another port), and I add the middleware that parses urlencoded data:
我加载了一堆我们将要使用的库,并初始化Express以使用CORS ,因此我们可以从客户端应用程序使用它(因为它在另一个端口上),并且我添加了用于解析urlencoded数据的中间件:
const express = require('express')
const cors = require('cors')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const app = express()
app.use(cors())
app.use(express.urlencoded({
extended: true
}))
Next I define a SECRET_KEY
we’ll use for the JWT signing, and I define the /login
POST endpoint handler. There’s an async
keyword because we’re going to use await
in the code. I extract the email and password fields from the request body, and I lookup the user in our users
“database”.
接下来,我定义一个将用于JWT签名的SECRET_KEY
,并定义/login
POST端点处理程序。 有一个async
关键字,因为我们将在代码中使用await
。 我从请求正文中提取电子邮件和密码字段,然后在users
“数据库”中查找用户。
If the user is not found by its email I send an error message back.
如果未通过电子邮件找到该用户,则会将错误消息发送回去。
Next, I check if the password does not match the hash we have, and send an error message back if so.
接下来,我检查密码是否与我们拥有的哈希不匹配,如果匹配,则发送错误消息。
If all goes well, I generate the token using the jwt.sign()
call, passing the email
and id
as user data, and I send it to the client as part of the response.
如果一切顺利,我将使用jwt.sign()
调用生成令牌,并将email
和id
作为用户数据传递,然后将其作为响应的一部分发送给客户端。
Here’s the code:
这是代码:
const SECRET_KEY = 'secret!'
app.post('/login', async (req, res) => {
const { email, password } = req.body
const theUser = users.find(user => user.email === email)
if (!theUser) {
res.status(404).send({
success: false,
message: `Could not find account: ${email}`,
})
return
}
const match = await bcrypt.compare(password, theUser.password)
if (!match) {
//return error to user to let them know the password is incorrect
res.status(401).send({
success: false,
message: 'Incorrect credentials',
})
return
}
const token = jwt.sign(
{ email: theUser.email, id: theUser.id },
SECRET_KEY,
)
res.send({
success: true,
token: token,
})
})
I can now start the Express app:
我现在可以启动Express应用程序:
app.listen(3000, () =>
console.log('Server listening on port 3000')
)
At this point client-side I add the token to the cookies and moves to the /private-area
URL.
此时,在客户端,我将令牌添加到cookie中并移至/private-area
URL。
What’s in that URL? Nothing! Let’s add a component to handle that, in src/PrivateArea.js
:
该网址是什么? 没有! 让我们在src/PrivateArea.js
添加一个组件来处理该问题:
import React from 'react'
const PrivateArea = () => {
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
In index.js
, we can add this to the app:
在index.js
,我们可以将其添加到应用程序中:
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'
import PrivateArea from './PrivateArea'
ReactDOM.render(
<Router>
<Form path="/" />
<PrivateArea path="/private-area" />
</Router>
document.getElementById('root')
)
I use the nice js-cookie
library to work easily with cookies. Using it I check if there’s the token
in the cookies. If not, just go back to the login form:
我使用漂亮的js-cookie
库轻松使用cookie。 使用它,我检查cookie中是否有token
。 如果没有,请返回登录表单:
import React from 'react'
import Cookies from 'js-cookie'
import { navigate } from '@reach/router'
const PrivateArea = () => {
if (!Cookies.get('token')) {
navigate('/')
}
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
Now in theory we’re all good to go and use the GraphQL API! But we have no such thing yet. Let’s make that thing.
从理论上讲,现在大家都可以使用GraphQL API了! 但是我们还没有这样的事情。 让我们做那件事。
Server-side I do everything in a single file. It’s not that big, as we have little things in place.
服务器端我将所有事情都放在一个文件中。 它没有那么大,因为我们没有什么东西。
I add this to the top of the file:
我将此添加到文件的顶部:
const {
ApolloServer,
gql,
AuthenticationError,
} = require('apollo-server-express')
which gives us all we need to make the Apollo GraphQL server.
这为我们提供了制作Apollo GraphQL服务器所需的一切。
I need to define 3 things:
我需要定义3件事:
Here’s the schema. I define the User
type, which represents what we have in our users object. Then the Todo
type, and finally the Query
type, which sets what we can directly query: the list of todos.
这是架构。 我定义了User
类型,它表示我们的用户对象中具有的内容。 然后是Todo
类型,最后是Query
类型,它设置了我们可以直接查询的内容:todo列表。
const typeDefs = gql`
type User {
id: ID!
email: String!
name: String!
password: String!
}
type Todo {
id: ID!
user: Int!
name: String!
}
type Query {
todos: [Todo]
}
`
The Query
type has one entry, and we need to define a resolver for it. Here it is:
Query
类型只有一个条目,我们需要为其定义一个解析器。 这里是:
const resolvers = {
Query: {
todos: (root, args) => {
return todos.filter(todo => todo.user === id)
}
}
}
Then the context, where we basically verify the token and error out if invalid, and we get the id
and email
values from it. This is how we know who is talking to the API:
然后是上下文,我们在此基本上验证令牌并在无效的情况下出错,然后从中获取id
和email
值。 这就是我们知道谁在与API通讯的方式:
const context = ({ req }) => {
const token = req.headers.authorization || ''
try {
return { id, email } = jwt.verify(token.split(' ')[1], SECRET_KEY)
} catch (e) {
throw new AuthenticationError(
'Authentication token is invalid, please log in',
)
}
}
The id
and email
values are now available inside our resolver(s). That’s where the id
value we use above comes from.
id
和email
值现在可以在我们的解析器中使用。 这就是我们上面使用的id
值的来源。
We need to add Apollo to Express as a middleware now, and the server side part is finished!
现在我们需要将Apollo作为中间件添加到Express中,服务器端部分就完成了!
const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app })
We are ready to initialize our Apollo Client now!
我们现在准备初始化我们的Apollo Client!
In the client-side index.js
file I add those libraries:
在客户端index.js
文件中,添加以下库:
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloProvider } from 'react-apollo'
import { setContext } from 'apollo-link-context'
import { navigate } from '@reach/router'
import Cookies from 'js-cookie'
import gql from 'graphql-tag'
I initialize an HttpLink object that points to the GraphQL API server, listening on port 3000 of localhost, on the /graphql
endpoint, and use that to set up the ApolloClient
object.
我初始化一个指向GraphQL API服务器的HttpLink对象,在/graphql
端点上侦听localhost的端口3000,然后使用该对象来设置ApolloClient
对象。
An HttpLink provides us a way to describe how we want to get the result of a GraphQL operation, and what we want to do with the response.
HttpLink为我们提供了一种描述如何获取GraphQL操作结果以及如何处理响应的方法。
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })
const authLink = setContext((_, { headers }) => {
const token = Cookies.get('token')
return {
headers: {
...headers,
authorization: `Bearer ${token}`
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
})
If we have a token I navigate to the private area:
如果我们有令牌,则导航到私有区域:
if (Cookies.get('token')) {
navigate('/private-area')
}
and finally I use the ApolloProvider
component we imported as a parent component and wrap everything in the app we defined. In this way we can access the client
object in any of our child components. In particular the PrivateArea one, very soon!
最后,我将导入的ApolloProvider
组件用作父组件,并将所有内容包装在我们定义的应用程序中。 这样,我们可以在任何子组件中访问client
对象。 特别是PrivateArea,很快!
ReactDOM.render(
<ApolloProvider client={client}>
<Router>
<Form path="/" />
<PrivateArea path="/private-area" />
</Router>
</ApolloProvider>,
document.getElementById('root')
)
So we’re at the last step. Now we can finally perform our GraphQL query!
因此,我们处于最后一步。 现在,我们终于可以执行GraphQL查询了!
Here’s what we have now:
这是我们现在拥有的:
import React from 'react'
import Cookies from 'js-cookie'
import { navigate } from '@reach/router'
const PrivateArea = () => {
if (!Cookies.get('token')) {
navigate('/')
}
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
I’m going to import these 2 items from Apollo:
我将从Apollo导入以下2个项目:
import { gql } from 'apollo-boost'
import { Query } from 'react-apollo'
and instead of
而不是
return (
<div>
Private area!
</div>
)
I’m going to use the Query
component and pass a GraphQL query. Inside the component body we pass a function that takes an object with 3 properties: loading
, error
and data
.
我将使用Query
组件并传递GraphQL查询。 在组件主体内部,我们传递了一个函数,该函数采用具有3个属性的对象: loading
, error
和data
。
While the data is not available yet, loading
is true and we can add a message to the user. If there’s any error we’ll get it back, but otherwise we’ll get our TO-DO items in the data
object and we can iterate over them to render our items to the user!
尽管数据尚不可用,但loading
是正确的,我们可以向用户添加一条消息。 如果有任何错误,我们会找回来,否则,我们会将TO-DO项目放入data
对象中,然后我们可以遍历它们以将项目呈现给用户!
return (
<div>
<Query
query={gql`
{
todos {
id
name
}
}
`}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>
if (error) {
navigate('/')
return <p></p>
}
return <ul>{data.todos.map(item => <li key={item.id}>{item.name}</li>)}</ul>
}}
</Query>
</div>
)
Now that things are working, I want to change a little bit how the code works and add the use of HTTPOnly cookies. This special kind of cookie is more secure because we can’t access it using JavaScript, and as such it can’t be stolen by 3rd part scripts and used as a target for attacks.
现在一切正常,我想稍微更改代码的工作方式,并添加对HTTPOnly cookie的使用。 这种特殊的cookie更加安全,因为我们无法使用JavaScript来访问它,因此它不能被第三部分脚本窃取并用作攻击的目标。
Things are a bit more complex now so I added this at the bottom.
现在事情变得更复杂了,所以我在底部添加了它。
All the code is available on GitHub at https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt and all I described up to now is available in this commit.
所有代码都可以在GitHub上的https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt上获得,而我到目前为止所描述的全部内容都可以在此commit中获得 。
The code for this last part is available in this separate commit.
最后一部分的代码在此单独的commit中可用。
First, in the client, in Form.js
, instead of adding the token to a cookie, I add a signedin
cookie.
首先,在客户端的Form.js
,我没有添加令牌到cookie,而是添加了一个signedin
cookie。
Remove this
删除这个
document.cookie = 'token=' + data.token
and add
并添加
document.cookie = 'signedin=true'
Next, in the fetch
options we must add
接下来,在fetch
选项中,我们必须添加
credentials: 'include'
otherwise fetch
won’t store in the browser the cookies it gets back from the server.
否则, fetch
将不会在浏览器中存储它从服务器返回的cookie。
Now in the PrivateArea.js
file we don’t check for the token cookie, but for the signedin
cookie:
现在,在PrivateArea.js
文件中,我们不检查令牌cookie,而是检查signedin
cookie:
Remove
去掉
if (!Cookies.get('token')) {
and add
并添加
if (!Cookies.get('signedin')) {
Let’s go to the server part.
让我们转到服务器部分。
First install the cookie-parser
library with npm install cookie-parser
and instead of sending back the token to the client:
首先使用npm install cookie-parser
安装cookie-parser
库,而不是将令牌发送回客户端:
res.send({
success: true,
token: token,
})
Only send this:
仅发送此:
res.send({
success: true
})
We send the JWT token to the user as an HTTPOnly cookie:
我们将JWT令牌作为HTTPOnly Cookie发送给用户:
res.cookie('jwt', token, {
httpOnly: true
//secure: true, //on HTTPS
//domain: 'example.com', //set your domain
})
(in production set the secure option on HTTPS and also the domain)
(在生产环境中,在HTTPS以及域上设置安全选项)
Next we need to set the CORS middleware to use cookies, too. Otherwise things will break very soon when we manage the GraphQL data, as cookies just disappear.
接下来,我们需要将CORS中间件也设置为使用cookie。 否则,当我们管理GraphQL数据时,事情会很快崩溃,因为cookie会消失。
Change
更改
app.use(cors())
with
与
const corsOptions = {
origin: 'http://localhost:3001', //change with your own client URL
credentials: true
}
app.use(cors(corsOptions))
app.use(cookieParser())
Back to the client, in index.js
we tell Apollo Client to include credentials (cookies) in its requests. Switch:
回到客户端,在index.js
我们告诉Apollo Client在其请求中包括凭据(cookie)。 开关:
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })
with
与
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql', credentials: 'include' })
and remove the authLink
definition altogether:
并完全删除authLink
定义:
const authLink = setContext((_, { headers }) => {
const token = Cookies.get('token')
return {
headers: {
...headers,
authorization: `Bearer ${token}`
}
}
})
as we don’t need it any more. We’ll just pass httpLink
to new ApolloClient()
, since we don’t need more customized authentication stuff:
因为我们不再需要它了。 我们将只是将httpLink
传递给new ApolloClient()
,因为我们不需要更多的自定义身份验证内容:
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
})
Back to the server for the last piece of the puzzle! Open index.js
and in the context
function definition, change
返回服务器以了解最后的难题! 打开index.js
并在context
函数定义中进行更改
const token = req.headers.authorization || ''
with
与
const token = req.cookies['jwt'] || ''
and disable the Apollo Server built-in CORS handling, since it overwrites the one we already did in Express, change:
并禁用Apollo Server内置的CORS处理,因为它会覆盖我们已经在Express中执行的操作,因此请更改:
const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app })
to
至
const server = new ApolloServer({ typeDefs, resolvers, context,
cors: false })
server.applyMiddleware({ app, cors: false })
翻译自: https://flaviocopes.com/graphql-auth-apollo-jwt-cookies/
jwt跨域身份验证