공부하기/React

[React] 블로그 만들기 8 - 토큰 발급 및 검증

다섯자두 2022. 3. 15. 17:21

다음은 책 리액트를 다루는 기술을 읽고 공부한 내용을 바탕으로 작성된 글입니다.


 

이제 클라이언트에서 사용자 로그인 정보를 지닐 수 있도록 토큰을 발급하자.

JWT 토큰을 만들기 위해서 jsdonwebtoken 이라는 모듈을 설치한다.

$> yarn add jsonwebtoken

 

1. 비밀키 설정하기

.env 파일을 열어서 JWT 토큰을 만들 때 사용할 비밀키를 만든다.

비밀키는 문자열로 아무거나 상관없으며, 길이도 자유다.

 

이 비밀키는 나중에 토큰의 서명을 만드는 과정에서 사용되므로 외부에 공개되면 절대로 안 된다.

비밀키가 공개되면 누구든지 마음대로 토큰을 발급할 수 있게 된다.

 

- .env

PORT=4000
MONGO_URI= mongodb://localhost:27017/blog
JWT_SECRET= o3v3mvw834tynveskjfnvksdvuw93p8nywvjklfsbvyp98wyesrfbnjvsdg98wybvnwerslidjipw098ut2p04u8t9865v468374p83745nv6u3498p5435yvpt98laiw4faij4f8j3w8fi83f4hwuifhjasdhfuqh2if7hafa

 

2. 토큰 발급하기

user 모델 파일에서 generateToken 이라는 instance method를 만든다.

 

- src/models/user.js

UserSchema.methods.generateToken = function () {
  const token = jwt.sign(
    // 첫 번째 파라미터에는 토큰 안에 집어넣고 싶은 데이터를 넣는다.
    {
      _id: this.id,
      username: this.username,
    },
    // 두 번째 파라미터에는 JWT 암호를 넣는다.
    // eslint-disable-next-line no-undef
    process.env.JWT_SECRET,
    {
      // 7일 동안 유효함
      expiresIn: '7d',
    },
  );
  return token;
};

 

이제 회원가입과 로그인에 성공했을 때 이 메서드를 통해 토큰을 생성해 사용자에게 전달하면 된다.

사용자 토큰을 쿠키에 담아서 사용하도록 하였다.

 

- src/api/auth/auth.ctrl.js

/*
POST /api/auth/register
{
  username: 'subbni',
  password: 'mypassword123',
}
*/
export const register = async (ctx) => {
  // 회원가입
  const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(20).required(),
    password: Joi.string().required(),
  });

  const result = schema.validate(ctx.request.body);
  if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }

  const { username, password } = ctx.request.body;
  try {
    //username이 이미 존재하는지 확인
    const exists = await User.findByUsername(username);
    if (exists) {
      ctx.status = 409; // conflict
      return;
    }

    const user = new User({
      username,
    });
    await user.setPassword(password);
    await user.save(); // 데이터베이스에 저장

    // 응답할 데이터에서 hashedPassword 필드 제거
    ctx.body = user.serialize();
    const token = user.generateToken();
    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
      httpOnly: true,
    });
  } catch (e) {
    ctx.throw(500, e);
  }
};

/*
POST /api/auth/login
{
  username: 'subbni',
  password: 'mypassword123',
}
*/
export const login = async (ctx) => {
  // 로그인
  const { username, password } = ctx.request.body;

  // username, password 없으면 에러 처리
  if (!username || !password) {
    ctx.status = 401;
    return;
  }

  try {
    const user = await User.findByUsername(username);
    // 계정이 존재하지 않으면 에러 처리
    if (!user) {
      ctx.status = 401;
      return;
    }
    const valid = await user.checkPassword(password);
    // 비밀번호가 일치하지 않는 경우
    if (!valid) {
      ctx.status = 401;
      return;
    }
    ctx.body = user.serialize();
    const token = user.generateToken();
    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, //7일
      httpOnly: true,
    });
  } catch (e) {
    ctx.throw(500, e);
  }
};

 

- Postman

 

로그인 - 토큰 발급 테스팅

테스팅 결과 성공적으로 Set-Cookie 라는 헤더로 토큰이 전달되는 것을 확인하였다. 

 

3. 토큰 검증하기

이제 사용자의 토큰을 확인 후 검증하는 작업이다.

이 작업은 미들웨어를 생성하여 처리하였다. 

 

- src/lib/jwtMiddleware.js

import jwt from 'jsonwebtoken';
import User from '../models/user';

const jwtMiddleware = async (ctx, next) => {
  const token = ctx.cookies.get('access_token');
  // 토큰이 없는 경우
  if (!token) return next();
  try {
    // eslint-disable-next-line no-undef
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    ctx.state.user = {
      _id: decoded._id,
      username: decoded.username,
    };

    // 토큰의 남은 유효기간이 3.5일 미만일 경우 재발급 처리
    const now = Math.floor(Date.now() / 1000);
    if (decoded.exp - now < 60 * 60 * 24 * 3.5) {
      const user = await User.findById(decoded._id);
      const token = user.generateToken();
      ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 7, //7일
        httpOnly: true,
      });
    }
    return next();
  } catch (e) {
    // 토큰 검증 실패
    return next();
  }
};

export default jwtMiddleware;

 

사용자의 cookie로부터 token을 받아온다. 

해당 token을 디코딩한다. (jsonwebtoken)

디코딩한 정보를 ctx.state에 넣어준다 -> API에서 ctx.state를 통해 디코딩 정보에 접근할 수 있게된다.

 

이제 이 미들웨어를 app에 적용한다.

 

- src/main.js

 

...

import jwtMiddleware from './lib/jwtMiddleware';

// 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
// eslint-disable-next-line no-undef
const { PORT, MONGO_URI } = process.env;

mongoose
  .connect(MONGO_URI)
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((e) => {
    console.log(e);
  });

const app = new Koa();
const router = new Router();

// 라우터 설정
router.use('/api', api.routes()); // api 라우트 적용

// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());
// 라우터 적용 전에 jwtMiddleware 적용
app.use(jwtMiddleware);

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

...

 

이제 check API를 구현한다.

 

- src/api/auth/auth.ctrl.js

/*
GET /api/auth/check
*/
export const check = async (ctx) => {
  // 로그인 상태 확인
  const { user } = ctx.state;
  if (!user) {
    // 로그인 중 아님
    ctx.status = 401; // Unauthorized
    return;
  }
  ctx.body = user;
};

export const logout = async (ctx) => {
  // 로그아웃
};

 

- Postman

로그인 여부 확인 테스팅

check API 호출 결과 서버가 성공적으로 응답함을 확인하였다.