엘리스 AI트랙 4기/프로젝트

[Typescript Express] 로그인 로직에 JWT Token Middleware 적용하기

남쪽마을밤송이 2022. 6. 9. 21:58

 [01 JWT Token이란? ] 

  • 인증받은 사용자에게 토큰을 발급해주고, 서버에 요청을 할 때 HTTP 헤더에 토큰을 함께 보내 인증받은 사용자(유효성 검사)인지 확인한다.
  • JWT는 JSON Web Token의 약자로 전자 서명 된 URL-safe (URL로 이용할 수있는 문자 만 구성된)의 JSON이다.
  • JWT는 헤더(header), 페이로드(payload), 서명(signature) 세 가지로 나눠져 있으며, 아래와 같은 형태로 구성되어 있다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • 각 구분은 . 구분자로 나눠 표현되며, 각 값은 BASE64로 인코딩 되어 있다.

 [02 Redis 설치 ] 

 (1) loginRequired.ts 파일 

  • 먼저 로그인 API의 서비스 계층 코드이다.
  • 중간에 로그인을 성공하면 JWT 웹 토큰을 생성해서 토큰 정보를 함께 넘겨주는 코드를 확인할 수 있다. 넘겨받은 token은 클라이언트측(Front End)가 잘 저장해뒀다가 다음에 로그인 기능이 필요한 서비스를 요청할 때 Header에 넣어서 보내줄 것이다.
  • 이 때 주의할 점은 JWT_SECRET_KEY는 유출되면 안되므로 환경변수 파일(.env)로 관리해야 한다는 것과 jwt.sign으로 Encoding을 할 때 넣어줄 Property 이름과 그에 해당하는 값을 잘 설정해주어야 한다는 것이다.
getUser: async (email: string, password: string) => {
    // 이메일 db에 존재 여부 확인
    const user = await User.findByEmail(email);
    if (!user) {
      throw new Error("해당 이메일은 가입 내역이 없습니다. 다시 한 번 확인해 주세요.");
    }

    // 비밀번호 일치 여부 확인
    const correctPasswordHash: string = user.password;
    const isPasswordCorrect: boolean = await bcrypt.compare(password, correctPasswordHash);
    if (!isPasswordCorrect) {
      throw new Error("비밀번호가 일치하지 않습니다. 다시 한 번 확인해 주세요.");
    }

    // 로그인 성공 -> JWT 웹 토큰 생성
    const secretKey: string = process.env.JWT_SECRET_KEY || "jwt-secret-key";
    const token = jwt.sign({ user_id: user.pk_user_id }, secretKey);

    // 반환할 loginuser 객체를 위한 변수 설정
    const { pk_user_id, user_name, gender, age_range, job } = user;

    const loginUser = {
      token,
      pk_user_id,
      user_name,
      email,
      password,
      gender,
      age_range,
      job,
    };

    return loginUser;
  }

 (2) loginRequired.ts 파일 

  • 그리고 middlewares 밑에 loginRequired.ts 파일을 만들어준다. 이 파일은 로그인이 필요한 서비스를 이용할 때 로그인 된(토큰이 있는) 사용자가 보낸 요청인지를 확인하는 로직이다.
  • req.headers["authorization"]에는  Bearer {JWT_TOKEN}  의 형식으로 구성되어 있기 때문에 6번째 줄로 토큰값만 추출해준다.
  • 그리고 토큰이 없다면 서비스 요청을 거부하고, 정상적으로 들어있다면 이를 decode 해주는 과정이 필요하다.
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const loginRequired = (req: Request, res: Response, next: NextFunction) => {
  // request 헤더로부터 authorization bearer 토큰을 받음.
  const userToken = req.headers["authorization"]?.split(" ")[1] ?? "null";

  // 이 토큰은 jwt 토큰 문자열이거나, 혹은 "null" 문자열임.
  // 토큰이 "null" 일 경우, loginRequired 가 필요한 서비스 사용을 제한함.
  if (userToken === "null") {
    if (!process.env.development) {
      console.log("서비스 사용 요청이 있습니다.하지만, Authorization 토큰이 없습니다.");
    }
    res.status(400).send("로그인한 유저만 사용할 수 있는 서비스입니다.");
    return;
  }

  // 해당 token 이 정상적인 token인지 확인 -> 토큰에 담긴 user_id 정보 추출
  try {
    if (!process.env.JWT_SECRET_KEY) {
      throw new Error("JWT_SECRET_KEY가 존재하지 않습니다.");
    }
    const secretKey: string = process.env.JWT_SECRET_KEY;
    const jwtDecoded: any = jwt.verify(userToken, secretKey);
    const userId = jwtDecoded.user_id;
    req.currentUserId = userId;
    next();
  } catch (error) {
    res.status(400).send("정상적인 토큰이 아닙니다. 다시 한 번 확인해 주세요.");
    return;
  }
};

export { loginRequired };
  • 이 때 "TypeScript"이기 때문에 주의해야 할 점이 두 가지 있었다.
  • 첫 째, jwtDecoded.user_id를 접근할 때 윗 줄의 verify 함수의 결과값을 string | JwtPayload 형식으로 인식해 string 형식의 경우 user_id 속성이 없다는 오류를 뱉어낸다.
    • 이 오류는 해결이 잘 안돼서 코치님께도 여쭤봤는데 스택 오버플로우를 참고하니 any로 하는게 가장 빠른 해결책이라 어쩔 수 없이 위와 같이 any로 지정했다. 
    • typescript에서 any를 쓰는건 최대한 지양해야 하기 때문에 나중에 코드 리팩토링 할 때 interface로 바꾸는 방법을 고민해봐야겠다.

  • 두 번째, Request type에  currentUserId 라는 property가 들어있지 않기 때문에 오류가 날 것이다.
    • 그렇다면 Request type에 property를 추가하는 과정이 필요한 것인데, tsconfig.json 파일의 설정 변경을 통해 해결할 수 있다.
    • 아래와 같이 compilerOptions에 사진에서 13번째 옵션을 추가하고 custom type을 만들 폴더 경로와 원래 타입을 참조하는 ./node_modules/@types를 넣어준다.

  • 그리고 그 경로로 가서 폴더와 파일을 생성해주는데, 이 때 주의할 점은 꼭 아래와 같이 폴더명은  express , 파일명은  index.d.ts 로 정해줘야 한다는 것이다.
    • customType 폴더명은 원하는걸로 짓고 typeRoots 경로만 잘 지정해주면 된다.

  • 그리고 아래 코드에서 본인의 코드에 맞게 타입을 지정해주면 된다.
    • 나는 encoding(sign함수)할 때 Users 모델의 pk_user_id를 user_id 속성으로 넣어주었기 때문에 다음과 같이 지정해줬다.
import { Users } from "../../db/models/user";

declare global {
  namespace Express {
    interface Request {
      currentUserId?: Users["pk_user_id"];
    }
  }
}
  • 이렇게 적용하면 TypeScript에서도 쉽게(?) JWT Token을 사용할 수 있다!