지난 포스팅에서 DB의 접근 및 리졸버의 구현을 알았으니, 이번 포스팅에서는 인증
에 대하여 알아볼 예정이다.
회원가입
과 로그인
의 과정이 이루어지는 기능적인 부분을 알아보도록 하자.
본 시리즈는 HOW TO GRAPHQL 글을 참고하여 작성했습니다.
기존에 참조하던 cadenzah 님의 포스팅이 deprecated되어 새롭게 작성하였습니다.
오타 및 의역이 있을 수 있으니 양해를 부탁 드리며, 수정 사항은 댓글로 알려주세요.
회원가입 및 로그인 기능을 포함하는 인증
기능을 구현하기 위해서 우선 다음과 같이 User
모델을 추가한다.
Uesr
모델은 Link
모델과 관계가 있으며, Link
는 User
에 의해 작성된다.
prisma/schema.prisma
파일에 데이터 모델을 명시적으로 표시한다.
model Link {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
description String
url String
postedBy User? @relation(fields: [postedById], references: [id])
postedById Int?
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
links Link[]
}
위의 코드에서 우리는 User
인스턴스를 참조하는 postedBy
필드를 Link
모델에 추가했다.
마찬가지로 Link
의 배열을 필드값으로 가지는 links
를 User
모델에 추가했다.
이렇게 참조관계를 성립하게 위해서는, postedBy
필드에 @realation
어노테이션을 추가해야한다.
이러한 방법은 Prisma 스키마를 따르는 모든 참조관계 모델에 해당하며, 참조되는 테이블에 외래키
설정을해야한다.
따라서 위의 경우에, 우리는 Link
를 만든 User
의 id
를 추가적으로 postedById
라는 필드 값에 저장하여 Prisma에게 참조관계를 알려준다.
Prisma의 참조관계는 이곳을 참조
데이터 모델의 변경이 일어났으므로, migrate
과정을 다시 진행해야 한다.
# ./hackernews-node
npx prisma migrate save --name "add-user-model" --experimental
위의 명령을 통해 prisma/migrations
위치에 2번째 migration이 생성됨을 알 수 있다.
해당 디렉토리 내부의 README.md
파일을 참조하면 자세한 변동내역이 기록이 열람 가능하다.
migration을 저장했으니, 이제 데이터베이스에 변동내역을 반영한다.
# ./hackernews-node
npx prisma migrate up --experimental
이제 우리의 데이터베이스는 우리의 데이터모델를 반영하게 되었다.
마지막으로 다음의 명령을 통해 Prisma Client를 재생성하자.
# ./hackernews-node
npx prisma generate
매번 이렇게 귀찮은 과정을 거쳐서 반영해야하는가? 라는 의문이 들 수 있다.
그러나 본 포스팅의 후반부에서자동화
과정을 다룰 예정이니 좀만 참길 바란다.
데이터모델을 반영한 데이터베이스가 준비되었으니, 이제 실제 데이터에 접근할 수 있는 API
를 구현할 차례이다.
인증
의 과정을 반영하기 위해 signup
과 login
두가지 뮤테이션을 작성하도록 하자.
뮤테이션 스키마 변경을 위해 src/schema.graphql
파일을 다음과 같이 수정한다.
type Mutation {
post(url: String!, description: String!): Link!
signup(email: String!, password: String!, name: String!): AuthPayload
login(email: String!, password: String!): AuthPayload
}
AuthPayload
타입을 사용하기 위해 새롭게 src/schema.graphql
파일에 추가한다.
type AuthPayload {
token: String
user: User
}
type User {
id: ID!
name: String!
email: String!
links: [Link!]!
}
새롭게 추가한 signup
과 login
큐테이션은 굉장히 비슷하게 동작한다.
둘다 GraphQL API에서 인증처리를 하는 token
을 기반으로 접속한 User
의 정보를 반환한다.
이러한 정보가 AuthPayload
타입에 정의된다.
마지막으로, User
와 Link
간의 양방향 참조관계를 반영하여 schema.graphql
파일의 Link
모델을 다음과 같이 수정한다.
type Link {
id: ID!
description: String!
url: String!
postedBy: User
}
스키마 정의를 새롭게 확장했으니, 추가적으로 리졸버
함수를 수정할 차례이다.
리졸버 함수를 수정하기 전에 기존에 하나의 파일에 존재하는 리졸버들을 별도
의 파일로 분리하자.
# ./hackernews-node
mkdir src/resolvers
touch src/resolvers/Query.js
touch src/resolvers/Mutation.js
touch src/resolvers/User.js
touch src/resolvers/Link.js
분리가 되었다면, 기존에 구현된 feed
리졸버를 Query.js
파일로 옮긴다.
// ./hackernews-node/src/resolvers/Query.js
function feed(parent, args, context, info) {
return context.prisma.link.findMany()
}
module.exports = {
feed,
}
쿼리를 별도의 리졸버 파일로 분리 했으므로, 다음으로는 뮤테이션
을 분리한다.
이때 위에서 새롭게 추가된 login
과 sginup
리졸버를 함께 추가해준다.
// ./hackernews-node/src/resolvers/Mutation.js
async function signup(parent, args, context, info) {
// 1
const password = await bcrypt.hash(args.password, 10)
// 2
const user = await context.prisma.user.create({ data: { ...args, password } })
// 3
const token = jwt.sign({ userId: user.id }, APP_SECRET)
// 4
return {
token,
user,
}
}
async function login(parent, args, context, info) {
// 1
const user = await context.prisma.user.findOne({ where: { email: args.email } })
if (!user) {
throw new Error('No such user found')
}
// 2
const valid = await bcrypt.compare(args.password, user.password)
if (!valid) {
throw new Error('Invalid password')
}
const token = jwt.sign({ userId: user.id }, APP_SECRET)
// 3
return {
token,
user,
}
}
module.exports = {
signup,
login,
post,
}
signup
은 다음의 과정을 통해 이루어진다.
signup
뮤테이션에서 User
의 비밀번호를 bcryptjs
를 이용하여 암호화한다.PrismaClient
인스턴스를 사용하여 새로운 User
를 생성 후 데이터베이스에 저장한다.jwt
라이브러리를 설치하고, APP_SECRET
을 이용하여 JWT
를 생성한다.token
과 user
를 AuthPayload
객체 모양으로 가공하여 반환한다.login
은 다음의 과정을 통해 이루어진다.
User
객체를 생성하는 대신, PrismaClient
를 활용하여 login
뮤테이션에서 인자로 받은 email
과 일치하는 User
를 가져온다.비교
하여 다르다면 에러를 반환한다.token
과 user
를 반환한다.위의 과정을 수행하기 위해 다음과 같은 명령어를 통해 필요한 라이브러리를 설치한다.
# ./hackernews-node
npm install jsonwebtoken bcryptjs
또한 사용할 라이브러리를 위한 새로운 파일을 src/utils.js
로 생성후, 내용을 추가한다.
# ./hackernews-node
touch src/utils.js
// ./hackernews-node/src/utils.js
const jwt = require('jsonwebtoken')
const APP_SECRET = 'GraphQL-is-aw3some'
function getUserId(context) {
const Authorization = context.request.get('Authorization')
if (Authorization) {
const token = Authorization.replace('Bearer ', '')
const { userId } = jwt.verify(token, APP_SECRET)
return userId
}
throw new Error('Not authenticated')
}
module.exports = {
APP_SECRET,
getUserId,
}
APP_SECRET
은 사용자들에게 발급해줄 JWT를 서명하는 데에 사용된다.
getUserId
는 헬퍼함수로써, 인증이 필요한 함수에서 사용할 수 있는 함수이다.
이 함수는 context
로 부터 User
의 JWT를 포함한 Authorization
Header를 받는다.
그 후 JWT를 검증하고, User
의 ID를 추출한다.
만약 정상적이지 않다면, exception
을 발생시키는데, 이는 인증 과정에서 리졸버들을 보호
하는 역할을 한다.
모든 과정을 수행하기 위해 Mutation.js
에 위의 함수들을 포함시켜준다.
// ./hackernews-node/src/resolvers/Mutation.js
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { APP_SECRET, getUserId } = require('../utils')
여기까지 왔다면, 마지막 작은 문제가 남게 된다. 우리는 context
객체 내의 request
객체에 접근하고 있다.
하지만 context를 처음 초기화할 때, 우리는 prisma
클라이언트 인스턴스만을 추가하고, 아직 request
객체가 추가되지 않았다.
이를 위해 index.js
에 다음과 같이 초기화 코드를 추가한다.
// ./hackernews-node/src/index.js
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
context: request => {
return {
...request,
prisma,
}
},
})
이번에는 request를 context
객체에 직접 추가하지 않고, context 객체를 반환하는 함수
로 그 형태를 바꿨다.
이러한 접근 방식의 장점은 바로 GraphQL 쿼리(또는 뮤테이션)를 전달하는 HTTP 요청을 context에 직접
추가할 수 있다는 점이다.
이렇게 되면 리졸버에서 Authorization 헤더를 읽고, 요청을 보낸 사용자가 해당 요청에 상응하는 동작을 수행할 수 있는지 여부를 검증
할 수 있다.
Mutation.js
파일을 열고, 다음의 코드를 post
리졸버에 추가한다.
// ./hackernews-node/src/resolvers/Mutation.js
function post(parent, args, context, info) {
const userId = getUserId(context)
return context.prisma.link.create({
data: {
url: args.url,
description: args.description,
postedBy: { connect: { id: userId } },
}
})
}
이는 기존의 방식과는 다음과 같은 2가지 다른 방식을 지닌다.
getUserId
함수를 사용하여 User
의 ID를 JWT로 부터 받아온다. 그러므로 우리는 어떤 User
가 새로운 Link
를 작성했는지 알 수 있다.exception
을 발생시키고, 해당 함수의 범위는 creatLink
뮤테이션이 실행되기 전에 종료된다.userId
를 사용하여, 생성될 Link와 이 Link를 생성한 User를 연결
하며, 이 과정은 중첩된 객체 쓰기를 통하여 이루어진다.서버를 구동해서 새로운 기능을 테스트하기 전에 User
와 Link
간의 관계가 올바르게 리졸브 되었는지 확인해야 한다.
앞선 포스팅에서 User
와 Link
타입에서 스칼라
값의 경우 리졸버를 생략했다.
Link: {
id: parent => parent.id,
url: parent => parent.url,
description: parent => parent.description,
}
하지만 이런 방식으로는 리졸브 할 수 없는 2개의 필드를 새롭게 추가했는데, 다음과 같다.
Link
타입의 postedBy
필드User
타입의 links
필드// ./hackernews-node/src/resolvers/Link.js
function postedBy(parent, args, context) {
return context.prisma.link.findOne({ where: { id: parent.id } }).postedBy()
}
module.exports = {
postedBy,
}
postedBy
리졸버에서는 우선 prisma 클라이언트를 사용하여 Link
를 불러오고, 다음으로 해당 Link에 대하여 postedBy
메서드를 호출한다.
이 리졸버는 schema.graphql
에 정의된 Link 타입이 가지는 postedBy 필드를 리졸브해야 하므로, postedBy
라는 이름을 가져야 한다.
links
관계 또한 비슷한 방식으로 이루어진다.
// ./hackernews-node/src/resolvers/User.js
function links(parent, args, context) {
return context.prisma.user.findOne({ where: { id: parent.id } }).links()
}
module.exports = {
links,
}
여태 진행하였던 부분을 하나로 모아, 다음의 명령으로 index.js
파일에 합치도록 하자.
// ./hackernews-node/src/index.js
const Query = require('./resolvers/Query')
const Mutation = require('./resolvers/Mutation')
const User = require('./resolvers/User')
const Link = require('./resolvers/Link')
...
const resolvers = {
Query,
Mutation,
User,
Link
}
signup
뮤테이션을 통해 새로운 User
를 데이터베이스에 저장하는 인증 과정을 테스트 해보자.
mutation {
signup(
name: "Alice"
email: "alice@prisma.io"
password: "graphql"
) {
token
user {
id
}
}
}
서버 측 응답에서 인증 token
을 복사하고, 새로운 탭
을 열어 Playground에 접속한다.
새 탭의 왼쪽 하단에 위치한 HTTP HEADERS
패널을 열고 Authorization 헤더를 설정한다.
아래의 코드에서 TOKEN 으로 표시해놓은 부분의 값을 복사한 토큰값으로 대체하면 된다.
{
"Authorization": "Bearer __TOKEN__"
}
이제 해당 탭에서 쿼리 혹은 뮤테이션을 전송할 때마다, 인증 토큰이 포함되어 전송된다.
마지막으로, Authorization
헤더를 포함한 상태로, GraphQL 서버에 다음 뮤테이션을 전송해보자.
mutation {
post(
url: "www.graphqlconf.org"
description: "An awesome GraphQL conference"
) {
id
}
}
서버가 이 뮤테이션을 받으면, post
리졸버를 호출하여 제공된 JWT를 검증
한다.
또한, 새로 생성되는 Link
는 이전에 signup
뮤테이션을 통하여 전달된 User
에 연결된다.
모든 과정을 테스트하기 위해 login
뮤테이션 테스트를 진행하자.
mutation {
login(
email: "alice@prisma.io"
password: "graphql"
) {
token
user {
email
links {
url
description
}
}
}
}
성공적으로 응답한다면 다음과 비슷한 응답이 돌아올 것이다.
{
"data": {
"login": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjanBzaHVsazJoM3lqMDk0NzZzd2JrOHVnIiwiaWF0IjoxNTQ1MDYyNTQyfQ.KjGZTxr1jyJH7HcT_0glRInBef37OKCTDl0tZzogekw",
"user": {
"email": "alice@prisma.io",
"links": [
{
"url": "www.graphqlconf.org",
"description": "An awesome GraphQL conference"
}
]
}
}
}
}
GraphQL 및 jwt를 활용한 인증
과정에 대하여 알아보았다.
다음 포스팅에서는 실시간 GraphQL 구독
에 대하여 알아보도록 한다.