JWT

2024. 11. 18. 12:25정보처리,전산/NODEJS

반응형

 

 

 

 

 Stateless의 정의

  • Stateless란 서버가 클라이언트와의 이전 요청 상태를 기억하지 않는 것을 의미한다.
  • 각 요청은 완전히 독립적이며, 서버는 요청 간의 관계를 추적하지 않는다.

 

첫 번째 요청:

  • 클라이언트는 서버에게 "나는 user123"이라고 자신의 정보를 보냈다.
  • 서버는 요청을 처리하고 응답을 반환한다. 하지만 요청을 처리한 후 user123이라는 정보는 사라진다.

두 번째 요청:

클라이언트가 서버에 다시 묻는다:

  • 서버는 이전 요청의 상태(즉, "user123"이라는 정보)를 기억하지 않는다.
  • Stateless 서버는 각 요청이 독립적이므로 이전에 받은 정보를 저장하지 않는다.

 왜 이런 일이 일어날까

HTTP는 본질적으로 Stateless 프로토콜이다.

  • HTTP 요청은 서버와 클라이언트 간의 독립적인 통신을 기반으로 하며, 요청 간의 상태를 공유하지 않는다.
  • 서버가 클라이언트의 상태를 기억하려면 추가적인 세션 관리 메커니즘이 필요하다.

Stateful과 비교

Stateful 방식으로 클라이언트와의 상태 정보를 유지하기 위해 세션이나 쿠키와 같은 기술을 사용한다.


5. Stateless의 장점

  • 확장성: 요청 간의 상태를 저장하지 않으므로 서버 간 부하를 쉽게 분산할 수 있다.
  • 유연성: 서버는 요청을 독립적으로 처리할 수 있어 클라이언트나 서버 간의 결합도가 낮다.

6. Stateless를 보완하는 방법

Stateless의 단점을 해결하기 위해 클라이언트와 서버 간에 상태 정보를 유지하려면 아래 방법들을 사용한다:

  1. 쿠키(Cookies): 클라이언트 측에 정보를 저장.
  2. 세션(Session): 서버 측에서 상태를 저장.
  3. JWT(Json Web Token): 요청 시 클라이언트가 상태 정보를 포함하여 서버에 전달.

 

Stateless는 서버가 클라이언트의 상태 정보를 기억하지 않기 때문에 발생한다. 이는 HTTP 프로토콜의 기본 동작 방식으로, 필요에 따라 추가적인 상태 관리 기술을 활용해 해결할 수 있다.

 

 

 

 

 

 

- 토큰 기반 인증 및 인가의 기본 흐름

1. 클라이언트의 요청:

클라이언트(브라우저)는 서버에 다음과 같이 요청한다:

 
안녕, 나는 123유저라고 해
  • 클라이언트는 자신이 누구인지(사용자 정보 또는 자격 증명) 서버에 알린다.
  • 이 요청은 보통 로그인 정보(예: 사용자 ID와 비밀번호)를 포함한다.

2. 서버가 토큰 생성:

서버는 클라이언트의 요청을 검증한 후(예: ID와 비밀번호가 맞는지 확인), 해당 유저 정보를 포함하는 토큰을 생성한다.

  • 이 토큰에는 사용자의 식별자(ID)와 권한 정보 등의 데이터를 포함할 수 있다.
  • 토큰은 보통 JWT(Json Web Token) 형식으로 암호화되거나 서명되어 발급된다.

3. 서버가 클라이언트에 토큰을 전달:

생성된 토큰은 HTTP 응답 헤더나 본문에 포함되어 클라이언트로 전송된다:

 
HTTP/1.1 200 OK Authorization: Bearer <발급된_토큰>

4. 클라이언트가 토큰을 저장:

클라이언트는 받은 토큰을 저장소에 보관한다. 보관 위치는 보안 수준에 따라 다른다:

  • 로컬 스토리지(Local Storage): 쉽게 접근 가능하지만, 보안에 취약할 수 있다.
  • 세션 스토리지(Session Storage): 브라우저 세션 동안만 유지.
  • 쿠키(Cookie): 보안 속성을 추가해 토큰을 저장할 수 있음(예: HttpOnly, Secure 속성).

