분류 Reactjs

GraphQL 쿠키 및 JWT를 사용하여 인증하는 방법

컨텐츠 정보

  • 조회 829 (작성일 )

본문

쿠키 및 JWT를 사용하여 Apollo가 제공하는 GraphQL API의 인증 프로세스 예제


이 튜토리얼에서는 Apollo를 사용하여 GraphQL API의 로그인 메커니즘을 처리하는 방법을 설명합니다.


사용자 로그인에 따라 다른 정보가 표시되는 개인 공간이 만들어집니다.


세부적으로, 다음 단계입니다.

  • 클라이언트에서 로그인 양식을 작성하십시오.
  • 로그인 데이터를 서버로 전송
  • 사용자를 인증하고 JWT를 다시 보냅니다.
  • JWT를 쿠키에 저장
  • GraphQL API에 대한 추가 요청에 JWT를 사용하십시오.

이 자습서의 코드는 GitHub (https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt)에서 제공됩니다.


클라이언트 응용 프로그램을 시작합니다 


create-react-app를 사용하여 클라이언트 측 부분을 작성하고 빈 폴더에서 npx create-react-app 클라이언트를 실행하십시오.


그런 다음 cd client를 호출하고 npm은 나중에 다시 갈 필요가 없도록 필요한 모든 것을 설치합니다.


npm install apollo-client apollo-boost apollo-link-http apollo-cache-inmemory react-apollo apollo-link-context @reach/router js-cookie graphql-tag


로그인 양식 


로그인 양식을 만들어 시작하겠습니다.


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


여기서는 서버가 localhost, HTTP 프로토콜, 포트 3000에서 실행된다고 가정합니다.


React Hooks와 Reach Router를 사용합니다. 여기에는 아폴로 코드가 없습니다. 성공적으로 인증되면 새 쿠키를 등록하는 양식과 코드 만 있습니다.


Fetch API를 사용하여 사용자가 양식을 보내면 POST 요청으로 / login REST 엔드 포인트의 서버에 접속합니다.


서버가 로그인을 확인하면 JWT 토큰을 쿠키에 저장하고 아직 구축하지 않은 / private-area URL로 이동합니다.


앱에 양식 추가 


이 구성 요소를 사용하도록 앱의 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')
)


서버 측 


서버 측을 전환합시다.


서버 폴더를 작성하고 npm init -y를 실행하여 ready-to-go package.json 파일을 작성하십시오.


npm install express apollo-server-express cors bcrypt jsonwebtoken


그런 다음 app.js 파일을 작성하십시오.


여기서는 먼저 로그인 프로세스를 처리하겠습니다.


더미 데이터를 만들어 봅시다. 한 명의 사용자 :


const users = [{
  id: 1,
  name: 'Test user',
  email: 'your@email.com',
  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에 대한 자세한 내용은 여기를 참조하십시오. 실제로 사용자와 작업 관리를 데이터베이스에 저장하고 사용자가 등록 할 때 비밀번호 해시가 자동으로 작성됩니다.


로그인 프로세스 처리 


이제 로그인 프로세스를 처리하고 싶습니다.


사용할 많은 라이브러리를 로드하고 CORS를 사용하도록 Express를 초기화하여 다른 포트에 있는 것처럼 클라이언트 앱에서 사용할 수 있으며 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}))

다음으로 JWT 서명에 사용할 SECRET_KEY를 정의하고 / login POST 엔드 포인트 핸들러를 정의합니다. 코드에서 await를 사용하기 때문에 비동기 키워드가 있습니다. 요청 본문에서 이메일 및 비밀번호 필드를 추출하고 users "데이터베이스"에서 사용자를 찾습니다.


이메일로 사용자를 찾지 못하면 오류 메시지를 다시 보냅니다.


다음으로, 암호가 우리가 가진 해시와 일치하지 않는지 확인하고, 그렇다면 오류 메시지를 다시 보냅니다.


모든 것이 잘되면 jwt.sign() 호출을 사용하여 이메일과 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: ${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,
  })
})


이제 Express 앱을 시작할 수 있습니다 :

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


The private area 


이 시점에서 클라이언트 측에 쿠키에 토큰을 추가하고 /private-area URL로 이동합니다.


해당 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 라이브러리를 사용하여 쿠키를 쉽게 사용할 수 있습니다. 쿠키를 사용하여 쿠키에 토큰이 있는지 확인합니다. 그렇지 않은 경우 로그인 양식으로 돌아가십시오.

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 API를 사용하는 것이 좋습니다! 그러나 아직 그런 것은 없습니다. 그렇게 하겠습니다.


GraphQL API 


서버 측 모든 것을 단일 파일로 수행합니다. 장소가 거의 없기 때문에 그렇게 크지는 않습니다.


파일 맨 위에 이것을 추가합니다.

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


pollo GraphQL 서버를 만드는 데 필요한 모든 것을 제공합니다.


세 가지를 정의해야 합니다.

  • GraphQL 스키마
  • 리졸버
  • the context

스키마는 다음과 같습니다. 사용자 유형을 정의합니다. 이는 사용자 개체에 있는 것을 나타냅니다. 그런 다음 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]
  }
