유동

[Express] jwt로 로그인 유지시켜주기 본문

node.js/ExpressJS

[Express] jwt로 로그인 유지시켜주기

동 선 2023. 11. 4. 18:21


이번 글에서는 javascript + express환경에서 jwt를 어떻게 만들고.
어떻게 써먹는지 포스팅할것이다.

  • 간단하게 설명하자면 요청을 보낸 사용자가 로그인한 사용자인지 상태를 체크할수있는거라고만 알아도 충분하다

먼저 jsonwebtoken 라이브러리를 설치해주자

npm install jsonwebtoken

일단 로그인이 성공하였을 경우 jwt를 발급해주니 로그인 api를 먼저 보겠다.

// 로그인 api
router.post("/login", async (req, res, next) => {
    const { email, pw } = req.body;
    const result = {
        message: "",
        data: {}
    };

    try {
        validate(email, "email").checkInput().checkLength(1, ACCOUNT.MAX_EMAIL_LENGTH);
        validate(pw, "pw").checkInput().checkLength(1, ACCOUNT.MAX_PW_LENGTH);

        const sql = "SELECT id, pw FROM account_TB WHERE email = $1";
        const params = [email];
        const data = await pool.query(sql, params);
        if (data.rowCount === 0) {
            throw new BadRequestException("아이디 또는 비밀번호가 올바르지 않습니다");
        }

        const userData = data.rows[0];
        const passwordMatch = bcryptUtil.compare(pw, userData.pw);
        // 입력받은 pw와 암호화된 pw가 일치하지 않은 경우
        if (!passwordMatch) {
            throw new BadRequestException("아이디 또는 비밀번호가 올바르지 않습니다.");
        }
        // 로그인 검증 성공, 토큰발급해줘야지?
        const accessToken = // 토큰 생성 후 변수에 담자
        result.data = {
            userId: userData.id
        }
    } catch (error) {
        return next(error);
    }
    res.send(result);
});

로그인 사용자를 검증한 후 토큰을 만들어야한다. 나같은경우는 module폴더에 jwt를 생성하는 유틸함수를 작성했다.

  • 토큰 발급에 대한 정보를 설정한다.
  • .env파일에 jwt를 만들기위해 필요한 secretKey를 만들어놔야한다. 토큰 해독할때도 필요하다
// /src/module/jwt.js
const jwt = require("jsonwebtoken");
const { secretKey, accessTokenOption } = require("../../config/jwtSetting");

const userSign = (user) => {
      // payload에는 서비스마다 다르겠지만 필요한 최소한의 정보만 담는다.
      // 웬만하면 로그인한 사용자의 pk를 담는다.
    const payload = {
        id: user.id,
        email: user.email
    }
    const secretKey = process.env.JWT_SECRET_KEY,
    const accessTokenOption = {
        "algorithm": "HS256",        // 어떤 알고리즘을 사용할건지
        "expiresIn": "1h",            // 토큰의 유효기간
        "issuer": "inko51366.com"    // 토큰 발행자
    }
    return jwt.sign(payload, secretKey, accessTokenOption);
}

module.exports = {
    userSign
};

이렇게 만들어주고 accessToken이 잘 출력되는지 확인해보자

  • 인하대 학생 아닙니다
// 로그인 api
router.post("/login", async (req, res, next) => {
    // ...생략
      const accessToken = jwtUtil.userSign(userData);
      console.log(accessToken); 
    } catch (error) {
            return next(error);
    }

    res.send(result);
});

아주잘나온다

나온 jwt를 복사해서 jwt.io에 복사 붙여넣기 해보자

설정한 payload가 잘 나오는걸 볼수있다, (만료시간과 발급자) 이처럼 아무나 해독해서 볼수 있기때문에 민감한 정보는 payload에 담으면 안된다 (password, 개인정보 등)

마저 발급해보자

  • 응답 본문(헤더)에 accessToken이라는 이름의 쿠키의 값에 jwt를 담아서 보내주자!이렇게하고 postman으로 테스트해보자
// 로그인 api
router.post("/login", async (req, res, next) => {
  const { email, pw } = req.body;
// ...생략

  const accessToken = jwtUtil.userSign(userData);
  console.log(accessToken);
  result.data = {
    userId: userData.id
  }
} catch (error) {
  return next(error);
}
res.send(result);
});

잘 담긴걸 볼수있을것이다

