본문 바로가기

DayDream Project/BackEnd

[Node.js] 유저 회원가입/로그인 구현하기 With Mongoose - 2

이제 앞서 구현한 내용에 JWT를 살포시 얹어보자.

이에 앞서 JWT 관련 라이브러리를 설치하자. `npm install jsonwebtoken`

 

/Controllers/authController.js

const signToken = (id) =>
  jwt.sign({ id: id }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_EXPIRES_IN,
  });

 

이렇게 간단하게 JWT 토큰을 생성할 수 있다.

여기서 JWT_SECRET, JWT_EXPIRES_IN을 설정해줘야 한다.

JWT_SECRET는 해싱하는 나만의 키? 같은 느낌으로, 알려지면 안되니 config.env 파일에 설정해두고 사용하자. 값은 내가 원하는 값 아무렇게나 설정해도 되는 것 같다. 짧지 않게!

또한 JWT 토큰은 인증에 사용되기 때문에 만료시간이 필요하다. 사용자가 오랫동안 안쓰다 들어왔을 때 초기화를 해줘야 하기 때문에 이 EXPIRE TIME을 설정해 주는 것이다. 이 또한 환경변수에 저장해두고 사용하도록 하자.

 

그리고 중복되는 코드를 줄이기 위해, 토큰을 만들고 값을 전달하는 부분을 따로 함수로 관리하도록 하자.

const createSendToken = (user, statusCode, res) => {
  const token = signToken(user._id);
  const cookieOptions = {
    expires: new Date(
      Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000,
    ),
    httpOnly: true,
  };
  if (process.env.NODE_ENV === "production") cookieOptions.secure = true;
  res.cookie("jwt", token, cookieOptions);

  // delete password from output
  user.password = undefined;

  res.status(statusCode).json({
    status: "success",
    token,
    user: {
      user,
    },
  });
};

 

생성된 유저의 ID를 기반으로 토큰을 생성하고, 이를 쿠키에 담아 전달한다.

쿠키의 옵션에는 아까 설정한 만료기간을 넣어주고, 탈취당하지 않도록 httpOnly 옵션을 켜준다.

또한 secure 옵션은 SSL이 적용된 https 요청에 대해서만 통과를 시키는 옵션인데, 개발환경에서는 http로 진행할 것이기 때문에 production 환경에서만 이를 설정해주자.

그리고 전에 비밀번호 검증을 위해 password 필드를 가져왔기 때문에, 이를 없애주고 response를 보내면 끝!

 

그리고 이를 적용하자.

exports.signup = catchAsync(async (req, res, next) => {
  const newUser = await User.create({
    name: req.body.name,
    email: req.body.email,
    password: req.body.password,
    passwordConfirm: req.body.passwordConfirm,
    passwordChangedAt: req.body.passwordChangedAt,
    role: req.body.role,
    height: req.body.height,
    weight: req.body.weight,
    age: req.body.age,
  });

  createSendToken(newUser, 201, res);
});

exports.login = catchAsync(async (req, res, next) => {
  const { email, password } = req.body;

  // 1) check if email and password exist
  if (!email || !password) {
    return next(new AppError("Please provide email and password", 400));
  }
  // 2) check if user exists && password is correct
  const user = await User.findOne({ email }).select("+password");
  const correct = await user.correctPassword(password, user.password);
  if (!user || !correct) {
    return next(new AppError("Incorrect email or passsword", 401));
  }

  // 3) if everything ok, send token to client
  createSendToken(user, 200, res);
});

 

회원가입을 하고 로그인을 할 때, createSendToken을 사용해 쉽게 토큰을 만들고 관리할 수 있다.

그리고, 전에 createUser를 구현했는데 보통 회원가입은 signin이기 때문에 따로 빼줬다. 그리고 필드값도 특정해줬다. 혹시 모를 이상한 값의 주입이 있을 수도 있으니!

 

