jwt跨域身份验证_如何使用GraphQL Cookies和JWT进行身份验证

本文介绍如何使用Apollo处理GraphQL API的登录机制,实现JWT跨域身份验证。详细步骤包括创建客户端登录表单,将数据发送到服务器进行验证,服务器验证后将JWT存储在HttpOnly cookie中,客户端使用JWT进行后续对GraphQL API的请求。同时,文章提供了源代码链接供参考。
摘要由CSDN通过智能技术生成

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:

详细来说,这些步骤是:

  • Create a login form on the client

    在客户端上创建登录表单
  • Send the login data to the server

    将登录数据发送到服务器
  • Authenticate the user and send a JWT back

    验证用户身份并将JWT发送回
  • Store the JWT in a cookie

    JWT存储在cookie中

  • Use the JWT for further requests to the GraphQL API

    将JWT用于对GraphQL API的进一步请求

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.

开始吧。

我启动客户端应用程序 (I start up the client application)

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

登录表格 (The login form)

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(我们尚未构建)。

将表单添加到应用程序 (Add the form to the app)

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')
)

服务器端 (The server side)

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的更多信息在这里 。 实际上,您会将用户和待办事项存储在数据库中,并且在用户注册时会自动创建密码哈希。

处理登录过程 (Handle the login process)

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()调用生成令牌,并将emailid作为用户数据传递,然后将其作为响应的一部分发送给客户端。

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')
)

私人区域 (The private area)

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了! 但是我们还没有这样的事情。 让我们做那件事。

GraphQL API (The 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件事:

  • the GraphQL schema

    GraphQL模式
  • resolvers

    解析器
  • the context

    上下文

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:

然后是上下文,我们在此基本上验证令牌并在无效的情况下出错,然后从中获取idemail值。 这就是我们知道在与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.

idemail值现在可以在我们的解析器中使用。 这就是我们上面使用的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 })

阿波罗客户 (The Apollo Client)

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')
)

私人区域 (The private area)

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个属性的对象: loadingerrordata

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跨域身份验证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值