[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을 사용할 수 있다!
'엘리스 AI트랙 4기 > 프로젝트' 카테고리의 다른 글
[TypeScript Express] Sequelize로 MySQL 기본 모델 생성하기 (0) | 2022.06.14 |
---|---|
[TypeScript Express] 라우터, 컨트롤러, 미들웨어 분리 (0) | 2022.06.10 |
[Typescript Express] Sequelize로 MySQL 연결하기 (0) | 2022.06.08 |
Oracle Cloud 인스턴스에 MySQL 서버 구축_3탄 (1) | 2022.06.06 |
Oracle Cloud 인스턴스에 MySQL 서버 구축_2탄 (0) | 2022.06.05 |