stateless
하게 만드는 이론이 담긴 방식임그렇다면, 왜 토큰을 관리하는 서버를 따로 두어야 할까?
- 이는 여러 개의 서버를 운영할 때, 토큰 기반 인증과 특정 도메인을 처리하는 서버를 함께 두는 경우 해당 도메인에서 에러가 발생하면 인증 기능이 마비되어 다른 도메인(예: B, C, D 등) 기능이 연쇄적으로 마비될 수 있기 때문임
- 이러한 설계는
MSA 환경에 매우 적합함
, MSA에서는 각 서비스가 독립적으로 운영되기 때문에 한 서비스의 장애가 다른 서비스에 영향을 미치지 않도록 토큰 관리 서버를 별도로 두는 것이 중요함
1. 로그인(Authentication)
- 사용자 인증 : 사용자가 사용자명과 비밀번호를 서버로 전송하여 로그인 요청을 함
- 서버 검증 : 서버는 자격 증명을 확인하고 유효한 경우 액세스 토큰과 리프레시 토큰을 발급
2. 토큰 발급(Token Issuance)
- 액세스 토큰 : 서버는 짧은 유효기간을 가진 액세스 토큰을 생성하여 클라이언트에게 반환
- 리프레시 토큰 : 서버는 긴 유효기간을 가진 리프레시 토큰을 생성하여 클라이언트에게 반환
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR...",
"refreshToken": "dXNlcm5hbWU6cGFzc3dvcmQ..."
}
3. 인증된 요청(Authenticated Requests)
- 액세스 토큰 사용 : 클라이언트는 보호된 리소스에 접근할 때 HTTP 요청 헤더에 액세스 토큰을 포함하여 서버로 전송
GET /protected-resource
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
4. 토큰 검증(Token Validation)
- 서버 검증 : 서버는 액세스 토큰을 검증하여 유효성을 확인
- 토큰이 유효하면 요청을 처리하고 보호된 리소스에 접근
5. 토큰 갱신(Token Renewal)
- 액세스 토큰 만료 : 액세스 토큰이 만료되면 클라이언트는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 요청
POST /refresh-token
Authorization: Bearer dXNlcm5hbWU6cGFzc3dvcmQ...
- 서버 검증 및 발급 : 서버는 리프레시 토큰을 검증하고 유효하면 새로운 액세스 토큰을 발급
- 리프레시 토큰을 함께 발급할 수도 있음
6. 로그아웃(Logout)
- 토큰 폐기 : 사용자가 로그아웃하면 클라이언트는 저장된 토큰을 삭제
- 서버 측에서는 리프레시 토큰을 무효화하여 보안을 강화할 수 있음
7. 보안 관리(Security Management)
- 토큰 유효성 관리 : 서버는 정기적으로 리프레시 토큰의 유효성을 검토하고
필요시 토큰을 무효화하거나 재발급함
- 위험 탐지 : 비정상적인 사용 패턴이나 보안 위협을 탐지하면 서버는
해당 토큰을 즉시 무효화하여 추가적인 보안을 강화
JSON 객체를 사용하여 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)
주로 사용자 인증 및 권한 부여를 위해 사용
JWT는 세 부분으로 구성되며 각 부분은 점(.
)으로 구분
헤더(Header), 페이로드(Payload), 서명(Signature)
1. 헤더(Header)
- 두 부분으로 구성
- 토큰 타입(JWT)
- 해싱 알고리즘(예 : HMAC SHA256 또는 RSA)
{
"alg": "HS256",
"typ": "JWT"
}
2. 페이로드(Payload)
- 클레임(claims)이라고 하는 여러 가지 정보가 담김
- 등록된 클레임(Registered Claims) : 토큰의 표준화된 정보
(예 : iss, exp, sub, aud 등)
- 공개 클레임(Public Claims) : 충돌 방지를 위해 IANA JSON Web Token Registry에 등록하거나 URI(Uniform Resource Identifier) 형식으로 정의된 클레임
- 비공개 클레임(Private Claims) : 양 당사자 간에 정의된 클레임
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3. 서명(Sinature)
- 헤더와 페이로드를 인코딩한 후 비밀 키 또는 RSA 개인 키로 서명
- 예시(HMAC SHA256)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
JWT 공식 사이트
장점 | 단점 |
---|---|
자기 포함: 필요한 모든 정보를 포함 | 토큰 크기: 많은 정보를 포함하면 크기가 커짐 |
안전성: 서명되어 있어 변경 불가 | 보안 취약점: 탈취 시 만료 기간 동안 악용 가능 |
확장성: 다양한 환경에서 사용 가능 | 무효화 어려움: 이미 발급된 토큰을 무효화하기 어려움 |
상태 비저장: 서버에 상태 저장 필요 없음 | |
JSON 기반: 쉽게 직렬화 및 역직렬화 가능 |
토큰 기반 인증 방식을 구현할 때는 refresh 토큰과 access 토큰 두개를 기반으로 구현함
- access 토큰의 수명은 짧게, refresh 토큰의 수명은 길게함
- refresh 토큰은 access 토큰이 만료되었을 때 다시 access 토큰을 얻기 위해 사용되는 토큰, 이를통해 access토큰이 만료됐을 때마다 인증에 관한 비용이 줄어들게 됨
- 로그인을 하게 되면 access 토큰과 refresh 토큰 두개를 얻음
- access 토큰이 만료되거나, 사용자가 새로고침을 할 때 refresh 토큰을 기반으로 새로운 access token을 얻음
npm install express cookie-parser jsonwebtoken cors
SHA256 해싱 알고리즘
// 토큰에 관한 내용은 따로 .env에 저장
const ACCESS_TOKEN_SECRET =
"a94fdc14161c28e4fed5d168ab4c0555dec116d22bd318655a77b4e046239afd";
const REFRESH_TOKEN_SECRET =
"a94fdc14161c28e4fed5d168ab4c0555dec116d22bd318655a77b4e046239afd";
const PORT = 12010;
//라우팅설정을 쉽게, 미들웨어 설정을 쉽게 도와주는 express 프레임워크 const express = require('express');
// 쿠키 핸들링을 쉽게 해주는 모듈
const cookieparser = require("cookie-parser");
//jwt에 관한 로직을 설정할 수 있게 하는 모듈.
const jwt = require("jsonwebtoken");
// browser - postman에서 요청을 하게 되면 CORS가 걸리는데 이를 가능하게 해주는 모듈.
// CORS의 자세한 설명은 큰돌의 터전 - CORS를 참고.
const cors = require("cors");
const app = express();
// 미들웨어를 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieparser());
// DB에 있는 유저 정보를 흉내내는 객체
const userInfo = {
username: "leesfact",
password: "1234",
email: "leesfact@gmail.com",
};
//토큰을 만들 때 쓰는 유저 객체
const user = {
username: userInfo.username,
email: userInfo.email,
};
// 두개의 토큰에 대한 만료기한 옵션 : access토큰은 짧게. refresh토큰은 길게
const accessOpt = {
expiresIn: "10m",
};
const refreshOpt = { expiresIn: "1d" };
// 쿠키 옵션
const cookieOpt = {
httpOnly: true,
sameSite: "Strict",
secure: true,
maxAge: 24 * 60 * 60 * 1000,
};
const isAuthenticated = (req, res, next) => {
if (!req.headers.authorization) {
return next("route");
}
let auth = req.headers.authorization;
if (auth.startsWith("Bearer ")) {
auth = auth.substring(7, auth.length);
}
const user = jwt.verify(auth, ACCESS_TOKEN_SECRET);
//인증된 유저가 있다면 다음 미들웨어로, 그게 아니라면 다음 라우팅으로, 제어권을 넘긴다.
if (user) return next();
else return next("route");
};
app.get("/", isAuthenticated, function (req, res) {
return res.status(200).send("허용된 요청입니다.");
});
app.get("/", (req, res) => {
return res.status(401).send("허용되지 않은 요청입니다.");
});
app.post("/login", (req, res) => {
const { username, password } = req.body;
console.log(req.body);
console.log(username, password);
if (username === userInfo.username && password === userInfo.password) {
const accessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, accessOpt);
const refreshToken = jwt.sign(user, REFRESH_TOKEN_SECRET, refreshOpt);
// cookie에는 refresh토큰을 담습니다.
// 이후 요청시에 refresh토큰을 기반으로 accesstoken을 갱신받습니다.
console.log("jwt토큰이 생성되었습니다.");
console.log(refreshToken);
console.log(accessToken);
res.cookie("jwt", refreshToken, cookieOpt);
return res.json({ accessToken, refreshToken });
} else {
// 만약 인증을 하지 않은 경우
return res.status(401).json({ message: "인증되지 않은요청입니다." });
}
});
// access토큰 요청 전에 먼저 refresh 토큰 요청을 먼저 함.
app.post("/refresh", (req, res) => {
console.log("REFRESH요청");
console.log(req.cookies);
if (req.cookies.jwt) {
const refreshToken = req.cookies.jwt;
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({ message: "인증되지 않은 요청입니다." });
} else {
console.log(decoded);
const accessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, accessOpt);
return res.json({ accessToken });
}
});
} else {
return res.status(401).json({ message: "인증되지 않은 요청입니다." });
}
});
app.listen(PORT, () => {
console.log(`서버시작 : http://localhost:${PORT}`);
console.log(`로그인요청 : http://localhost:${PORT}/login`);
console.log(`refresh요청 : http://localhost:${PORT}/refresh`);
});
/*
서버시작 : http://localhost:12010
로그인요청 : http://localhost:12010/login refresh요청 : http://localhost:12010/refresh {
"username": "leesfact",
"password": "1234",
"email": "leesfact@gmail.com"
}
curl -X POST http://localhost:12010/refresh -H 'Content-Type:
application/json' --cookie
"jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imt1bmRv
bCIsImVtYWlsIjoia3VuZG9sQGdtYWlsLmNvbSIsImlhdCI6MTY3MjI3MzgxNSwiZX
hwIjoxNjcyMzYwMjE1fQ.rAhD3tF11APHaSoxUp1F5DtNUte-5mt8HjawHoVVbns"
*/
구현 시 주의점
HTTP Header - Authorization
또는 HTTP Header - Cookie
에 담아 요청을 하게 됨, 이ㄷ 떄 당므과 같은 규칙을 지키는 것이 좋음✔︎ Bearer
<token>
으로 Bearer을 앞에 둬서 토큰 기반 인증 방식이라는 것을 알려야 함
✔︎ HTTPS를 사용해야함 (예제는 HTTP로 진행)
✔︎ 쿠키에 저장한다면sameSite:'Strict'
으로
✔︎ 수명이 짧은 AccessToken을 발급해야함
✔︎ url에 토큰을 전달하면 안됨
포스트맨 - API 테스트 플랫폼
로그인이 안된 상태, 요청 보낼 경우
POST 요청, http://localhost:12010/login
accessToken 복사 후, authorization key의 value에 붙여넣기
refresh 토큰으로 access 토큰 발급 받기
curl -X POST http://localhost:12010/refresh -H 'Content-Type: application/json' --cookie "jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxlZXNmYWN0IiwiZW1haWwiOiJsZWVzZmFjdEBnbWFpbC5jb20iLCJpYXQiOjE3MTkyMjYwNjgsImV4cCI6MTcxOTMxMjQ2OH0.sju7nbdc0csswiN_1NZ34j50olwpQorthXs7sKbwSDg"
post 요청(refresh 토큰 요청), http://localhost:12010/refresh
- 독립적 배포 : 각 서비스는 독립적으로 배포, 확장, 업데이트가 가능
- 자율적 팀 : 각 서비스는 자율적인 개발 팀에 의해 관리되며 팀들은 서로 독립적으로 작업할 수 있음
- 작은 코드베이스 : 각 서비스는 상대적으로 작은 코드베이스를 가지며 유지보수가 용이함
- 다양한 기술 스택 : 각 서비스는 필요한 기술 스택을 독립적으로 선택하여 사용할 수 있음
- 폴리글랏(Polyglot) 프로그래밍 : 서로 다른 프로그래밍 언어와 데이터베이스를 사용할 수 있음
장점 | 단점 |
---|---|
확장성: 각 서비스가 독립적으로 확장 가능 | 복잡성 증가: 서비스 간 통신과 데이터 일관성 유지 필요 |
유연성: 새로운 기술 도입 용이 | 관리 오버헤드: 여러 서비스의 배포, 모니터링, 관리 필요 |
신뢰성: 한 서비스의 장애가 다른 서비스에 미치는 영향 적음 | 데이터 관리: 데이터 일관성과 트랜잭션 관리 어려움 |
빠른 배포: 부분적인 변경이 전체 시스템에 영향 없음 | 테스트 복잡성: 통합 테스트와 서비스 간 상호작용 테스트 복잡 |
독립적 개발: 자율적 팀이 독립적으로 작업 가능 | 성능 문제: 서비스 간 통신 오버헤드 발생 가능 |
- 단일 코드베이스 : 애플리케이션의 모든 기능이 하나의 코드베이스에 포함됨
- 통합 배포 : 전체 애플리케이션이 한 번에 빌드되고 배포됨
- 통합 데이터 관리 : 모든 기능이 동일한 데이터베이스를 공유함
장점 | 단점 |
---|---|
확장성: 각 서비스가 독립적으로 확장 가능 | 복잡성 증가: 서비스 간 통신과 데이터 일관성 유지 필요 |
유연성: 새로운 기술 도입 용이 | 관리 오버헤드: 여러 서비스의 배포, 모니터링, 관리 필요 |
신뢰성: 한 서비스의 장애가 다른 서비스에 미치는 영향 적음 | 데이터 관리: 데이터 일관성과 트랜잭션 관리 어려움 |
빠른 배포: 부분적인 변경이 전체 시스템에 영향 없음 | 테스트 복잡성: 통합 테스트와 서비스 간 상호작용 테스트 복잡 |
독립적 개발: 자율적 팀이 독립적으로 작업 가능 | 성능 문제: 서비스 간 통신 오버헤드 발생 가능 |