"Stateless"라는 용어는 HTTP 프로토콜의 핵심 특징 중 하나로, 각 요청과 응답이 독립적이라는 것을 의미한다. HTTP가 Stateless 프로토콜이라는 것은 서버가 클라이언트의 이전 요청이나 상태에 대한 정보를 저장하지 않는다는 것이다. 각 요청은 별개의 트랜잭션으로 처리되며, 요청 간에 상태 정보가 유지되지 않는다.
첫 번째 요청에서 서버에 자신이 누군지 말해줘도, 서버에게 다시 물어봐도 서버는 사용자가 누구인지 모른다.
그 이유는 HTTP는 stateless이기 때문이다.
Status와 Info를 HTTP Request에 포함시키지 않는다.
(성능 문제: 각 요청에 대한 연결을 재설정하는 데 소요되는 시간/대역폭을 최소화하기 위한 것이다.)
독립적인 요청: 서버는 각 요청을 완전히 독립적으로 처리합니다. 즉, 이전 요청의 상태를 기억하지 않는다.
클라이언트의 책임: 클라이언트가 이전 상태를 기억하고 필요한 정보를 매 요청마다 전달해야 한다.
상태 정보의 부재: 서버는 요청을 처리한 후 요청에 대한 상태 정보를 저장하지 않는다.
단순성: 서버와 클라이언트 간의 상호작용이 간단해진다. 각 요청은 독립적으로 처리되므로, 복잡한 상태 관리 로직이 필요 없다.
확장성: 서버가 상태 정보를 저장하지 않으므로, 여러 서버 간의 부하 분산이 용이하다. 각 서버는 독립적으로 요청을 처리할 수 있다.
상태 유지 필요성: 일부 애플리케이션에서는 클라이언트와 서버 간의 상태 유지가 필요할 수 있다. 이런 경우에는 별도의 메커니즘이 필요하다 (예: 세션, 쿠키).
데이터 중복: 클라이언트는 매 요청마다 필요한 상태 정보를 전달해야 하므로, 데이터 전송이 중복될 수 있다.
HTTP가 Stateless 프로토콜이지만, 웹 애플리케이션에서는 상태 정보를 유지해야 하는 경우가 많다. 예를 들어, 로그인한 사용자의 정보, 장바구니 상태 등이 있다. 이러한 상태 정보를 유지하기 위한 기술로는 쿠키, 세션, 토큰 기반 인증 (예: JWT) 등이 있다.
HTTP의 Stateless 특징은 프로토콜의 단순성과 확장성을 증대시킨다. 하지만 일부 상태 유지가 필요한 경우에는 추가적인 기술을 활용해야 한다.
JWT는 JSON 기반의 오픈 표준 (RFC 7519)으로, 두 개체 사이에서 정보를 안전하게 전송하기 위한 간결하고 자가 포함된 방법을 제공한다.
토큰을 생성할 때 사용하는 모듈이다.
주로 다음과같이 사용된다.
JWT는 세 부분으로 구성된다.
세 부분은 각각 Base64Url로 인코딩되고, .으로 구분된다.
로그인 → 토큰 클라이언트에 전달 → 토큰을 이용한 요청
프로젝트 폴더 생성 후, package.json파일 생성
npm init
npm install dotenv express jsonwebtoken nodemon
const express = require('express')
const app = express();
app.use(express.json());
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
})
app.post('/login/', (req, res) => {
const username = req.body.username
const user = { name: username }
const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
res.json({ accessToken: accessToken })
})
이 코드 조각은 클라이언트로부터 /login 경로로 POST 요청을 받아 처리하는 Express 라우트를 정의한다. 요청 본문에 포함된 username을 사용해 JSON Web Token (JWT)를 생성하고, 이를 응답으로 반환한다.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretText = 'superSecret';
app.use(express.json());
app.post('/login/', (req, res) => {
const username = req.body.username
const user = { name: username }
const accessToken = jwt.sign(user, secretText);
res.json({ accessToken: accessToken })
})
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
})
const posts = [
{
username: 'John',
title: 'Post 1'
},
{
username: 'Han',
title: 'Post 2'
}
]
app.get('/posts', (req, res) => {
res.json(posts)
})
이 코드는 Express 웹 서버에서 /posts 경로로 들어오는 GET 요청을 처리하여 posts 배열을 JSON 형식으로 응답한다.
const posts = [
{
username: 'John',
title: 'Post 1'
},
{
username: 'Han',
title: 'Post 2'
}
]
app.get('/posts', (req, res) => {
res.json(posts)
})
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretText = 'superSecret';
const posts = [
{
username: 'John',
title: 'Post 1'
},
{
username: 'Han',
title: 'Post 2'
}
]
app.use(express.json());
app.post('/login/', (req, res) => {
const username = req.body.username
const user = { name: username }
const accessToken = jwt.sign(user, secretText);
res.json({ accessToken: accessToken })
})
app.get('/posts', (req, res) => {
res.json(posts);
})
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
})
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts)
})
function authMiddleware(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token == null) return res.sendStatus(401)
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
console.log(err)
if (err) return res.sendStatus(403)
req.user = user
next()
})
}
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts)
})
function authMiddleware(req, res, next) { ... }
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token == null) return res.sendStatus(401)
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { ... })
if (err) return res.sendStatus(403)
req.user = user
next()
토큰 없이 정보를 전달하면 위와 같이 Unauthorized가 나온다.
토큰을 입력하여 정보를 전달하면 위와 같이 잘 응답한다.
Refresh Token은 웹 애플리케이션에서 주로 사용되는 인증 방법 중 하나로, Access Token과 쌍으로 사용된다. Refresh Token의 주요 목적은 Access Token의 수명이 만료된 후에도 사용자를 다시 로그인 시키지 않고 새로운 Access Token을 발급받는 것이다.
현재 앱에서 accessToken 하나만 있다면 이것을 가지고 계속 인증이 필요한 요청을 보낼 수 있다.
로그인을 한번 더 해서 다른 토큰을 받고 그 이전에 토큰을 이용해도 아직 유효하다.
보안 문제가 발생할 수 있다.
토큰의 유효시간이 너무 짧으면, 자동으로 로그아웃돼서 너무 자주 로그인을 다시 해야한다.
토큰의 유효시간이 너무 길면, 토큰에 유효시간을 주는 이유가 사라지게 된다. 토큰이 탈취당하면 긴 유효시간이 끝날 때 까지 계속 탈취당한 토큰이 사용된다.
이러한 문제점을 보완하기 위해 RefreshToken을 사용한다.
refresh토큰은 accessToken의 유효시간은 짧게 해주며, refreshToken의 유효시간은 길게 해준다. 그래서 accessToken의 유효시간이 다 지나면 refreshToken을 이용해서 새로운 accessToken을 발급해준다.
토큰 | 설명 |
---|---|
accessToken | Access Token은 리소스에 접근하기 위해서 사용되는 토큰이다. |
refreshToken | Refresh Token은 기존에 클라이언트가 가지고 있던 Access Token이 만료되었을 때 Access Token을 새로 발급받기 위해 사용한다. |
Refresh Token은 사용자 경험을 향상시키기 위해 사용되며, 사용자가 반복적으로 로그인해야 하는 불편함을 줄인다. 하지만, 이를 안전하게 관리하고 적절한 보안 조치를 취하는 것이 중요하다.
let refreshTokens = []
app.post('/login', (req, res) => {
const username = req.body.username
const user = { name: username }
const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '30s' });
const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '1d' });
refreshTokens.push(refreshToken)
res.cookie('jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ accessToken: accessToken });
})
이 코드는 사용자가 로그인을 시도할 때 실행되는 Express.js 라우트를 정의한다. 특정 username에 대해 액세스 토큰과 리프레시 토큰을 생성하고, 리프레시 토큰을 쿠키로 설정한 후 클라이언트에 액세스 토큰을 반환한다.
let refreshTokens = []:
app.post('/login', (req, res) => {...}:
/login 경로에 대한 POST 요청을 처리하는 라우트를 생성한다.
const username = req.body.username; const user = { name: username };:
클라이언트로부터 전달된 username을 추출하고, user 객체를 생성한다.
const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '30s' });:
user 객체와 환경 변수에 저장된 액세스 토큰의 비밀 키를 사용하여, 30초의 만료 시간을 가진 액세스 토큰을 생성한다.
const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '1d' });:
user 객체와 환경 변수에 저장된 리프레시 토큰의 비밀 키를 사용하여, 1일의 만료 시간을 가진 리프레시 토큰을 생성한다.
refreshTokens.push(refreshToken);:
생성된 리프레시 토큰을 refreshTokens 배열에 추가한다.
res.cookie('jwt', refreshToken, {...});:
refreshToken을 jwt라는 이름의 쿠키로 설정하고, 클라이언트에 전달한다. 쿠키는 httpOnly 속성을 가지며, 만료 시간은 1일이다.
res.json({ accessToken: accessToken });:
생성된 액세스 토큰을 JSON 형식으로 응답 본문에 포함하여 클라이언트에 반환한다.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretText = 'superSecret';
const refreshSecretText = 'supersuperSecret';
const posts = [
{
username: 'John',
title: 'Post 1'
},
{
username: 'Han',
title: 'Post 2'
}
]
let refreshTokens = [];
app.use(express.json());
app.get('/', (req, res) => {
res.send('hi')
})
app.post('/login/', (req, res) => {
const username = req.body.username
const user = { name: username }
// jwt를 이용해서 토큰 생성하기 payload + secretText
// 유효기간 추가
const accessToken = jwt.sign(user, secretText, { expiresIn: '30s' });
// jwt를 이용해서 refreshToken도 생성
const refreshToken = jwt.sign(user,
refreshSecretText,
{expiresIn: '1d'});
refreshTokens.push(refreshToken);
// refreshToken을 쿠키에 넣어주기
res.cookie('jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
})
res.json({ accessToken: accessToken })
})
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts)
})
function authMiddleware(req, res, next) {
// 토큰을 request headers에서 가져오기
const authHeader = req.headers['authorization']
// Bearer oasjfoafo.soafjogajo.aosgjaogjo
const token = authHeader && authHeader.split(' ')[1]
if (token == null) return res.sendStatus(401)
// 토큰이 있으니 유효한 토큰인지 확인
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
console.log(err)
if (err) return res.sendStatus(403)
req.user = user
next()
})
}
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
})
app.get('/refresh', (req, res) => {
const cookies = req.cookies;
if (!cookies?.jwt) return res.sendStatus(401);
const refreshToken = cookies.jwt;
if (!refreshTokens.includes(refreshToken)) {
return res.sendStatus(403)
}
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
const accessToken = jwt.sign({ name: user.name }), process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '30s' });
res.json({ accessToken });
})
})
이 코드 조각은 /refresh 엔드포인트를 통해 클라이언트로부터 리프레시 토큰을 받아, 새로운 액세스 토큰을 생성하고 반환하는 기능을 구현한다.
app.get('/refresh', (req, res) => { ... }):
const cookies = req.cookies;:
if (!cookies?.jwt) return res.sendStatus(401);:
const refreshToken = cookies.jwt;:
if (!refreshTokens.includes(refreshToken)) return res.sendStatus(403);:
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => { ... }):
if (err) return.res.sendStatus(403);:
const accessToken = jwt.sign({ name: user.name }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '30s' });:
res.json({ accessToken });:
const cookieParser = require('cookie-parser');
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretText = 'superSecret';
const refreshSecretText = 'supersuperSecret';
const posts = [
{
username: 'John',
title: 'Post 1'
},
{
username: 'Han',
title: 'Post 2'
}
]
let refreshTokens = [];
app.use(express.json());
app.use(cookieParser());
app.get('/', (req, res) => {
res.send('hi')
})
app.post('/login/', (req, res) => {
const username = req.body.username
const user = { name: username }
// jwt를 이용해서 토큰 생성하기 payload + secretText
// 유효기간 추가
const accessToken = jwt.sign(user, secretText, { expiresIn: '30s' });
// jwt를 이용해서 refreshToken도 생성
const refreshToken = jwt.sign(user,
refreshSecretText,
{expiresIn: '1d'});
refreshTokens.push(refreshToken);
// refreshToken을 쿠키에 넣어주기
res.cookie('jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
})
res.json({ accessToken: accessToken })
})
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts)
})
function authMiddleware(req, res, next) {
// 토큰을 request headers에서 가져오기
const authHeader = req.headers['authorization']
// Bearer oasjfoafo.soafjogajo.aosgjaogjo
const token = authHeader && authHeader.split(' ')[1]
if (token == null) return res.sendStatus(401)
// 토큰이 있으니 유효한 토큰인지 확인
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
console.log(err)
if (err) return res.sendStatus(403)
req.user = user
next()
})
}
app.get('/refresh', (req, res) => {
// cookies 가져오기 cookie-parser
const cookies = req.cookies;
if(!cookies?.jwt) return res.sendStatus(403);
const refreshToken = cookies.jwt;
// refreshtoken이 데이터베이스 있는 토큰인지 확인
if(!refreshToken.inncludes(refreshToken)) {
return res.sendStatus(403);
}
// token이 유효한 토큰인지 확인
jwt.verify(refreshToken, refreshSecretText, (err, user) => {
if(err) return res.sendStatus(403);
//accessToken을 생성하기
const accessToken = jwt.sign({name: user.name},
secretText,
{ expiresIn: '30s'}
)
res.json({ accessToken })
})
})
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
})