users.js도 channels.js에 적용한 validate 미들웨어를 모두 적용했어요.
router.post(
"/login",
[
body("email").notEmpty().isEmail().withMessage("email 형식이 아닙니다."),
body("password")
.notEmpty()
.isString()
.withMessage("password must be a string"),
validate,
],
async (req, res) => {
try {
const { email, password } = req.body;
const sql = `SELECT * FROM users WHERE email = ? AND password = ?`;
const values = [email, password];
const [results] = await conn.query(sql, values);
if (results.length > 0) {
const user = results[0];
return res.json(user);
} else {
return res
.status(400)
.json({ message: "이메일 또는 비밀번호가 틀렸습니다." });
}
} catch (err) {
console.error(err);
return res.status(500).json({ message: "서버 오류가 발생했습니다." });
}
},
);
body의 유효성을 검증했으니 기존 유효성 검사 부분을 제거했어요.
인증(Authentication)은 참이라는 근거가 있는 무언가를 확인하거나 확증하는 행위로 로그인을 예로 들 수 있다.
인가(Authorization)는 리소스에 대한 접근 권한 및 정책을 지정하는 기능이다.
예를 들어 관리자로 로그인할 때와 일반 사용자로 로그인할 때 사용할 수 있는 기능의 차이를 예를 볼 수 있다.
쿠키는 HTTP의 일종으로서 인터넷 사용자가 어떤 웹사이트를 방문할 경우 사용자의 웹 브라우저를 통해 인터넷 사용자의 컴퓨터나 다른 기기에 설치되는 작은 기록 정보 파일이에요.
장점
서버가 저장하지 않아서 서버의 저장 공간을 사용하지 않아요.
Stateless 해서 RESTful 해요.
단점
브라우저가 가지고 있기 때문에 임의의 수정과 삭제가 쉬워요.
가로채기 쉽기 때문에 보안에 취약해요.
세션은 반영구적이고 상호작용적인 정보 교환을 전제하는 둘 이상의 통신 장치나 컴퓨터와 사용자 간의 대화나 송수신 연결상태를 의미하는 보안적인 다이얼로그 및 시간대를 가리켜요.
장점
브라우저가 아닌 서버가 관리하기 때문에 보안이 비교적 좋아요.
단점
서버가 이를 저장, 즉 서버 자원을 사용한다는 뜻이에요.
Stateless 하지 못해요.
쿠키와 세션의 단점을 어느 정도 보완한 것입니다.
JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준이에요.
장점
암호화되어 있기 때문에 보안에 강하고, Stateless 해요.
서버에 저장하지 않기 때문에 서버에 부담이 적어요.
추가적으로 토큰을 발행하는 서버를 따로 만들 수도 있어요.
구조
HEADER : 암호화 시 사용한 알고리즘(alg)과 토큰의 형태(typ)를 저장
PAYLOAD : 보내고자 하는 데이터
VERIFY SIGNATURE : 데이터를 보내는 사람을 증명하는 것 즉 서명이에요.
만약 payload 값이 바뀌면 서명값이 통째로 바뀌기 때문에 믿고 사용할 수 있어요.
jsonwebtoken 이라는 라이브러리가 필요해요.
import jwt from "jsonwebtoken";
let token = jwt.sign({ foo: "bar" }, "shhhh");
// token 생성 = jwt 서명을 한 것 (페이로드, 나만의 암호키) + SHA256
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3NzAxMzc2NzR9.Ham8UjzEsnXU0C5KGfMQKWm7hPHVsR6KbDdutVSf3TA
jwt를 불러와 secretKey로 서명을 하여 jwt를 생성했어요.
// 검증
// 만약, 검증에 성공하면 payload 값을 확인할 수 있음!
const decoded = jwt.verify(token, "shhhh");
console.log(decoded);
// { foo: 'bar', iat: 1770138014 }
같은 시크릿으로 verify 를 호출하면 payload도 가져올 수 있어요.
node.js에서는 환경변수를 .env 에 저장해요.
키값을 파스칼케이스로 지정해야해요.
CLIENT_ID='abc'
처럼 지정하고 process.env.CLIENT_ID 와 같이 사용할 수 있어요.
저희는 JWT_SECRET을 사용했어요.
JWT_SECRET=shhhh 로 해놓고, 다음과 같이 사용했어요.
import jwt from "jsonwebtoken";
let token = jwt.sign({ foo: "bar" }, process.env.JWT_SECRET);
import dotenv from "dotenv";
// token 생성 = jwt 서명을 한 것 (페이로드, 나만의 암호키) + SHA256
console.log(token);
// 검증
// 만약, 검증에 성공하면 payload 값을 확인할 수 있음!
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded);
// { foo: 'bar', iat: 1770138014 }
이렇게 환경변수는 숨기고 로직은 구현할 수 있게 되었어요.
dotenv.config()는 Node.js 프로젝트에서 루트 디렉토리에 있는 .env 파일을 읽어와 그 안의 환경 변수(key=value 형태)를 process.env 객체로 로드하는 역할을 해요.
import jwt from "jsonwebtoken"; import dotenv from "dotenv"; dotenv.config();많은 import 문에서 jwt 관련 모듈만 따로 개행해서 모아놨어요.
보통은 쿠키로 돌려주는 것이 관례이나, 우선 jwt 발급이 제대로 되었는지 확인하기 위해 바디에 담아주게끔 했어요.
if (user && user.password === password) {
// 토큰 발급
const token = jwt.sign(
{
email: user.email,
name: user.name,
},
process.env.JWT_SECRET,
);
return res.json({ token });
}
});

잘 발급되었으니, 쿠키에 담아주는 것으로 바꿔보려고해요.
이를 위해 라이브러리를 설치할게요. 
cookie-parser 는 req에서 쿠키를 가져오는 곳에서도 쓰기 때문에 해당 라이브러리를 채택했어요.
if (user && user.password === password) {
// 토큰 발급
const token = jwt.sign(
{
email: user.email,
name: user.name,
},
process.env.JWT_SECRET,
);
return res.cookie("token", token).end();
}
res.cookie() 만 쓰면 응답이 끝나지 않으니 end를 명시했어요.
이제는 로그인 실패시 예외처리를 추가해야해요.
강의에서는 로그인 실패시 403을 주는데, 저는 403이 인가, 즉 RBAC시에 사용해야한다고 생각하기 때문에 401 Unauthorized를 주는 게 맞다고 생각했어요.
쿠키의 보안을 위해 httponly: true 를 설정하기로 했어요.
이렇게 해야 쿠키를 오로지 API 호출에만 사용할 수 있게 해서 XSS 공격을 막을 수 있어요.
return res
.cookie("token", token, {
httpOnly: true,
})
.end();
다음과 같이 httpOnly 를 객체로 전달할 수 있어요.

엑세스 토큰은 탈취당할 수 있으니 유효기간을 짧게 하는 것이 보안에 좋아요.
토큰 발급 로직에 expiresIn 속성을 넣어줄 수 있어요.
원한다면 issuer 와 같은 속성도 넣어줄 수 있어요.
// 토큰 발급
const token = jwt.sign(
{
email: user.email,
name: user.name,
},
process.env.JWT_SECRET,
{
expiresIn: "1d",
issuer: "my-app",
},
);

jwt.io에 들어가보니 유효기간과 발급인까지 payload에 잘 나온 것을 볼 수 있었어요.
이제 jwt가 있으니 인가를 제대로 구현할 수 있게 되었어요.