본문 바로가기
내일배움캠프_게임서버(202410)/분반 수업 Basic-A

(진행)7주차 - Acces Token / Refresh Token, API, Insomnia

by GREEN나무 2025. 1. 1.
728x90

베이직 2441231 - Access Token과 Refresh Token

https://teamsparta.notion.site/7-Acces-Token-Refresh-Token-API-Insomnia-2434fa1feafe4c7e96e61395188428db

 

Access Token

jwt를 만들 때 생김

Access Token은 보호된 리소스에 접근할 때 사용하는 사용자의 정보를 담은 토큰

이 토큰으로 접근 권한을 확인함

 

사용 예시 : 프라이빗 게시물 조회 

서버 코드 예시

더보기
import express from 'express';
import jwt from 'jsonwebtoken';
const app = express();

const SECRET_KEY = 'mySecretKey'; // JWT 생성에 사용할 비밀 키

// 1. 로그인: JWT 생성
app.get('/login', (req, res) => {
  const user = { id: 123, name: '곰돌이' }; // 사용자 정보
  const token = jwt.sign( // 이게 AcessToken 입니다.
	  user,
	  SECRET_KEY,
	  { expiresIn: '1h' } // 1시간 유효
  );
  res.json({ token }); // JWT를 브라우저에 전달
});

// 2. 인증된 사용자만 접근
app.get('/dashboard', (req, res) => {
  const token = req.headers['authorization']; // 요청 헤더에서 JWT 가져옴
  if (!token) return res.status(401).send('로그인이 필요합니다!');

  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).send('유효하지 않은 토큰입니다.');
    res.send(`환영합니다, ${decoded.name}님!`);
  });
});

app.listen(3000, () => console.log('서버 실행 중!'));

Refresh Token

Refresh Token은 만료기간이 짧은 Access Token을 다시 생성해 주는 친구

Refresh Token(만료기간 길다) Access Token(만료기간 짧음) 이 만료되면 자동으로 Access Token을 갱신해 줍니다

서비스 보안의 민감도에 따라서 두 토큰의 만료 기간이 조금씩 차이 있습니다.

 

더보기

1. 일반적인 로그인

// 1. 로그인: JWT 생성
app.get('/login', (req, res) => {
  const user = { id: 123, name: '곰돌이' }; // 사용자 정보
  const token = jwt.sign( // 이게 AcessToken 입니다.
	  user,
	  SECRET_KEY,
	  { expiresIn: '1h' } // 1시간 유효
  );
  res.json({ token }); // JWT를 브라우저에 전달
});

 

 

 

 

Access Token / Refresh Token을 활용해서 예시 서비스를 만들고 테스트

 

로직

더보기
  1. 유저 및 Refresh Token, 게시글 데이터는 메모리에 저장.
    - 홍길동, 이순신을 미리 만들고 게시글도 미리 만들어 둡니다. 
    - users, posts, refreshTokens 변수를 사용.

  2. 로그인
    - Access Token(1시간), Refresh Token(7일)을 생성.
    - Refresh Token은 서버 메모리에 저장.
    - Access Token과 Refresh Token을 클라이언로 JSON 형태로 응답.

  3. 게시글 조회(Public + Private)
    - 클라는 요청 헤더에 Authorization: Bearer <Access Token> 전달.
    - 서버는 요청 헤더의 Authorization: Bearer <Access Token>에서 Access Token을 추출.
    - 토큰이 만료 되었다면 → 401에러 → 클라이언트는 가지고 있는 Refresh Token을 서버로 전달 → 서버에서 Access Tokne생성 및 클라로 전달.
    - 토큰이 없다면 → 401에러.

  4. 토큰 갱신(Refresh)
    - 클라이언트가 /refresh로 Refresh Token을 전송.(Authorization Header)
    - 서버는 Refresh Token이 서버 메모리에 있는지 확인, 유효기간 확인.(verify)
    - 유효하면 새로운 Access Token(1시간)을 생성 후 반환.

  5. 로그아웃
    - 클라이언트가 /logout을 요청, 서버는 메모리에 저장된 해당 유저의 Refresh Token을 제거 후 성공 응답.
    - 성공 응답을 받은 클라는 본인이 저장한 Access, Refresh Token을 삭제.

  6. 게시글 조회(Public)
    - 로그아웃 상태에서 /posts 요청을 보냅니다
    - 로그 아웃 상태이기 때문에 Public으로 된 게시글만 보입니다.

