Apollo Server로 GraphQL API 서버 개발하기 (3)

곽태욱·2020년 2월 13일
4

깃허브 : https://github.com/rmfpdlxmtidl/movie-server

이번에는 회원가입/로그인/로그아웃 기능을 구현해볼 것이다.

이 글에서 설명하는 내용은 개인적으로 공부하면서 생각한 것으로 이 방식을 다른 프로젝트에 그대로 도입하면 보안상 문제가 생길 수도 있으니 주의하세요

1. 패키지 설치

> yarn add bcrypt crypto-js csprng
  • bcrypt

  • crypto-js

  • csprng

2. 스키마 수정

// src/graphql/typeDefs.js
import { gql } from 'apollo-server';

const typeDefs = gql`
  ...
  type User {
    id: Int!
    name: String
    ID: String!
    passwordHash: String
    role: [String!]!
    token: String
  }
  
  type Query {
    ...
    me: User!
  }
  type Mutation {
    ...
    signup(name: String!, ID: String!, password: String!): Boolean!
    login(ID: String!, password: String!): User
    logout: Boolean!
  }
`;

export default typeDefs;

User

사용자의 어떤 데이터를 저장할지 정의한다. 임의로 설정해도 된다. nameID, password는 사용자 요청으로부터 받아 저장하고, 그외 정보는 리졸버에서 계산해 저장한다.

Query - me

Mutation - signup

이름과 아이디, 비밀번호를 받아 데이터베이스에 등록하는 API이다. 성공적으로 가입하면 true를, 아니면 false를 반환한다.

Mutation - login

가입한 아이디와 비밀번호를 통해 로그인을 진행한다. 성공적으로 인증이 완료되면 해당 사용자 정보(User)를, 아니면 null을 반환한다.

Mutation - logout

로그인 상태일 때 서버에 등록된 사용자의 로그인 정보를 초기화하는 API이다. 성공적으로 로그아웃했다면 true를, 아니면 false을 반환한다.

3. 리졸버 수정

// src/graphql/resolvers.js
...
import { AuthenticationError, ForbiddenError } from 'apollo-server';
import bcrypt from 'bcrypt';
import sha256 from 'crypto-js/sha256';
import rand from 'csprng';

const resolver = {
  Query: {
    ...
    users: (_, __, { user }) => {
      if (!user) throw new AuthenticationError('Not Authenticated');
      if (!user.roles.includes('admin'))
        throw new ForbiddenError('Not Authorized');

      return users;
    },
    me: (_, __, { user }) => {
      if (!user) throw new AuthenticationError('Not Authenticated');

      return user;
    }
  },
  Mutation: {
    ...
    signup: (_, { name, ID, password }) => {
      if (users.find(user => user.ID === ID)) return false;

      bcrypt.hash(password, 10, function(err, passwordHash) {
        const newUser = {
          id: users.length + 1,
          name,
          ID,
          passwordHash,
          role: ['user'],
          token: ''
        };
        users.push(newUser);
      });

      return true;
    },
    login: (_, { ID, password }) => {
      let user = users.find(user => user.ID === ID);

      if (!user) return null; // 해당 ID가 없을 때
      if (user.token) return null; // 해당 ID로 이미 로그인되어 있을 때
      if (!bcrypt.compareSync(password, user.passwordHash)) return null; // 비밀번호가 일치하지 않을 때

      user.token = sha256(rand(160, 36) + ID + password).toString();
      return user;
    },
    logout: (_, __, { user }) => {
      if (user?.token) { // 로그인 상태라면(토큰이 존재하면)
        user.token = '';
        return true;
      }

      throw new AuthenticationError('Not Authenticated'); // 로그인되어 있지 않거나 로그인 토큰이 없을 때
    }
  }
};

export default resolvers;

`users

`

me

signup

기존에 사용자 ID가 이미 존재하면 false를 반환한다. 그 후 사용자로부터 받은 passwordbcrypt를 이용해 암호화해서 데이터베이스에 저장하고 true를 반환한다. 데이터베이스에 사용자 아이디는 평문으로 저장되지만 비밀번호는 암호화되어 저장된다.

login

요청 데이터에 있는 ID로 사용자를 검색한 후, 해당 ID가 없거나, 토큰이 이미 존재하거나, 비밀번호가 일치하지 않으면 null을 반환한다. 그 외 경우 랜덤 토큰을 생성하고 해당 사용자 데이터를 반환한다. 사용자 정보에 토큰이 존재한다는 것은 로그인 상태라는 것을 의미한다.

logout

만약 logout API 요청 시 로그인 상태라면, 로그아웃 시 해당 사용자 토큰은 ''로 초기화된다.

4. Context 생성

// src/graphql/context.js
import users from '../database/users';

const context = ({ req }) => {
  const token = req.headers.authorization || '';
   // 로그인되어 있지 않거나 로그인 토큰이 없을 때
  if (token.length != 64) return { user: null };

  const user = users.find(user => user.token === token);
  return { user };
};

export default context;

Context는 모든 GraphQL API 요청이 불릴 때마다 항상 실행되는 함수이다. 보통 Context에 사용자 인증 정보를 저장해서 특정 API 실행 권한이 있는지 확인하는 용도로 사용한다.

코드 설명

req.headersauthorization 항목이 없다면 { user: undefined }을 반환하고, 이는 로그인 전을 의미한다.

HTTP HEADERSauthorization 항목으로 토큰이 전달됐다면 이는 로그인 후를 의미하고, 이 토큰을 통해 로그인된 사용자를 특정할 수 있고 해당 사용자 정보를 반환한다.

5. 서버에 Context 추가

// src/index.js
...
import context from './graphql/context';

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

ApolloServercontext를 추가한다.

프로젝트 구조

6. 서버 API 테스트

localhost:4000으로 접속해서 GraphQL Playground로 테스트한다.

회원가입

true가 반환되면 성공적으로 회원가입을 완료한 것이다.

로그인

사용자 정보가 반환되면 성공적으로 로그인된 것이다. 그리고 토큰값은 매 로그인 시마다 달라진다.

로그아웃

로그인 성공 후 반환된 사용자 토큰을 HTTP HEADERAuthorization로 전달한다. true가 반환되면 성공적으로 로그아웃을 완료한 것이다.

참고 : Salt Hashing https://starplatina.tistory.com/entry/%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%ED%95%B4%EC%8B%9C%EC%97%90-%EC%86%8C%EA%B8%88%EC%B9%98%EA%B8%B0-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0
profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.

0개의 댓글