كيفية المصادقة باستخدام ملفات تعريف الارتباط GraphQL و JWT

مثال على عملية المصادقة لواجهة برمجة تطبيقات GraphQL المدعومة من Apollo ، باستخدام ملفات تعريف الارتباط و JWT

سأشرح في هذا البرنامج التعليمي كيفية التعامل مع آلية تسجيل الدخول لملفGraphQLAPI باستخدامأبولو.

سننشئ منطقة خاصة تعرض معلومات مختلفة بناءً على تسجيل دخول المستخدم الخاص بك.

بالتفصيل ، هذه هي الخطوات:

  • قم بإنشاء نموذج تسجيل دخول على العميل
  • أرسل بيانات تسجيل الدخول إلى الخادم
  • قم بمصادقة المستخدم وأرسل JWT مرة أخرى
  • قم بتخزين ملفJWTفي ملف تعريف الارتباط
  • استخدم JWT لمزيد من الطلبات إلى واجهة برمجة تطبيقات GraphQL

رمز هذا البرنامج التعليمي متاح على GitHub فيhttps://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt

لنبدأ.

أبدأ تشغيل تطبيق العميل

لنقم بإنشاء جزء جانب العميل باستخدامcreate-react-app، يركضnpx create-react-app clientفي مجلد فارغ.

ثم اتصل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

نموذج تسجيل الدخول

لنبدأ بإنشاء نموذج تسجيل الدخول.

إنشاءForm.jsملف فيsrcالمجلد ، وأضف هذا المحتوى إليه:

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

<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
  <span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'post'</span>,
  <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
    <span style="color:#e6db74">'Content-type'</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'application/x-www-form-urlencoded; charset=UTF-8'</span>
  },
  <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`email=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">email</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&amp;password=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">password</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
}

<span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">url</span>, <span style="color:#a6e22e">options</span>)
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">response</span> =&gt; {
  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">404</span>) {
      <span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">'Email not found, please retry'</span>)
    }
    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">401</span>) {
      <span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">'Email and password do not match, please retry'</span>)
    }
  }
  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>
})
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">response</span> =&gt; <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>())
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">data</span> =&gt; {
  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">success</span>) {
    document.<span style="color:#a6e22e">cookie</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'token='</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">token</span>
    <span style="color:#a6e22e">navigate</span>(<span style="color:#e6db74">'/private-area'</span>)
  }
})

}

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

هنا أفترض أن الخادم سيعملlocalhost، على بروتوكول HTTP ، على المنفذ3000.

أستخدم React Hooks والوصول إلى جهاز التوجيه. لا يوجد كود أبولو هنا. مجرد نموذج وبعض الكود لتسجيل ملف تعريف ارتباط جديد عندما تتم المصادقة علينا بنجاح.

باستخدام Fetch API ، عندما يرسل المستخدم النموذج ، أتصل بالخادم على/loginنقطة نهاية REST مع طلب POST.

عندما يؤكد الخادم تسجيل الدخول ، سيخزن رمز JWT في ملف تعريف الارتباط ، وسينتقل إلى/private-areaURL ، الذي لم نقم ببنائه بعد.

أضف النموذج إلى التطبيق

دعنا نعدل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’) )

جانب الخادم

دعنا نبدل من جانب الخادم.

إنشاءserverمجلد وتشغيلnpm init -yلإنشاء جهاز جاهز للاستخدامpackage.jsonملف.

الآن تشغيل

npm install express apollo-server-express cors bcrypt jsonwebtoken

بعد ذلك ، أنشئ ملف app.js.

هنا ، سنتعامل أولاً مع عملية تسجيل الدخول.

لنقم بإنشاء بعض البيانات الوهمية. مستخدم واحد:

const users = [{
  id: 1,
  name: 'Test user',
  email: '[email protected]',
  password: '$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W' // = ssseeeecrreeet
}]

وبعض عناصر TODO:

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'
  }
]

يتم تعيين أول 2 منهم للمستخدم الذي حددناه للتو. العنصر الثالث ينتمي إلى مستخدم آخر. هدفنا هو تسجيل دخول المستخدم ، وإظهار عناصر TODO الخاصة بهم فقط.

تم إنشاء تجزئة كلمة المرور ، من أجل المثال ، يدويًا باستخدامbcrypt.hash()ويتوافق معssseeeecrreeetخيط. مزيد من المعلومات حول bcryptهنا. من الناحية العملية ، ستقوم بتخزين المستخدمين والمهام في قاعدة بيانات ، ويتم إنشاء تجزئات كلمة المرور تلقائيًا عند تسجيل المستخدمين.

تعامل مع عملية تسجيل الدخول

الآن ، أريد التعامل مع عملية تسجيل الدخول.

أقوم بتحميل مجموعة من المكتبات التي سنستخدمها ، وأهيئ Express لاستخدامهاكورس، حتى نتمكن من استخدامه من تطبيق العميل الخاص بنا (لأنه موجود على منفذ آخر) ، وأضيف البرنامج الوسيط الذي يوزع البيانات المشفرة لعنوان url:

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

بعد ذلك أعرّف ملفSECRET_KEYسنستخدمه لتوقيع JWT ، وأنا أحدد امتداد/loginمعالج نقطة نهاية POST. هناكasyncالكلمات الرئيسية لأننا سنستخدمawaitفي الكود. أستخرج حقلي البريد الإلكتروني وكلمة المرور من نص الطلب ، وأبحث عن المستخدم في ملفusers"قاعدة البيانات".

إذا لم يتم العثور على المستخدم عن طريق البريد الإلكتروني الخاص به ، فأنا أرسل رسالة خطأ مرة أخرى.

بعد ذلك ، أتحقق مما إذا كانت كلمة المرور لا تتطابق مع التجزئة التي لدينا ، وأرسل رسالة خطأ مرة أخرى إذا كان الأمر كذلك.

إذا سارت الأمور على ما يرام ، فأنا أقوم بإنشاء الرمز المميز باستخدام ملفjwt.sign()استدعاء تمريرemailوidكبيانات مستخدم ، وأرسلها إلى العميل كجزء من الاستجابة.

ها هو الكود:

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: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">email</span><span style="color:#e6db74">}</span><span style="color:#e6db74">, }) 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, }) })

يمكنني الآن بدء تشغيل تطبيق Express:

app.listen(3000, () =>
  console.log('Server listening on port 3000')
)

المجال الخاص

في هذه المرحلة ، من جانب العميل ، أقوم بإضافة الرمز المميز إلى ملفات تعريف الارتباط وانتقل إلى ملف/private-areaURL.

ماذا يوجد في عنوان URL هذا؟ لا شيئ! دعنا نضيف مكونًا للتعامل مع ذلك ، فيsrc/PrivateArea.js:

import React from 'react'

const PrivateArea = () => { return ( <div> Private area! </div> ) }

export default PrivateArea

في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’) )

أنا استخدم لطيفjs-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

الآن من الناحية النظرية ، نحن جميعًا على ما يرام لاستخدام واجهة برمجة تطبيقات GraphQL! لكن ليس لدينا مثل هذا الشيء حتى الآن. دعونا نصنع هذا الشيء.

واجهة برمجة تطبيقات GraphQL

من جانب الخادم أفعل كل شيء في ملف واحد. إنه ليس بهذا الحجم ، لأن لدينا أشياء صغيرة في مكانها.

أقوم بإضافة هذا إلى أعلى الملف:

const {
  ApolloServer,
  gql,
  AuthenticationError,
} = require('apollo-server-express')

مما يعطينا كل ما نحتاجه لإنشاء خادم Apollo GraphQL.

أحتاج إلى تحديد 3 أشياء:

  • مخطط GraphQL
  • محللات
  • السياق

ها هو المخطط. أحدد الUsertype ، والذي يمثل ما لدينا في كائن مستخدمينا. ثمTodoاكتب ، وأخيرًا ملفQuerytype ، الذي يحدد ما يمكننا الاستعلام عنه مباشرة: قائمة todos.

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] } `

