2024. 11. 18. 12:25ㆍ정보처리,전산/NODEJS
Stateless의 정의
- Stateless란 서버가 클라이언트와의 이전 요청 상태를 기억하지 않는 것을 의미한다.
- 각 요청은 완전히 독립적이며, 서버는 요청 간의 관계를 추적하지 않는다.
첫 번째 요청:
- 클라이언트는 서버에게 "나는 user123"이라고 자신의 정보를 보냈다.
- 서버는 요청을 처리하고 응답을 반환한다. 하지만 요청을 처리한 후 user123이라는 정보는 사라진다.
두 번째 요청:
클라이언트가 서버에 다시 묻는다:
- 서버는 이전 요청의 상태(즉, "user123"이라는 정보)를 기억하지 않는다.
- Stateless 서버는 각 요청이 독립적이므로 이전에 받은 정보를 저장하지 않는다.
왜 이런 일이 일어날까
HTTP는 본질적으로 Stateless 프로토콜이다.
- HTTP 요청은 서버와 클라이언트 간의 독립적인 통신을 기반으로 하며, 요청 간의 상태를 공유하지 않는다.
- 서버가 클라이언트의 상태를 기억하려면 추가적인 세션 관리 메커니즘이 필요하다.
Stateful과 비교
Stateful 방식으로 클라이언트와의 상태 정보를 유지하기 위해 세션이나 쿠키와 같은 기술을 사용한다.
5. Stateless의 장점
- 확장성: 요청 간의 상태를 저장하지 않으므로 서버 간 부하를 쉽게 분산할 수 있다.
- 유연성: 서버는 요청을 독립적으로 처리할 수 있어 클라이언트나 서버 간의 결합도가 낮다.
6. Stateless를 보완하는 방법
Stateless의 단점을 해결하기 위해 클라이언트와 서버 간에 상태 정보를 유지하려면 아래 방법들을 사용한다:
- 쿠키(Cookies): 클라이언트 측에 정보를 저장.
- 세션(Session): 서버 측에서 상태를 저장.
- JWT(Json Web Token): 요청 시 클라이언트가 상태 정보를 포함하여 서버에 전달.
Stateless는 서버가 클라이언트의 상태 정보를 기억하지 않기 때문에 발생한다. 이는 HTTP 프로토콜의 기본 동작 방식으로, 필요에 따라 추가적인 상태 관리 기술을 활용해 해결할 수 있다.
- 토큰 기반 인증 및 인가의 기본 흐름
1. 클라이언트의 요청:
클라이언트(브라우저)는 서버에 다음과 같이 요청한다:
- 클라이언트는 자신이 누구인지(사용자 정보 또는 자격 증명) 서버에 알린다.
- 이 요청은 보통 로그인 정보(예: 사용자 ID와 비밀번호)를 포함한다.
2. 서버가 토큰 생성:
서버는 클라이언트의 요청을 검증한 후(예: ID와 비밀번호가 맞는지 확인), 해당 유저 정보를 포함하는 토큰을 생성한다.
- 이 토큰에는 사용자의 식별자(ID)와 권한 정보 등의 데이터를 포함할 수 있다.
- 토큰은 보통 JWT(Json Web Token) 형식으로 암호화되거나 서명되어 발급된다.
3. 서버가 클라이언트에 토큰을 전달:
생성된 토큰은 HTTP 응답 헤더나 본문에 포함되어 클라이언트로 전송된다:
4. 클라이언트가 토큰을 저장:
클라이언트는 받은 토큰을 저장소에 보관한다. 보관 위치는 보안 수준에 따라 다른다:
- 로컬 스토리지(Local Storage): 쉽게 접근 가능하지만, 보안에 취약할 수 있다.
- 세션 스토리지(Session Storage): 브라우저 세션 동안만 유지.
- 쿠키(Cookie): 보안 속성을 추가해 토큰을 저장할 수 있음(예: HttpOnly, Secure 속성).
5. 클라이언트가 요청 시 토큰 포함:
클라이언트는 다음 요청을 보낼 때 토큰을 HTTP 헤더에 포함한다:
- "Bearer"는 인증 유형을 의미하며, 토큰을 전달하기 위한 표준 형식이다.
6. 서버가 토큰을 복호화 및 검증:
서버는 받은 요청에서 토큰을 추출하여 다음을 수행한다:
- 토큰 유효성 검증: 서명 또는 암호화를 확인해 토큰이 변조되지 않았는지 확인.
- 토큰 해독(복호화): 유저 정보를 추출.
- 인가 결정: 토큰에 포함된 권한 정보를 기반으로 클라이언트의 요청을 처리할지 여부를 결정.
흐름 요약:
- 클라이언트가 서버에 로그인 정보로 요청.
- 서버가 클라이언트의 정보를 검증 후 토큰을 생성.
- 생성된 토큰을 클라이언트에 전송.
- 클라이언트가 토큰을 저장.
- 이후 요청 시 클라이언트는 토큰을 헤더에 포함.
- 서버가 토큰을 검증하고 요청 처리.
장점:
- Stateless 인증: 서버는 상태를 기억하지 않고, 토큰만으로 사용자를 식별 가능.
- 확장성: 여러 서버에서 동일한 토큰을 사용할 수 있어 확장성이 뛰어남.
- 보안성: 토큰에는 암호화된 데이터가 포함되어 있어 안전하게 사용자 정보를 전달.
주의사항:
- 토큰이 탈취되면 악용될 수 있으므로 보관과 전송 시 보안 강화 필요(예: HTTPS, HttpOnly 쿠키 사용)
npm install dotenv express jsonwebtoken nodemon
이 명령어는 Node.js 프로젝트에서 dotenv, express, jsonwebtoken, nodemon 패키지를 설치하는 데 사용된다.
1. dotenv
- dotenv는 환경 변수(environment variables)를 .env 파일로 관리하는 데 사용된다. 보통 API 키나 DB 연결 정보 등 중요한 정보를 코드에 하드코딩하지 않고, .env 파일에서 불러오는 방식으로 보안성을 높이는 데 사용된다.
2. express
- express는 가장 많이 사용되는 Node.js 웹 프레임워크이다. 간단한 API 서버부터 복잡한 웹 애플리케이션까지 쉽게 구축할 수 있도록 도와준다.
3. jsonwebtoken
- jsonwebtoken (JWT)는 JSON Web Token을 생성하고 검증하는 라이브러리이다. 보통 인증(authentication)과 인가(authorization) 절차에서 JWT를 사용하여 클라이언트와 서버 간의 안전한 데이터를 주고받을 때 사용된다.
4. nodemon
- nodemon은 Node.js 서버를 자동으로 재시작해주는 도구이다. 개발 중에 서버가 변경될 때마다 수동으로 서버를 재시작할 필요 없이 자동으로 재시작되므로 효율적인 개발이 가능하다.
const { request } = require('express');
const express= require('express');
const app= express();
const secretText= 'superSecret';
app.use(express.json());
const jwt =require('jsonwebtoken');
app.post('/login',(req,res)=>{
const username =req.body.username;
const user ={name :username};
//JWT Token Creation payload+secretText
const accessToken=jwt.sign(user,secretText);
res.json({accessToken:accessToken})
})
const port=4000;
app.listen(port,()=>{
console.log('listening on port '+port);
})
JWT에서 Payload의 의미
JWT (JSON Web Token)에서 Payload는 토큰에 담을 정보를 의미한다. 이 정보는 주로 클라이언트와 서버 간에 전송되는 데이터로, 사용자의 인증 상태, 권한, 기타 중요한 데이터를 포함할 수 있다.
Payload는 JWT의 두 번째 부분으로, 헤더(header)와 서명(signature) 사이에 위치한다.
Payload 구성
Payload는 일반적으로 세 가지 타입의 정보를 포함할 수 있다:
- 등록된 클레임 (Registered Claims):
- JWT에서 미리 정의된 클레임으로, 특정 정보를 전달하는 데 사용된다.
- 예시: iss (issuer), exp (expiration), sub (subject), aud (audience) 등.
- 공개 클레임 (Public Claims):
- 사용자 정의 클레임으로, 필요한 정보를 자유롭게 포함할 수 있다.
- 예시: 사용자 ID, 이메일 주소 등.
- 비공개 클레임 (Private Claims):
- 두 시스템 간에만 의미가 있는 정보이다. 다른 시스템에서 사용할 수 없으며, 특정 목적에 맞게 정의된다.
- 예시: 사용자 권한 정보, 세션 ID 등.
JWT 구조
JWT는 다음과 같은 세 부분으로 구성된다:
- Header: 토큰의 유형(예: JWT)과 사용하는 서명 알고리즘을 포함.
- Payload: 실제 데이터, 사용자의 정보나 권한 정보를 담은 JSON 객체.
- Signature: 서명, 비밀 키를 사용하여 JWT의 무결성과 신뢰성을 확인.
따라서, JWT에서 Payload는 토큰에 포함된 사용자 정보나 메타데이터로, 인증이나 인가 처리를 위한 중요한 데이터를 담고 있으며, 이를 통해 서버는 클라이언트를 인증하고, 클라이언트는 자신이 누구인지 확인할 수 있게 된다.
토큰 인증 미들웨어 구현
위 코드에서는 JWT를 사용하여 유효한 토큰을 가진 사용자만 /posts 경로에 접근할 수 있도록 인증 미들웨어를 설정 한다.
app.get('/posts', authMiddleware, (req,res)=>{
res.json(posts);
})
//middleware
function authMiddleware(req,res,next){
//token을 request header에서 가져오기
const authHeader=req.headers['authorization'];
//토큰만
const token=authHeader && authHeader.split(' ')[1]
if(token == null) return res.sendStatus(401);
//유효한 토큰인지 확인
jwt.verify(token, secretText,(err,user)=>{
if(err) return res.sendStatus(403);
req.user=user;
next();
})
JWT를 이용한 인증 및 권한 부여 흐름
Access Token과 Refresh Token을 사용한 JWT 인증 및 권한 부여 흐름을 기반으로 각 단계에 대해 필요한 작업
1. 사용자가 로그인 시도
- 클라이언트는 로그인 폼을 통해 사용자 아이디와 비밀번호를 입력한다.
- 서버는 해당 정보를 바탕으로 데이터베이스에서 사용자를 조회하고, 비밀번호를 확인한다. 비밀번호가 일치하면 로그인 성공.
2. 로그인 완료 후 Access Token과 Refresh Token 발급
- 사용자가 로그인에 성공하면, 서버는 Access Token과 Refresh Token을 발급하여 클라이언트에 전달한다.
- Access Token: 사용자가 인증된 후 일정 기간 동안 유효하며, API 요청을 할 때마다 헤더에 포함하여 사용된다.
- Refresh Token: Access Token이 만료된 경우, 새로운 Access Token을 발급하기 위해 사용되며, 서버에서 안전하게 저장된다.
3. 클라이언트가 Access Token을 헤더에 담아 서버에 요청
- 클라이언트는 Access Token을 Authorization 헤더에 담아서 요청을 보낸다.
4. 서버에서 Access Token 검증
- 서버는 클라이언트가 보낸 Access Token을 jwt.verify() 메서드를 통해 검증한다.
- 토큰이 유효하면, 요청한 데이터를 반환하고, 유효하지 않으면 401 Unauthorized 오류를 반환한다.
5. Access Token의 유효기간 만료
- Access Token은 설정된 시간(예: 1시간)이 지나면 만료된다.
- 만료된 Access Token으로 요청을 보내면, 서버는 403 Forbidden을 반환한다.
6. Access Token 만료 시 권한 없음 오류
- 서버는 Access Token이 만료된 경우, 401 Unauthorized 또는 403 Forbidden을 반환한다.
7. 사용자가 Refresh Token을 서버로 보내어 Access Token 발급 요청
- 사용자는 Refresh Token을 서버로 보내어 새로운 Access Token을 발급받을 수 있다.
- 클라이언트가 보낸 Refresh Token을 서버에서 처리하여, 새 Access Token을 발급한다.
8. 서버에서 Refresh Token 검증 및 Access Token 발급
- 서버는 클라이언트에서 받은 Refresh Token과 DB에 저장된 Refresh Token을 비교한다.
- Refresh Token이 일치하고 만료되지 않았다면, 새로운 Access Token을 발급하여 클라이언트에 반환한다.
전체 흐름
- 사용자가 로그인하여 서버에서 Access Token과 Refresh Token을 발급받다.
- 클라이언트는 Access Token을 사용해 서버에 요청을 보낸다.
- 서버는 Access Token을 검증하고, 유효하면 요청한 데이터를 반환한다.
- Access Token이 만료되면, 클라이언트는 Refresh Token을 보내어 새로운 Access Token을 발급받다.
- 서버는 Refresh Token을 검증하고, 유효하면 새로운 Access Token을 발급한다.
이 흐름은 JWT 인증과 권한 부여에서 일반적으로 사용되는 패턴으로, 보안과 효율성을 모두 고려한 방식이다.
코드 해석: Express 기반 JWT 인증 서버 구현
const { request } = require('express');
const express = require('express');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser'); // 쿠키 파서 추가
const app = express();
const secretText = 'superSecret';
const refreshSecretText = 'supersuperSecret';
// 데이터 배열(posts)
const posts = [
{ username: 'jj', title: 'Post 1' },
{ username: 'hh', 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 Token Creation payload + secretText
const accessToken = jwt.sign(user, secretText, { expiresIn: '30s' });
const refreshToken = jwt.sign(user, refreshSecretText, { expiresIn: '1d' });
refreshTokens.push(refreshToken); // 배열 이름 수정
// refresh 토큰은 쿠키에 넣어줌
res.cookie('jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 1일
});
res.json({ accessToken: accessToken });
});
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts);
});
// middleware
function authMiddleware(req, res, next) {
// token을 request header에서 가져오기
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401); // 토큰이 없는 경우
// 유효한 토큰인지 확인
jwt.verify(token, secretText, (err, user) => {
if (err) return res.sendStatus(403); // 유효하지 않은 토큰
req.user = user;
next();
});
}
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
});
1. 필요한 모듈 및 기본 설정
- express: Node.js의 웹 애플리케이션 프레임워크로 HTTP 서버를 쉽게 구축할 수 있다.
- jsonwebtoken: JSON Web Token(JWT)을 생성하고 검증하는 데 사용된다.
- cookie-parser: 쿠키를 파싱하여 쉽게 접근할 수 있도록 도와주는 미들웨어이다.
2. 비밀키 및 데이터 설정
- secretText: Access Token을 서명하는 데 사용되는 비밀키.
- refreshSecretText: Refresh Token을 서명하는 데 사용되는 비밀키.
- posts: 사용자별 게시글 데이터를 포함하는 배열.
- refreshTokens: 서버가 발급한 Refresh Token을 저장하는 배열.
3. Express 애플리케이션 및 미들웨어 설정
- express.json(): HTTP 요청의 JSON 데이터를 자동으로 파싱하여 사용할 수 있게 한다.
- cookieParser(): 클라이언트의 쿠키를 파싱하여 사용할 수 있게 한다.
4. 로그인 엔드포인트 (JWT 토큰 발급)
- POST /login:
- 클라이언트로부터 사용자 이름을 받아 JWT Access Token 및 Refresh Token을 생성.
- Access Token: 인증 및 인가에 사용되며 30초 후 만료된다.
- Refresh Token: Access Token 갱신 시 사용되며 1일 동안 유효.
- Refresh Token은 서버의 refreshTokens 배열에 저장되며, 클라이언트의 쿠키에 저장된다.
- 응답으로 Access Token을 반환한다.
5. 게시글 엔드포인트 (인증 필요)
- GET /posts:
- 사용자가 인증된 경우에만 게시글 데이터를 반환.
- 인증은 authMiddleware를 통해 처리된다.
쿠키 파서 모듈 추가
- 설명: cookie-parser 모듈을 가져와서 사용 준비를 한다.
const cookiePareser = require('cookie-parser'); // 쿠키 파서 모듈 추가
app.use(cookiePareser()); // 쿠키 파서 미들웨어 등록
app.get('/refresh', (req, res) => {
// 쿠키 파싱
console.log('req.cookies', req.cookies);
});
Refresh Token과 JWT 인증을 사용하는 Express 서버
const { request } = require('express');
const cookieParser = require('cookie-parser'); // 오타 수정
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretText = 'superSecret';
const refreshSecretText = 'supersuperSecret';
// 데이터 배열(posts)
const posts = [
{ username: 'jj', title: 'Post 1' },
{ username: 'hh', 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 Token Creation payload + secretText
const accessToken = jwt.sign(user, secretText, { expiresIn: '30s' });
const refreshToken = jwt.sign(user, refreshSecretText, { expiresIn: '1d' });
refreshTokens.push(refreshToken);
// refresh 토큰은 쿠키에 넣어줌
res.cookie('jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 1일
});
res.json({ accessToken });
});
app.get('/posts', authMiddleware, (req, res) => {
res.json(posts);
});
// middleware
function authMiddleware(req, res, next) {
// token을 request header에서 가져오기
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401);
// 유효한 토큰인지 확인
jwt.verify(token, secretText, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/refresh', (req, res) => {
// 쿠키 파싱
const cookies = req.cookies;
if (!cookies?.jwt) return res.sendStatus(403);
const receivedToken = cookies.jwt;
// refreshToken 확인
if (!refreshTokens.includes(receivedToken)) {
return res.sendStatus(403);
}
// token 유효성 확인
jwt.verify(receivedToken, refreshSecretText, (err, decodedUser) => {
if (err) return res.sendStatus(403);
// accessToken 생성
const accessToken = jwt.sign(
{ name: decodedUser.name },
secretText,
{ expiresIn: '30s' }
);
res.json({ accessToken });
});
});
const port = 4000;
app.listen(port, () => {
console.log('listening on port ' + port);
});
-
- express: Node.js 웹 프레임워크.
- cookie-parser: 클라이언트가 보낸 쿠키를 읽기 위한 미들웨어.
- jsonwebtoken: JWT 토큰을 생성하고 검증하기 위한 라이브러리.
- JWT 비밀키:
- secretText: 액세스 토큰에 사용되는 비밀키.
- refreshSecretText: 리프레시 토큰에 사용되는 비밀키.
- 입력: 클라이언트는 username을 요청 본문에 포함.
- 액세스 토큰 생성:
- 사용자의 username을 기반으로 30초 만료되는 액세스 토큰을 생성.
- 리프레시 토큰 생성:
- 동일한 사용자 정보를 기반으로 1일 동안 유효한 리프레시 토큰을 생성.
- 리프레시 토큰은 refreshTokens 배열에 저장.
- 쿠키 설정:
- 리프레시 토큰은 클라이언트의 쿠키에 저장되며, 서버에만 접근 가능하도록 httpOnly 설정.
- 응답:
- 액세스 토큰을 JSON 형식으로 반환.
Authorization 헤더에서 액세스 토큰 추출.
-
- 토큰이 없으면 401 응답.
- jwt.verify를 통해 토큰 검증.
- 유효하지 않으면 403 응답.
- 유효하면 req.user에 사용자 정보를 저장하고 다음 핸들러로 이동.
- 쿠키 검증:
- 요청의 cookies.jwt를 통해 리프레시 토큰을 가져온다.
- 리프레시 토큰이 없다면 403 응답.
- 리프레시 토큰 유효성 확인:
- 토큰이 refreshTokens 배열에 포함되지 않았다면 403 응답.
- JWT 검증:
- refreshSecretText로 리프레시 토큰 검증.
- 유효하지 않으면 403 응답.
- 액세스 토큰 재발급:
- 리프레시 토큰이 유효하면 새 액세스 토큰을 생성하고 반환
GET | / | 기본 엔드포인트, 간단한 응답 반환 |
POST | /login | 로그인 및 JWT 발급 |
GET | /posts | 인증된 사용자만 게시글 데이터 반환 |
GET | /refresh | 리프레시 토큰을 사용하여 새 액세스 토큰 발급 |
'정보처리,전산 > NODEJS' 카테고리의 다른 글
호이스팅 실행 컨텍스트 (Execution Context) (0) | 2024.11.23 |
---|---|
클로저 | 실행컨텍스트 (0) | 2024.11.23 |
Express JS와 미들웨어 (0) | 2024.11.17 |
Express.js 미들웨어 설정 Postman으로 요청한 경우의 동작 (0) | 2024.11.17 |
nodemon 설치 | 설정 (0) | 2024.11.17 |