서버 코드

더보기
import express from "express";
import jwt from "jsonwebtoken";

const app = express();
app.use(express.json());

// ===== 1. 유저와 게시글을 미리 준비합시다. =====
const users = [
    { id: 1, name: "홍길동", username: "hong" },
    { id: 2, name: "이순신", username: "lee" },
];

const posts = [
    {
        id: 101,
        title: "안녕하세요(길동) ✅",
        content: "공개 글",
        isPrivate: false, // 공개 비공개 글 구분 false면 공개, true면 비밀글
        userId: 1, // 글 작성자 판단
    },
    {
        id: 102,
        title: "비밀 게시글(길동) ❌",
        content: "나는 활빈당 대장이다.",
        isPrivate: true,
        userId: 1,
    },
    {
        id: 103,
        title: "또 다른 공개글(순신) ✅",
        content: "누구나 볼 수 있음",
        isPrivate: false,
        userId: 2,
    },
    {
        id: 104,
        title: "원균아...(순신) ❌",
        content: "원균야 도대체 뭐하고 있어?",
        isPrivate: true,
        userId: 2,
    },
];

const refreshTokens = {}; // refreshToken을 저장해서 accessToken이 만료 되었을때 refreshToken을 확인하고 새로운 accessToken생성
/**객체 형태로 유저id와 토큰을 묶어서 저장. */
/* '/refresh'로 토큰을 재발행해달라고 요청하면 클라이언트가 실제 유저가 맞는지 확인하는 저장소 */
// ===== 2. JWT 비밀키와 만료 기간을 상수로 할당 =====
const ACCESS_SECRET = "ACCESS_SECRET_KEY"; // ACCESS토큰의 SECRET키
const REFRESH_SECRET = "REFRESH_SECRET_KEY"; // REFRESH토큰의 SECRET키
const ACCESS_EXPIRE = "1h"; // ACCESS토큰의 만료날짜
const REFRESH_EXPIRE = "7d"; // REFRESH토큰의 만료날짜

// const ACCESS_EXPIRE = '5s'; // 테스트 용도
// const REFRESH_EXPIRE = '10s'; // 테스트 용도

// ===== 3. Access Token, Refresh Token을 생성 하는 함수 =====
function createAccessToken(user) {
    // jwt.sign({유저 정보}, 토큰의 SECRET키, {토큰의 만료날짜})
    // sign : 토큰을 만듦
    return jwt.sign({ userId: user.id, name: user.name }, ACCESS_SECRET, {
        expiresIn: ACCESS_EXPIRE,
    });
}

function createRefreshToken(user) {
    return jwt.sign({ userId: user.id, name: user.name }, REFRESH_SECRET, {
        expiresIn: REFRESH_EXPIRE,
    });
}

// ===== 4. 로그인 시 Access, Refresh 토큰을 발급합니다. =====
// "/login"이라는 url을 주면 아래의 라우터를 실행함
app.post("/login", (req, res) => {
    // 클라이언트가 body로 유저의 이름을 주면 req.body로 뽑아내서 변수에 할당해줌
    const { username } = req.body; // 여기선 간단히 이름만 받아서 로그인 시켜주겠습니다.
    // 유저s 테이블에서 유저이름으로 유저를 찾음
    const user = users.find((u) => u.username === username);
    if (!user) {
        // 일치하는 유저가 없을 때 오류처리
        return res.status(401).json({ message: "해당 유저가 없습니다!" }); // 로그인시 실패해서 401 주겠습니다.
    }

    // 로그인 성공 했으니 토큰 2개 만들어 주겠습니다.
    const accessToken = createAccessToken(user);
    const refreshToken = createRefreshToken(user);

    refreshTokens[user.id] = refreshToken; // 44번줄에서 선언한 배열 refreshTokens에 유저와 매핑해서 토큰 저장.

    return res.json({
        accessToken,
        refreshToken,
        userId: user.id,
        name: user.name,
        message: "login success",
    });
});