`

쿼리 유형에는 하나의 항목이 있으며 이에 대한 리졸버를 정의해야 합니다. 여기있어:

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


그런 다음 기본적으로 토큰을 확인하고 유효하지 않은 경우 오류가 발생하는 컨텍스트에서 ID 및 이메일 값을 얻습니다. 이것이 우리가 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 값이 나오는 곳입니다.


이제 미들웨어로 Express에 Apollo를 추가해야 하며 서버 측 부분이 완료되었습니다!

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


아폴로 클라이언트 


이제 Apollo Client를 초기화 할 준비가 되었습니다!


클라이언트 측 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'


/graphql 엔드 포인트에서 localhost의 포트 3000을 수신하여 GraphQL API 서버를 가리키는 HttpLink 오브젝트를 초기화하고 이를 사용하여 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 ${token}`
    }
  }
})

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


토큰이 있으면 개인 영역으로 이동합니다.

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

마지막으로 우리는 부모 구성 요소로 가져온 ApolloProvider 구성 요소를 사용하고 정의한 앱에서 모든 것을 래핑합니다. 이러한 방식으로 모든 하위 구성 요소의 클라이언트 객체에 액세스 할 수 있습니다. 특히 PrivateArea 하나, 곧!


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


The private area 


우리는 마지막 단계에 있습니다. 이제 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


아폴로에서 이 두 항목을 가져옵니다.

import { gql } from 'apollo-boost'
import { Query } from 'react-apollo'

대신에

return (
    <div>
      Private area!
    </div>
  )

쿼리 구성 요소를 사용하고 GraphQL 쿼리를 전달하겠습니다. 컴포넌트 바디 안에서 로딩, 에러, 데이터의 3 가지 속성을 가진 객체를 취하는 함수를 전달합니다.


아직 데이터를 사용할 수 없지만 로딩은 사실이며 사용자에게 메시지를 추가 할 수 있습니다. 오류가 있으면 다시 가져 오지만 그렇지 않으면 데이터 개체에 TO-DO 항목을 가져 와서 반복하여 항목을 사용자에게 렌더링 할 수 있습니다!

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


더 나은 보안을 위해 HttpOnly 쿠키 사용 


이제는 작동하지만 코드 작동 방식을 약간 변경하고 HTTPOnly 쿠키 사용을 추가하고 싶습니다. 이 특별한 종류의 쿠키는 JavaScript를 사용하여 액세스 할 수 없기 때문에 더욱 안전합니다. 따라서 제 3 자 스크립트에 의해 도난 당하거나 공격의 대상으로 사용될 수 없습니다.


상황이 좀 더 복잡해 졌으므로 맨 아래에 추가했습니다.


이 코드는 GitHub의 https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt에서 사용할 수 있으며 지금까지 설명한 모든 내용은 이 커밋에서 사용할 수 있습니다.


이 마지막 부분의 코드는 이 별도 커밋에서 사용할 수 있습니다.


먼저 클라이언트의 Form.js에서 쿠키에 토큰을 추가하는 대신 서명 된 쿠키를 추가합니다.


이것을 제거하십시오


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


그리고 추가


document.cookie = 'signedin=true'


다음으로 가져 오기 옵션에서 추가해야 합니다

credentials: 'include'

그렇지 않으면 가져 오기는 브라우저에서 서버에서 가져온 쿠키를 브라우저에 저장하지 않습니다.


이제 PrivateArea.js 파일에서 토큰 쿠키를 확인하지 않고 로그인 한 쿠키를 확인합니다.


if (!Cookies.get('token')) {
if (!Cookies.get('signedin')) {


서버 부분으로 갑니다.


먼저 npm install cookie-parser를 사용하여 쿠키 파서 라이브러리를 설치하고 토큰을 클라이언트로 다시 보내지 마십시오.

res.send({
  success: true,
  token: token,
})


res.send({
  success: true
})


JWT 토큰을 HTTPOnly 쿠키로 사용자에게 보냅니다.

res.cookie('jwt', token, {
  httpOnly: true
  //secure: true, //on HTTPS
  //domain: 'example.com', //set your domain
})

(생산시 HTTPS 및 도메인의 보안 옵션 설정)


다음으로 쿠키를 사용하도록 CORS 미들웨어를 설정해야 합니다. 그렇지 않으면 쿠키가 사라지기 때문에 GraphQL 데이터를 관리 할 때 상황이 매우 빨리 중단됩니다.


app.use(cors())
const corsOptions = {
  origin: 'http://localhost:3001', //change with your own client URL
  credentials: true
}


app.use(cors(corsOptions))
app.use(cookieParser())


클라이언트로 돌아가서 index.js에서 요청에 자격 증명 (쿠키)을 포함 시키도록 Apollo Client에 지시합니다. 스위치:

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

authLink 정의를 모두 제거하십시오.

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

  return {
    headers: {
      ...headers,
      authorization: `Bearer ${token}`
    }
  }
})


더 이상 필요하지 않습니다 더 많은 사용자 정의 인증 항목이 필요하지 않으므로 httpLink를 새로운 ApolloClient()로 전달합니다.


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


퍼즐의 마지막 조각을 위해 서버로 돌아갑니다! index.js를 열고 컨텍스트 함수 정의에서 변경하십시오.

const token = req.headers.authorization || ''
const token = req.cookies['jwt'] || ''


Express에서 이미 수행 한 것을 덮어 쓰기 때문에 Apollo 서버 내장 CORS 처리를 비활성화 하십시오.


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


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