5. 클라이언트가 요청 시 토큰 포함:

클라이언트는 다음 요청을 보낼 때 토큰을 HTTP 헤더에 포함한다:

 
 
GET /api/resource Authorization: Bearer <저장된_토큰>
  • "Bearer"는 인증 유형을 의미하며, 토큰을 전달하기 위한 표준 형식이다.

6. 서버가 토큰을 복호화 및 검증:

서버는 받은 요청에서 토큰을 추출하여 다음을 수행한다:

  1. 토큰 유효성 검증: 서명 또는 암호화를 확인해 토큰이 변조되지 않았는지 확인.
  2. 토큰 해독(복호화): 유저 정보를 추출.
  3. 인가 결정: 토큰에 포함된 권한 정보를 기반으로 클라이언트의 요청을 처리할지 여부를 결정.

흐름 요약:

  1. 클라이언트가 서버에 로그인 정보로 요청.
  2. 서버가 클라이언트의 정보를 검증 후 토큰을 생성.
  3. 생성된 토큰을 클라이언트에 전송.
  4. 클라이언트가 토큰을 저장.
  5. 이후 요청 시 클라이언트는 토큰을 헤더에 포함.
  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

  • nodemonNode.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는 일반적으로 세 가지 타입의 정보를 포함할 수 있다:

  1. 등록된 클레임 (Registered Claims):
    • JWT에서 미리 정의된 클레임으로, 특정 정보를 전달하는 데 사용된다.
    • 예시: iss (issuer), exp (expiration), sub (subject), aud (audience) 등.
  2. 공개 클레임 (Public Claims):
    • 사용자 정의 클레임으로, 필요한 정보를 자유롭게 포함할 수 있다.
    • 예시: 사용자 ID, 이메일 주소 등.
  3. 비공개 클레임 (Private Claims):
    • 두 시스템 간에만 의미가 있는 정보이다. 다른 시스템에서 사용할 수 없으며, 특정 목적에 맞게 정의된다.
    • 예시: 사용자 권한 정보, 세션 ID 등.

 


JWT 구조

JWT는 다음과 같은 세 부분으로 구성된다:

  1. Header: 토큰의 유형(예: JWT)과 사용하는 서명 알고리즘을 포함.
  2. Payload: 실제 데이터, 사용자의 정보나 권한 정보를 담은 JSON 객체.
  3. 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 TokenRefresh Token을 사용한 JWT 인증 및 권한 부여 흐름을 기반으로 각 단계에 대해 필요한 작업


1. 사용자가 로그인 시도

  • 클라이언트는 로그인 폼을 통해 사용자 아이디와 비밀번호를 입력한다.
  • 서버는 해당 정보를 바탕으로 데이터베이스에서 사용자를 조회하고, 비밀번호를 확인한다. 비밀번호가 일치하면 로그인 성공.

2. 로그인 완료 후 Access Token과 Refresh Token 발급

  • 사용자가 로그인에 성공하면, 서버Access TokenRefresh 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을 발급하여 클라이언트에 반환한다.
 

전체 흐름 

  1. 사용자가 로그인하여 서버에서 Access TokenRefresh Token을 발급받다.
  2. 클라이언트는 Access Token을 사용해 서버에 요청을 보낸다.
  3. 서버는 Access Token을 검증하고, 유효하면 요청한 데이터를 반환한다.
  4. Access Token이 만료되면, 클라이언트는 Refresh Token을 보내어 새로운 Access Token을 발급받다.
  5. 서버는 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를 통해 처리된다.

 

 

 

 

 

쿠키생성

 

 

30초안에 토큰 사용 가능

 

30초가 지난 후

 

 

 

 

 

 

 

 

 

 

 


쿠키 파서 모듈 추가

 
  • 설명: 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 리프레시 토큰을 사용하여 새 액세스 토큰 발급

 

 

 

 

 

 

반응형