// ===== 5. 인증이 필요한 dashboard API =====
app.get("/dashboard", (req, res) => {
    const authHeader = req.headers["authorization"]; // 요청 헤더에 authorization에 토큰이 있는지 확인.
    if (!authHeader) return res.status(401).json({ message: "토큰이 없습니다." });

    const token = authHeader.split(" ")[1]; // Bearer 확인로직은 생략 하겠습니다. 여러분은 해주세요.
    jwt.verify(token, ACCESS_SECRET, (err, decoded) => {
        if (err) {
            return res.status(401).json({ message: "Access Token 만료 또는 유효하지 않습니다." });
        }
        res.json({ message: `환영합니다, ${decoded.name}님!`, user: decoded });
    });
});

// ===== 6. 토큰 갱신 (Refresh Token 사용) =====
app.post("/refresh", (req, res) => {
    // 클라이언트에게 받은 refreshToken
    const { refreshToken } = req.body; // refreshToken은 cookie로 받거나 body로 받는게 일반적입니다.
    if (!refreshToken) {
        // refreshToken 없을 때 오류처리리
        return res.status(401).json({ message: "Refresh Token이 없습니다." });
    }

    let foundUserId = null; // 유저id를 담을 변수를 준비!
    for (const uid in refreshTokens) {
        // 44번 줄에서 선언한 객체를 값으로 가지는 배열 refreshTokens
        // 객체를 돌면서 클라가준 refreshToken이 현재 저장된 토큰과 일치 하는지 분석
        if (refreshTokens[uid] === refreshToken) {
            foundUserId = uid;
            break;
        }
    }
    if (!foundUserId) {
        // refreshToken가 서버에 등록되었는지 확인. 없을 때 오류처리
        // null이면 Refresh Token이 없다는 뜻!
        return res.status(403).json({ message: "서버에 등록되지 않은 Refresh Token입니다." });
    }

    // foundUserId가 있다면 서버에도 클라가 준 Refresh Token이 있다는 뜻이죠
    // verify : 유효성 검증
    // REFRESH_SECRET 일치여부 확인
    jwt.verify(refreshToken, REFRESH_SECRET, (err, decoded) => {
        // refreshToken을 검증합니다.
        if (err) {
            return res
                .status(403)
                .json({ message: "Refresh Token이 만료되었거나 잘못되었습니다." });
        }

        const user = users.find((u) => u.id === +foundUserId); // 유저 테이블에서 토큰으로 찾은 유저가 있는지 Check!
        if (!user) {
            return res.status(404).json({ message: "유저를 찾을 수 없습니다." });
        }

        const newAccessToken = createAccessToken(user); // 모든 검증이 완료 되었으니 새로운 AccessToken 만들어서 주겠습니다.
        return res.json({
            accessToken: newAccessToken,
            userId: user.id,
            name: user.name,
            message: "Access Token 갱신 완료",
        });
    });
});

// ===== 7. 로그아웃 (Refresh Token 무효화) =====
app.post("/logout", (req, res) => {
    const { userId } = req.body; // 원래 userId는 AuthMiddleware에서 받으면 됩니다.
    if (!userId) return res.status(400).json({ message: "userId가 필요합니다." });

    // 서버에 refreshTokens.유저Id에 값이 없으면 성공 메시지(204) 반환
    if (!refreshTokens[userId]) {
        return res.status(204).end(); // 204 -> 요청 이미 성공했고 먼가 더 줄것이 없다는 뜻입니다. 그래서 json으로 먼가 돌려주지 않습니다.
        // return res.status(409).json({ message: '이미 로그아웃된 상태입니다.' }); // 다시 로그아웃을 하는것을 충돌로 보고 싶으면 409 conflict을 사용할 수 있습니다.
    }

    // 서버의 refreshTokens.유저Id에 값이 있으면
    // 서버에서 해당 유저의 refreshToken 삭제
    delete refreshTokens[userId]; // refreshTokens객체에서 userId를 삭제 -> refreshToken을 삭제

    // 로그아웃 성공 메시지 반환
    // client는 200성공을 받으면 클라이언트에 저장된 AT, RT를 지워 줘야 합니다.
    return res.json({ message: "logout success" });
});

// ===== 8. 게시글 조회 로직 (쿼리 파라미터 사용) =====
// GET /posts?filter=private
app.get("/posts", (req, res) => {
    // 권한(accessToken)을 해더로 받아 authHeader에 할당당
    const authHeader = req.headers["authorization"]; // Bearer "234234234.3423fs3242.23f234324"
    // 쿼리는 ? 뒷부분
    const filter = req.query.filter; // "private", "public", undefined
    console.log({ filter });
    let userId = null;

    // 우선 사용자가 보낸 Access Token을 파싱
    if (authHeader) {
        // Bearer "234234234.3423fs3242.23f234324"
        const token = authHeader.split(" ")[1]; // 공백으로 분해(.split(" "))해서 AT만([1])을 받습니다. [ "Bearer", "234234234.3423fs3242.23f234324"]
        try {
            // 토큰 유효성 검사
            // jwt.verify(token, ACCESS_SECRET);
            const decoded = jwt.verify(token, ACCESS_SECRET); //  { userId: 1, name: "홍길동", username: "hong" },
            userId = decoded.userId; //1 로그인된 사용자 ID
        } catch (error) {
            // 토큰 만료 또는 유효하지 않음 -> 로그인 안 된 상태 -> 로그인 안 됬다면 공개글만 보도록 설정
            const publicPosts = posts.filter((post) => post.isPrivate === false);
            return res.json(publicPosts);
        }
    }

    // 여기 까지 왔다면 일단 로그인 성공 입니다~
    // 이제 filter 파라미터에 따라 분기 처리 해보죠~

    // GET /posts?filter=private
    if (filter === "private") {
        // userId와 일치하는 비공개 게시글만 보기
        // 로그인 안 한 경우 -> 401
        if (!userId) {
            return res.status(401).json({ message: "비공개 게시글은 로그인해야 볼 수 있습니다." });
        }
        // 로그인 했다면 isPrivate === true인 글을 전달.
        const privatePosts = posts.filter(
            (post) => post.isPrivate === true && post.userId === userId
        );
        return res.json(privatePosts);
    } else if (filter === "public") {
        // GET /posts?filter=public
        // 공개 게시글만 보기
        const publicPosts = posts.filter((post) => post.isPrivate === false);
        return res.json(publicPosts);
    } else {
        // GET /posts
        // 로그인 하였고 filter 파라미터가 없으면 공개 + 자신의 비공개 게시글 모두 볼 수 있도록 설정.
        const visiblePosts = posts.filter((post) => {
            if (post.isPrivate === false) {
                return true; // 공개글은 모두 보여줌
            }
            // 비공개인 경우 -> 내가 쓴 글만 보이도록 설정
            return post.userId === userId;
        });
        return res.json(visiblePosts);
    }
});

// 서버 실행~
app.listen(3000, () => console.log("http://localhost:3000"));

// 서버 실행 방법
/**
 * # 패키지 설치치
 * npm i express jsonwebtoken # npm install,  npm install express, npm install jsonwebtoken
 *
 * package.json에 추가
 *  "type": "module",
 *
 * # 실행
 * node server.js
 */

 

라우터

 

Insomnia로 테스트

브라우저로 테스트하지 않는 이유
🔹프론트를 만들어서 테스트 할 시간적 여유가 없슴니다
🔹프론트는 URL에서 GET요청밖에 못합니다.
       

브라우저로 테스트하는 방법

🔹 프론트 코드를 만들고

🔹 js파일에서 POST, PUT, PATCH, DELETE 를 사용하는 API는 fetch함수나 jQuery Ajax등을 사용해서 만들어야 합니다. 

클라이언트 테스트 도구(Insomnia, postman 등)로 테스트 후에 프론트엔드 개발자가 열심히 만든 프론트랑 합칩니다.

 

Insomnia 환경설정

{
	"accessToken": "",
	"refreshToken": ""
}


🔹로그인

 

🔹게시글 조회

  1. 토큰 적용하기

     Auth > Bearer Token

로그인 response에서 accessToken을 넣으세요

 

공개 게시물 + 내 비공개 게시물 조회

 

쿼리 추가하기

 

내 비공개 게시물만 조회

공개 게시물만 조회

 

 

 

 

 

 

🔹로그 아웃 ( 1h2m)

 access token 넣고

 

🔹토큰 갱신

로그인할 때 받은 refreshToken 가져오기

 

Insomnia 토큰 자동 갱신

const response = insomnia.response.json();
// 인섬니아객체로 응답 받은 데이터를 json 형태로 변환
if (response && response.accessToken && response.refreshToken) {
  // 응답 받은 데이터 있는지 토큰 2개 있는지 체크
  insomnia.environment.set('accessToken', response.accessToken); 
  // 인섬니아 환경변수 accessToken에 우리가 받은 accessToken토큰 넣어주기.
  insomnia.environment.set('refreshToken', response.refreshToken); 
  // 더 이상 설명은 생략하겠습니다.
  console.log('성공 ✅');
} else {
  console.log('실패 ❌');
}

로그인 Scripts > After-responmse에 위 코드를 추가하세요

결과

자동으로 토큰의 값이 저장됩니다.

 

 

🔹 Auth 토큰 칸에 '{{ " 를 치면 자동완성이 뜹니다.

 

 

🔹 body에 자동저장 토큰사용

 

🔹토큰 갱신

 const response = insomnia.response.json();
if (response && response.accessToken) {
  insomnia.environment.set('accessToken', response.accessToken);
  console.log('성공');
} else {
  console.log('AccessToken이 응답에 없습니다.');
}

스크립트 > after 응답에 위의 코드를 넣으세요

body에 refreshToken 추가

 

🔹로그아웃

const statusCode = insomnia.response.code;

if(statusCode === 200){
	insomnia.environment.set('accessToken', '');
	insomnia.environment.set('refreshToken', '');
	console.log("성공");
}else{
	console.log("실패");
}

스크립트 > after 응답에 위의 코드를 넣으세요

Auth에 accessToken 추가

 

게시글 조회 때 사용한 코드(헤더의 토큰 읽어오기)를 로그아웃 코드에 추가해 줍니다.

    // 권한(accessToken)을 해더로 받아 authHeader에 할당
    const authHeader = req.headers["authorization"]; // Bearer "234234234.3423fs3242.23f234324"
    let userId = null;

    // 우선 사용자가 보낸 Access Token을 파싱
    if (authHeader) {
        // Bearer "234234234.3423fs3242.23f234324"
        const token = authHeader.split(" ")[1]; // 공백으로 분해(.split(" "))해서 AT만([1])을 받습니다. [ "Bearer", "234234234.3423fs3242.23f234324"]
        try {
            // 토큰 유효성 검사
            // jwt.verify(token, ACCESS_SECRET);
            const decoded = jwt.verify(token, ACCESS_SECRET); //  { userId: 1, name: "홍길동", username: "hong" },
            userId = decoded.userId; //1 로그인된 사용자 ID
        } catch (error) {
            return res.status(400).json({ error: error, message: "userId가 필요합니다." });
        }
    }

전체코드

