베이직 2441231 - Access Token과 Refresh Token
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을 활용해서 예시 서비스를 만들고 테스트
로직
- 유저 및 Refresh Token, 게시글 데이터는 메모리에 저장.
- 홍길동, 이순신을 미리 만들고 게시글도 미리 만들어 둡니다.
- users, posts, refreshTokens 변수를 사용. - 로그인
- Access Token(1시간), Refresh Token(7일)을 생성.
- Refresh Token은 서버 메모리에 저장.
- Access Token과 Refresh Token을 클라이언로 JSON 형태로 응답. - 게시글 조회(Public + Private)
- 클라는 요청 헤더에 Authorization: Bearer <Access Token> 전달.
- 서버는 요청 헤더의 Authorization: Bearer <Access Token>에서 Access Token을 추출.
- 토큰이 만료 되었다면 → 401에러 → 클라이언트는 가지고 있는 Refresh Token을 서버로 전달 → 서버에서 Access Tokne생성 및 클라로 전달.
- 토큰이 없다면 → 401에러. - 토큰 갱신(Refresh)
- 클라이언트가 /refresh로 Refresh Token을 전송.(Authorization Header)
- 서버는 Refresh Token이 서버 메모리에 있는지 확인, 유효기간 확인.(verify)
- 유효하면 새로운 Access Token(1시간)을 생성 후 반환. - 로그아웃
- 클라이언트가 /logout을 요청, 서버는 메모리에 저장된 해당 유저의 Refresh Token을 제거 후 성공 응답.
- 성공 응답을 받은 클라는 본인이 저장한 Access, Refresh Token을 삭제. - 게시글 조회(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 버튼 클릭
🔹
🔹
'내일배움캠프_게임서버(202410) > 분반 수업 Basic-A' 카테고리의 다른 글
9주차 Jest - 테스트(작성중) (0) | 2025.01.14 |
---|---|
(수정중)코드 분리 - Layered Architecture Pattern (0) | 2025.01.07 |
숙제하기 - 6주차 인증 (Session / Cookie / JWT) (0) | 2024.12.24 |
교육과정 틀기 (0) | 2024.12.19 |
베이직 강의 24.12.12 - 5주차 Database와 ORM(Prisma) (1) | 2024.12.12 |