자 이제 jwt를 발급해줬으니 로그인한 사용자만 이용할수 있는 api에서 로그인했는지 안했는지 테스트해보자

  • 예를들어 회원가입 같은 api는 로그인하지 않은 사용자가 사용할수있을것임
  • 회원가입 router.post("/", async (req, res, next) => { });

예제) 게시글 pk를 받아서 해당 게시글을 조회하는 api

// 특정 게시글 조회 api
// postId
// GET
router.get("/:postId", loginAuth, async (req, res, next) => {
    const { postId } = req.params;
    const result = {
        data: null,
    };

    try {
        validate(postId, "postId").checkInput().isNumber().checkLength(1, maxPostIdLength);

        const sql = `SELECT 
                            post_TB.*,
                            user_TB.name AS author_name,
                            user_TB.profile_img AS author_profile_img
                        FROM
                            post_TB
                        JOIN
                            user_TB
                        ON
                            post_TB.user_id = user_TB.id
                        WHERE
                            post_TB.id = $1`;
        const params = [postId];

        const data = await pool.query(sql, params)
        if (data.rows.length !== 0) {
            result.data = data.rows[0];
            return res.send(result);
        }

        throw new NotFoundException("해당하는 페이지가 존재하지 않습니다");

    } catch (error) {
        next(error);
    }
});
  • 잘 보면 loginAuth라는 미들웨어를 특정 게시글 조회 api가 실행하기 전에 한번 실행된다. 저 loginAuth미들웨어에 가보자
const jwt = require("jsonwebtoken");
const { UnauthorizedException } = require("../module/customError");
const env = require('../config/env');

module.exports = (req, res, next) => {
    // 쿠키에 담긴 토큰을 추출
    const { accessToken } = req.cookies;

    try {
        req.decoded = jwt.verify(accessToken, env.JWT_SECRET_KEY);
        return next();

    } catch (error) {
        return next(new UnauthorizedException("로그인 후 이용가능합니다"));
    }
};
  • jsonwebtoken 라이브러리에서 제공하는 verify 메서드를 통해 우리가 쿠키에 넣어줬던 jwt와 만들어줬던 secretkey를 가지고 토큰을 해독할 수 있다

req.decoded에 payload에 우리가 담아놓은 값들을 사용할 수 있다. 예시를 보자

예제) 게시글 pk, 수정하고싶은 내용들을 body로 받아서 해당하는 게시글을 수정시켜주는 api

고려해야 할 점

    • 수정 api는 게시글을 작성 한 본인만 수정할수 있음.
    • 어 우리는 게시글pk만 받는데 어떻게 본인인지 아닌지 확인하고 수정시켜줌?

이전에 loginAuth에서 토큰을 해독하고 req.decoded에다가 넣어줬으니
저 loginAuth미들웨어를 사용 한 api에서 req.decoded로 로그인 한 유저의 pk를 얻어낼 수 있다!

  •  
// 게시글 수정 api
// postId, title, content
// PUT
router.put("/", loginAuth, async (req, res, next) => {
    const userId = req.decoded.id; //                 <= 1. 여기서 유저의 pk를 저장하고
    const { postId, title, content, images } = req.body;
    const result = {
        isSuccess: false,
        message: ""
    };

    try {
        validate(postId, "postId").checkInput().isNumber().checkLength(1, maxPostIdLength);
        validate(title, "title").checkInput().checkLength(1, maxPostTitleLength);
        validate(content, "content").checkInput().checkLength(1, maxPostContentLength);

          // sql에서 WHERE문으로 id와 user_id를 걸어주자
        const updatePostSql = `UPDATE 
                            post_TB
                        SET
                            title = $1,
                            content = $2,
                            image_key = $3
                        WHERE
                            user_id = $4
                        AND
                            id = $5`;
        const updatePostParams = [title, content, images, userId, postId];
        const updatePostData = await pool.query(updatePostSql, updatePostParams);

        if (updatePostData.rowCount !== 0) {
            result.isSuccess = true;
            return res.send(result);
        }

        throw new BadRequestException("수정 실패, 해당하는 게시글이 존재하지 않습니다");

    } catch (error) {
        next(error);
    }
});

이처럼 로그인이 필요한 api에 저렇게 토큰검증하는 미들웨어를 박아주면 요청 객체(req)에 해독한 정보들을 미리 담아둘수 있음

이후 필요할때 해독해서 쿵짝뽕짝 사용하면 된다