더보기
// ===== 7. 로그아웃 (Refresh Token 무효화) =====
app.post("/logout", (req, res) => {
    // const { userId } = req.body; // 원래 userId는 AuthMiddleware에서 받으면 됩니다.
    // if (!userId) return res.status(400).json({ message: "userId가 필요합니다." });

    // 권한(accessToken)을 해더로 받아 authHeader에 할당
    const authHeader = req.headers["authorization"]; // Bearer "234234234.3423fs3242.23f234324"
    let userId = null;

    // 우선 사용자가 보낸 Access Token을 파싱
    if (authHeader) {
        // Bearer "234234234.3423fs3242.23f234324"
        const token = authHeader.split(" ")[1]; // 공백으로 분해(.split(" "))해서 AT만([1])을 받습니다. [ "Bearer", "234234234.3423fs3242.23f234324"]
        try {
            // 토큰 유효성 검사
            // jwt.verify(token, ACCESS_SECRET);
            const decoded = jwt.verify(token, ACCESS_SECRET); //  { userId: 1, name: "홍길동", username: "hong" },
            userId = decoded.userId; //1 로그인된 사용자 ID
        } catch (error) {
            return res.status(400).json({ error: error, message: "userId가 필요합니다." });
        }
    }
    // 서버에 refreshTokens.유저Id에 값이 없으면 성공 메시지(204) 반환
    if (!refreshTokens[userId]) {
        return res.status(204).end(); // 204 -> 요청 이미 성공했고 먼가 더 줄것이 없다는 뜻입니다. 그래서 json으로 먼가 돌려주지 않습니다.
        // return res.status(409).json({ message: '이미 로그아웃된 상태입니다.' }); // 다시 로그아웃을 하는것을 충돌로 보고 싶으면 409 conflict을 사용할 수 있습니다.
    }

    // 서버의 refreshTokens.유저Id에 값이 있으면
    // 서버에서 해당 유저의 refreshToken 삭제
    delete refreshTokens[userId]; // refreshTokens객체에서 userId를 삭제 -> refreshToken을 삭제

    // 로그아웃 성공 메시지 반환
    // client는 200성공을 받으면 클라이언트에 저장된 AT, RT를 지워 줘야 합니다.
    return res.json({ message: "logout success" });
});

 

 

성공

 

 

 

🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹 🔹
🔹

 

REST API의 목적은 이해하기 쉽고 사용하기 쉬운 API를 제작하는데에 있다.

상태, 전송을 표현함

웹상에서 리소스들을 url을 통해 조작하겠다

 

 

 

 

 


과제

 

학습

🔷🔹🟦💠◇◆◈♢  ❄️🛋🐬💧🐟💎🐳💦👕🐋🧵🧢 🥶🧊🥏

🔵🛄📘🛃👖🛅🛂🌊🈂️🈳💙💤🧿🌀🚹🚾♿

 🚫❌ ❓❔?

🔗🔨⚒️⛏🔧🔩🗜🛠🧰🧲⚙️⛓🪓🦯🪚🪛🪝🪜

📕📖📚 📘📒🔖

❄️❇❅❄❆❉❊⛄

⛥⛧✫✡✬ 🔮✪

문자티콘 : https://xn--yq5bk9r.com/blog/emoji-color-sky-blue

 


 


 



 


숙제

7주차 과제 1,2 URL : https://www.notion.so/16dfbd856e4980f28b33fced3bfa7f5a?pvs=4

 


※ 요약

 

※ 기억할 것

 

※Tip

🔹vscode에서 js 코드 접기

더보기

전체 접기

  • Ctrl + K + 0(숫자)  : Windows and Linux
  • ⌘ + K + 0 (숫자) : Mac

전체 펼치기

  • Ctrl + K + J : Windows and Linux
  • ⌘ + K + J : Mac

 

현재영역 접기

  • Ctrl + Shift + [  : Windows and Linux
  • ⌥ + ⌘ + [  : Mac

현재영역 펼치기

  • Ctrl + Shift + ]  : Windows and Linux
  • ⌥ + ⌘ + ]  : Mac

🔹 변수명은 직관적으로

🔹 auth는 어스라고 부름
🔹insomnia body 보기 좋게 수정

     Beautihy JSON 버튼 클릭


🔹
🔹