الQueryالنوع له إدخال واحد ، ونحن بحاجة إلى تحديد محلل له. ها هو:

const resolvers = {
  Query: {
    todos: (root, args) => {
      return todos.filter(todo => todo.user === id)
    }
  }
}

ثم السياق ، حيث نتحقق أساسًا من الرمز المميز والخطأ إذا كان غير صالح ، وحصلنا على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’, ) } }

الidوemailالقيم متاحة الآن داخل المحلل الخاص بنا. هذا حيثidالقيمة التي نستخدمها أعلاه تأتي من.

نحتاج إلى إضافة Apollo إلى Express كبرنامج وسيط الآن ، وانتهى الجزء الجانبي من الخادم!

const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app })

عميل أبولو

نحن على استعداد لتهيئة عميل Apollo الآن!

من جانب العميل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'

أقوم بتهيئة كائن HttpLink الذي يشير إلى خادم GraphQL API ، والاستماع على المنفذ 3000 للمضيف المحلي ، على/graphqlنقطة النهاية ، واستخدمها لإعداد ملفApolloClientموضوع.

يوفر لنا HttpLink طريقة لوصف كيف نريد الحصول على نتيجة عملية GraphQL ، وما نريد فعله بالاستجابة.

const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })

const authLink = setContext((_, { headers }) => { const token = Cookies.get(‘token’)

return { headers: { …headers, authorization: Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> } } })

const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache() })

إذا كان لدينا رمز مميز ، فأنا انتقل إلى المجال الخاص:

if (Cookies.get('token')) {
  navigate('/private-area')
}

وأخيرًا أستخدم ملفApolloProviderالمكون الذي قمنا باستيراده كمكون أصلي وقمنا بلف كل شيء في التطبيق الذي حددناه. بهذه الطريقة يمكننا الوصول إلىclientكائن في أي من مكونات الأطفال لدينا. على وجه الخصوص PrivateArea one ، قريبًا جدًا!

ReactDOM.render(
  <ApolloProvider client={client}>
    <Router>
      <Form path="/" />
      <PrivateArea path="/private-area" />
    </Router>
  </ApolloProvider>,
  document.getElementById('root')
)

المجال الخاص

لذلك نحن في الخطوة الأخيرة. الآن يمكننا أخيرًا إجراء استعلام GraphQL الخاص بنا!

هذا ما لدينا الآن:

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

سأقوم باستيراد هذين العنصرين من Apollo:

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.

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!

  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.

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.

The code for this last part is available in this separate commit.

First, in the client, in Form.js, instead of adding the token to a cookie, I add a signedin cookie.

Remove this

document.cookie = 'token=' + data.token

and add

document.cookie = 'signedin=true'

Next, in the fetch options we must add

credentials: 'include'

otherwise fetch won’t store in the browser the cookies it gets back from the server.

Now in the PrivateArea.js file we don’t check for the token cookie, but for the 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:

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:

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)

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.

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:

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:

const authLink = setContext((_, { headers }) => {
  const token = Cookies.get('token')

return { headers: { …headers, authorization: Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> } } })

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:

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

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:

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

More graphql tutorials: