[다이어리프로젝트] 로그인 인증 방식 - JWT

송나·2025년 3월 16일

💡 JWT 기반 인증

프로젝트의 로그인 기능을 구현하면서 JWT를 활용하는 방법을 알아보고 과정을 정리해봄.

📑 로그인 인증 방식 : 세션 vs JWT

  1. 세션 ID를 이용하는 방식
    ⦁ 서버에서 사용자 세션을 생성하고, 세션 ID를 HTTPOnly 쿠키로 클라이언트에 전달함.
    ⦁ 장점 : HTTPOnly 쿠키는 JavaScript로 접근할 수 없기 때문에 XSS(크로스 사이트 스크립팅) 공격에 강함. 서버에서 사용자의 로그인 상태를 유지할 수 있음.
    ⦁ 단점 : CSRF(사이트 간 요청 위조) 공격에 취약. 서버에서 세션을 관리해야 하므로 확장성 낮음.

  2. JWT (Json Web Token)을 이용하는 방식
    ⦁ 로그인 서버 요청 시 사용자 정보를 담은 JWT를 발급하고, 클라이언트는 이를 LocalStorage나 쿠키에 저장함.
    ⦁ 장점 : 서버가 로그인 상태를 유지할 필요없는 stateless(무상태) 방식이므로 확장이 용이함. 서버 부하가 적고, 어떤 서버에서든 JWT를 검증가능하기때문에 확장성이 뛰어남.
    ⦁ 단점 : JWT를 로컬 스토리지에 저장하면 XSS 공격에 취약할 수 있으므로 보안 조치 필요. HttpOnly 쿠키를 사용해서 CSRF 이슈 줄이는 것이 최적화.

📌 XSS와 CSRF📌
⦁ XSS (크로스 사이트 스크립팅) 공격
악성 스크립트가 주입되어 실행되는 공격 방식
사용자의 브라우저에서 실행되는 JavaScript를 조작해서 정보를 탈취
⦁ CSRF (사이트 간 요청 위조) 공격
사용자의 세션을 가로채어 악의적인 요청을 서버에 전송하는 공격

📌브라우저에서 데이터를 저장하는 방법 : localStorage vs sessionStorage📌
⦁ localStorage
브라우저를 닫아도 유지됨
브라우저 탭 간 공유 가능
⦁ sessionStorage
브라우저 또는 탭을 닫으면 삭제됨
현재 탭에서만 접근 가능


📑 JWT 설치 및 사용법

  1. JWT 라이브러리 설치
    npm install jsonwebtoken

  2. JWT 기본 구조
    ⦁ JWT는 헤더, 페이로드, 서명 3가지 부분으로 구성됨.
    ⦁ Header : 토큰의 타입(JWT)과 해싱 알고리즘을 명시함.

    📌 알고리즘 종류📌
    HS256: 하나의 비밀키. 빠르고 간단하지만 비밀 키가 노출되면 위험하므로 보안 관리가 중요함.
    RS256: 공개키와 비밀키를 분리하여 보안성을 강화할 때 적합. 더 안전하지만 속도가 느림.

    {
      "alg": "HS256", //  알고리즘
      "typ": "JWT" // 타입
    }	  

    ⦁ Payload : 토큰에 포함할 사용자 정보를 저장.

    {
      "id": 1,
      "name": "홍길동",
      "iat": 1616000000,  // 발행 시간
      "exp": 1616036000   // 만료 시간
    }

    ⦁ Signature : 헤더와 페이로드를 인코딩한 후, 서버의 비밀키를 이용해 해싱한 값. 이 부분이 토큰의 무결성을 보장.

  3. JWT 주요 메서드 정리
    jwt.sign(payload, secret, options) : JWT 토큰 생성
    jwt.verify(token, secret) : 토큰 유효한지 확인하는 검증
    jwt.decode(token) : 디코딩(인코딩된 데이터를 원래형태로 변환.서명검증없이 단순히 페이로드 데이터 읽어옴.)

  4. JWT_SECRET 설정
    ⦁ JWT를 생성하고 검증할 때 사용하는 비밀 키
    ⦁ 환경 변수로 설정
    JWT_SECRET=SecretKey123!

  5. 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 쿠키 기반 인증 도입하기! 보안도 신경쓰며 보다 안전한 인증 시스템 구축하기👊

0개의 댓글