이제 마지막으로, 특정 기능을 수행했을 때 이 사람이 인증된 사람인지를 검증하는 함수를 구현해보겠다.

내 정보를 확인하는데 권한이 없는, 외부의 사람이 확인하면 이상하니까!

 

/Controllers/authController.js

exports.protect = catchAsync(async (req, res, next) => {
  let token;
  // 1) Getting token and check of it's there
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith("Bearer")
  ) {
    token = req.headers.authorization.split(" ")[1];
  }

  if (!token) {
    return next(new AppError("You are not logged in !", 401));
  }
  // 2) Verification token
  const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);

  // 3) Check if user still exists
  const currentUser = await User.findById(decoded.id);
  if (!currentUser) {
    return next(new AppError("The user dose not exist", 401));
  }
  // 4) Check if user changed password after JWT was issued
  if (currentUser.changedPasswordAfter(decoded.iat)) {
    return next(
      new AppError("user recentlhy changed password. please login again", 401),
    );
  }

  // Grant access to protected route
  req.user = currentUser;
  next();
});

 

HTTP 통신은 기본적으로 Stateless이기 때문에, 어떤 요청을 할 때 항상 나의 정보를 보내줘야 한다.

즉 클라이언트 측에서는 로그인 했을 때 받은 JWT 토큰을 계속 헤더에 담아 나를 인증해줘야 하는 것!

아직 클라이언트를 구현하진 않았지만, 보통 Authorization 헤더에 이를 담아 전달한다.

형식은 Authorization: Bearer [JWT] 의 형식인데, 약속같은 것 같다.

 

그래서 가장 먼저 할 일은 Authorization 헤더가 설정되어 있는지, 그리고 Bearer로 시작하는지를 체크하는 것이다.

아니라면 에러를, 맞다면 이를 공백 기준으로 자른 뒤 1번 인덱스의 값을 가져온다. ['Bearer', 'JWTToken']

 

이를 검증하는 방법은 jwt.verify(token, SECRET_KEY)의 방식인데,  verify는 async의 형식이고, 우리는 모두 Promise의 형태로 관리해왔기 때문에 해당 패턴을 지키기 위해 이를 프로미스화 시켜줬다.

이렇게 decoded에는 해당 유저의 정보가 들어간다.

 

다음은 이렇게 decoded된 유저의 정보에서 id를 통해 유저를 찾고, 유효한지 판단한다.

 

마지막으로, JWT 토큰이 발급되고 난 뒤 비밀번호가 바뀌었을 때를 체크하는데, 이 부분은 추가적으로 넣어줬다.

아무튼 이제 이렇게 검증된 유저의 값으로 req.user를 갈아껴주면 OK!

 

앞에서 모두 걸리지 않고 next() 까지 왔다면 해당 유저는 올바르게 등록된 유저임을 확인해주는 것이다.

Node는 이러한 미들웨어를 잘 설정하는 것이 중요한 것 같다.

 

아무튼 대충 이 부분은 완료가 됐고 .. 권한 설정 부분은 간단하게

exports.restritTo =
  (...roles) =>
  (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return next(new AppError("권한이 없습니다."), 403);
    }
    next();
  };

 

이렇게 설정을 해주고, 라우터 부분에서

router
  .route("/:id")
  .get(
    authController.protect,
    authController.restritTo("admin"),
    userController.getOneUser,
  );

 

 요런 식으로 넣어준 권한만 사용할 수 있게 구현했다.

사실 인증 부분에서 Access Token, Refresh Token으로 나눠서 Access Token은 만료기간을 짧게 두어 세션을 키고 짧은 시간이 지나면 만료시키고, 비교적 긴 만료시간을 가진 Refresh Token을 통해 Access Token을 재발급 하는 방식을 많이 사용하는데, 일단은 간단하게 구현해봤다. 추후 시간이 된다면 해당 방식으로 리팩토링 할 예정!