
프로젝트의 로그인 기능을 구현하면서 JWT를 활용하는 방법을 알아보고 과정을 정리해봄.
세션 ID를 이용하는 방식
⦁ 서버에서 사용자 세션을 생성하고, 세션 ID를 HTTPOnly 쿠키로 클라이언트에 전달함.
⦁ 장점 : HTTPOnly 쿠키는 JavaScript로 접근할 수 없기 때문에 XSS(크로스 사이트 스크립팅) 공격에 강함. 서버에서 사용자의 로그인 상태를 유지할 수 있음.
⦁ 단점 : CSRF(사이트 간 요청 위조) 공격에 취약. 서버에서 세션을 관리해야 하므로 확장성 낮음.
JWT (Json Web Token)을 이용하는 방식
⦁ 로그인 서버 요청 시 사용자 정보를 담은 JWT를 발급하고, 클라이언트는 이를 LocalStorage나 쿠키에 저장함.
⦁ 장점 : 서버가 로그인 상태를 유지할 필요없는 stateless(무상태) 방식이므로 확장이 용이함. 서버 부하가 적고, 어떤 서버에서든 JWT를 검증가능하기때문에 확장성이 뛰어남.
⦁ 단점 : JWT를 로컬 스토리지에 저장하면 XSS 공격에 취약할 수 있으므로 보안 조치 필요. HttpOnly 쿠키를 사용해서 CSRF 이슈 줄이는 것이 최적화.
📌 XSS와 CSRF📌
⦁ XSS (크로스 사이트 스크립팅) 공격
악성 스크립트가 주입되어 실행되는 공격 방식
사용자의 브라우저에서 실행되는 JavaScript를 조작해서 정보를 탈취
⦁ CSRF (사이트 간 요청 위조) 공격
사용자의 세션을 가로채어 악의적인 요청을 서버에 전송하는 공격
📌브라우저에서 데이터를 저장하는 방법 : localStorage vs sessionStorage📌
⦁ localStorage
브라우저를 닫아도 유지됨
브라우저 탭 간 공유 가능
⦁ sessionStorage
브라우저 또는 탭을 닫으면 삭제됨
현재 탭에서만 접근 가능
JWT 라이브러리 설치
npm install jsonwebtoken
JWT 기본 구조
⦁ JWT는 헤더, 페이로드, 서명 3가지 부분으로 구성됨.
⦁ Header : 토큰의 타입(JWT)과 해싱 알고리즘을 명시함.
📌 알고리즘 종류📌
HS256: 하나의 비밀키. 빠르고 간단하지만 비밀 키가 노출되면 위험하므로 보안 관리가 중요함.
RS256: 공개키와 비밀키를 분리하여 보안성을 강화할 때 적합. 더 안전하지만 속도가 느림.
{
"alg": "HS256", // 알고리즘
"typ": "JWT" // 타입
}
⦁ Payload : 토큰에 포함할 사용자 정보를 저장.
{
"id": 1,
"name": "홍길동",
"iat": 1616000000, // 발행 시간
"exp": 1616036000 // 만료 시간
}
⦁ Signature : 헤더와 페이로드를 인코딩한 후, 서버의 비밀키를 이용해 해싱한 값. 이 부분이 토큰의 무결성을 보장.
JWT 주요 메서드 정리
⦁ jwt.sign(payload, secret, options) : JWT 토큰 생성
⦁ jwt.verify(token, secret) : 토큰 유효한지 확인하는 검증
⦁ jwt.decode(token) : 디코딩(인코딩된 데이터를 원래형태로 변환.서명검증없이 단순히 페이로드 데이터 읽어옴.)
JWT_SECRET 설정
⦁ JWT를 생성하고 검증할 때 사용하는 비밀 키
⦁ 환경 변수로 설정
JWT_SECRET=SecretKey123!
JWT 생성 (로그인 시 토큰 발급)
로그인 성공 시, 서버에서 JWT를 생성하여 클라이언트에게 반환함.
//회원정보, 토큰
const [user, setUser] = useState(() => {
const storedUser = localStorage.getItem("user");
return JSON.parse(storedUser); // 문자열가져와서 다시객체화
});
const [token, setToken] = useState(localStorage.getItem("token"));
// 로그인확인 API
router.post("/", async (req, res) => {
const { email, pw } = req.body;
try {
//회원확인
const [user] = await db.query(
"select * from member where user_id = ? and pw = ?",
[email, pw]
);
console.log("로그인DB진입:", req.body);
if (user.length === 0) {
return res
.status(404)
.json({ error: "이메일 또는 비밀번호를 확인해주세요." });
}
//토큰생성
const token = jwt.sign(
{ name: user[0].name, user_id: user[0].user_id },
JWT_SECRET,
{ expiresIn: "1h" } // 유효시간
);
//user와 token 반환
return res.status(200).json({
user: {
name: user[0].name,
user_id: user[0].user_id,
},
token,
});
} catch (error) {
console.error("서버로그인오류:", error);
res.status(500).json({
error: "로그인에 실패했습니다. 다시 시도해주세요.",
});
}
});
//로그인
const loginGo = async (email, pw) => {
try {
const res = await axios.post(`${URL}/login`, { email, pw });
const { user, token } = res.data;
console.log("서버응답유저토큰", user, token);
setUser(user); //회원상태업데이트
setToken(token); //토큰상태업데이트
localStorage.setItem("user", JSON.stringify(user)); //객체를 문자열로 저장
console.log("유저저장확인", localStorage.getItem("user"));
localStorage.setItem("token", token); //로컬스토리지저장
alert(`${user.name}님 로그인되었습니다.`);
navigate("/home");
} catch (error) {
console.error("로그인 요청 실패:", error);
ApiError(error);
}
};
// 로그아웃
const logout = () => {
setUser(null); //회원상태초기화
setToken(null); //토큰초기화
localStorage.removeItem("user"); // 로컬스토리지제거
localStorage.removeItem("token"); // 로컬스토리지제거
};
📌토큰 생성 options📌
토큰 생성시 options을 설정해도 토큰은 자동으로 삭제되지 않음. localStorage에그대로 남아 있음. 토큰이 만료되서 서버에서 인증을 거부하는 것 뿐. 로그인 전용페이지에서 토큰이 만료되었는지 확인하기 위해 매번 jwt.verify() 실행이 필요함.
📌로컬스토리지 확인📌
크롬 개발자 도구 > Application > Storage > Local Storage
다음은 보안 강화를 위해 HttpOnly 쿠키 기반 인증 도입하기! 보안도 신경쓰며 보다 안전한 인증 시스템 구축하기👊