[로그인, 인증 기능 구축 프로젝트] 1. jwt를 이용한 인증 앱 생성하기

Shy·2023년 10월 3일
0

NodeJS(Express&Next.js)

목록 보기
34/39

인증 서비스 구현을 위해 필요한 지식

HTTP Stateless

"Stateless"라는 용어는 HTTP 프로토콜의 핵심 특징 중 하나로, 각 요청과 응답이 독립적이라는 것을 의미한다. HTTP가 Stateless 프로토콜이라는 것은 서버가 클라이언트의 이전 요청이나 상태에 대한 정보를 저장하지 않는다는 것이다. 각 요청은 별개의 트랜잭션으로 처리되며, 요청 간에 상태 정보가 유지되지 않는다.

첫 번째 요청에서 서버에 자신이 누군지 말해줘도, 서버에게 다시 물어봐도 서버는 사용자가 누구인지 모른다.
그 이유는 HTTP는 stateless이기 때문이다.

Status와 Info를 HTTP Request에 포함시키지 않는다.
(성능 문제: 각 요청에 대한 연결을 재설정하는 데 소요되는 시간/대역폭을 최소화하기 위한 것이다.)

Stateless의 특징

  1. 독립적인 요청: 서버는 각 요청을 완전히 독립적으로 처리합니다. 즉, 이전 요청의 상태를 기억하지 않는다.

  2. 클라이언트의 책임: 클라이언트가 이전 상태를 기억하고 필요한 정보를 매 요청마다 전달해야 한다.

  3. 상태 정보의 부재: 서버는 요청을 처리한 후 요청에 대한 상태 정보를 저장하지 않는다.

Stateless의 장점

  1. 단순성: 서버와 클라이언트 간의 상호작용이 간단해진다. 각 요청은 독립적으로 처리되므로, 복잡한 상태 관리 로직이 필요 없다.

  2. 확장성: 서버가 상태 정보를 저장하지 않으므로, 여러 서버 간의 부하 분산이 용이하다. 각 서버는 독립적으로 요청을 처리할 수 있다.

Stateless의 단점

  1. 상태 유지 필요성: 일부 애플리케이션에서는 클라이언트와 서버 간의 상태 유지가 필요할 수 있다. 이런 경우에는 별도의 메커니즘이 필요하다 (예: 세션, 쿠키).

  2. 데이터 중복: 클라이언트는 매 요청마다 필요한 상태 정보를 전달해야 하므로, 데이터 전송이 중복될 수 있다.

상태 유지 방법

HTTP가 Stateless 프로토콜이지만, 웹 애플리케이션에서는 상태 정보를 유지해야 하는 경우가 많다. 예를 들어, 로그인한 사용자의 정보, 장바구니 상태 등이 있다. 이러한 상태 정보를 유지하기 위한 기술로는 쿠키, 세션, 토큰 기반 인증 (예: JWT) 등이 있다.

정리

HTTP의 Stateless 특징은 프로토콜의 단순성과 확장성을 증대시킨다. 하지만 일부 상태 유지가 필요한 경우에는 추가적인 기술을 활용해야 한다.

JWT (JSON Web Token)에 대하여

JWT는 JSON 기반의 오픈 표준 (RFC 7519)으로, 두 개체 사이에서 정보를 안전하게 전송하기 위한 간결하고 자가 포함된 방법을 제공한다.

토큰을 생성할 때 사용하는 모듈이다.

주로 다음과같이 사용된다.

  1. 인증: 사용자가 로그인 한 후, 모든 subsequent request에는 JWT를 포함시켜 사용자를 식별한다.
  2. 정보 교환: 안전하고, 서명된 방식으로 정보를 교환할 수 있다.

구조

JWT는 세 부분으로 구성된다.

  1. Header: 토큰의 유형과 사용된 알고리즘을 정의한다. (메타데이터가 포함되 있다.)
  2. Payload: 토큰에 담을 클레임 (claims)이 들어 있습니다. 클레임은 토큰에 포함된 정보 조각이다. (유저 정보(issuer), 만료 기간(expiration time), 주제(subject) 등등...)
  3. Signature: 헤더와 페이로드를 암호화하는 데 사용되는 서명이다. 어떤 식으로든 변경되지 않았는지 확인하는 데 사용되는 서명이다.

세 부분은 각각 Base64Url로 인코딩되고, .으로 구분된다.

작동 방식

  1. 토큰 생성: 사용자가 시스템에 로그인하면, 서버는 사용자의 고유 정보와 서버의 비밀키를 사용해 JWT를 생성한다.
  2. 토큰 전송: 생성된 토큰은 클라이언트에게 전송된다.
  3. 토큰 사용: 클라이언트는 이후 요청마다 이 토큰을 Authorization 헤더에 포함시켜 보낸다.
  4. 토큰 검증: 서버는 받은 토큰이 유효한지 검증하고, 요청을 처리한다.

장점

  1. 자가 포함: JWT는 필요한 모든 정보를 자체적으로 가지고 있다.
  2. 확장성: 크로스 도메인 요청에 적합하며, CORS 문제에 대해 걱정할 필요가 없다.
  3. 모바일 어플리케이션에 적합: 모바일 환경에서도 잘 동작한다.

단점

  1. 토큰 크기: 페이로드에 많은 데이터를 포함시키면, 토큰의 크기가 커진다.
  2. 서버에서의 상태 관리 필요성: 일부 경우에서는 서버 측에서 토큰을 추적하거나 관리해야 하는 상황이 발생할 수 있다.

사용 주의사항

  1. 보안: JWT는 클라이언트 측에서 디코드할 수 있으므로, 민감한 정보는 포함하지 않아야 한다.
  2. HTTPS: JWT를 사용할 때는 통신 채널이 안전해야 하므로, HTTPS를 사용하는 것이 좋다.

간단한 인증 시스템 구현

로그인토큰 클라이언트에 전달토큰을 이용한 요청

구현

Express앱 생성

프로젝트 폴더 생성 후, package.json파일 생성

npm init

필요한 모듈을 설치

npm install dotenv express jsonwebtoken nodemon
  • dotenv: 환경 변수 생성을 위한 모듈
  • jsonwebtoken: 토큰 생성을 위한 모듈

express 코드 작성

const express = require('express')

const app = express();

app.use(express.json());

const port = 4000;
app.listen(port, () => {
  console.log('listening on port ' + port);
})
  • const express = require('express')
    • express 모듈을 불러와 express 상수에 할당한다.
    • require('express')는 Express.js 라이브러리를 가져오는 명령이다.
  • const app = express();
    • express 함수를 호출하여 Express 애플리케이션 인스턴스를 생성하고, app 상수에 할당한다.
    • app은 Express 애플리케이션의 주요 인스턴스로, Express 애플리케이션을 설정하고 실행하는 데 사용된다.
  • app.use(express.json());
    • express.json() 미들웨어를 애플리케이션 스택에 추가한다.
    • express.json() 미들웨어는 클라이언트로부터 받은 JSON 데이터를 파싱하고, req.body 객체에 할당한다.
    • 이 미들웨어는 클라이언트가 서버에 JSON 형태로 데이터를 보낼 때 사용된다.

추가 사항

  • 현재 코드는 서버를 실행하고 JSON 데이터를 파싱할 수 있는 기본 설정만을 가지고 있다.
  • 실제 웹 서버 구현에서는 app.get(), app.post(), app.put(), app.delete() 등의 메서드를 사용하여 여러 라우트와 핸들러를 정의하게 된다.
  • 라우트는 클라이언트의 요청 URL과 HTTP 메서드에 따라 적절한 핸들러 함수를 호출한다.
  • 핸들러 함수는 요청(req)과 응답(res) 객체를 사용하여 클라이언트의 요청을 처리하고 응답을 전송한다.

login.api

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)를 생성하고, 이를 응답으로 반환한다.

  1. app.post('/login/', (req, res) => { ... })
    • app.post 메서드를 사용하여 /login/ 경로로 들어오는 POST 요청을 처리하는 라우트를 정의한다.
    • (req, res)는 요청과 응답 객체를 나타낸다.
  2. const username = req.body.username
    • 클라이언트로부터 전송받은 요청 본문(req.body)에서 username을 추출한다.
    • 클라이언트는 POST 요청 본문에 JSON 형태로 username을 포함시켜 전송해야 한다.
  3. const user = { name: username }
    • 추출된 username을 사용하여 user 객체를 생성한다.
    • 이 객체는 JWT를 생성할 때 사용된다.
  4. const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
    • jwt.sign 메서드를 사용하여 user 객체와 환경 변수 ACCESS_TOKEN_SECRET의 값으로 JWT를 생성한다.
    • process.env.ACCESS_TOKEN_SECRET은 서버의 환경 변수로, JWT의 서명에 사용되는 비밀 키를 포함해야 한다.
    • jwt.sign의 첫 번째 인자는 토큰의 페이로드에 포함될 데이터, 두 번째 인자는 비밀 키이다.
  5. res.json({ accessToken: accessToken })
    • 생성된 accessToken을 JSON 형태로 응답 본문에 담아 클라이언트에게 반환한다.
    • 클라이언트는 이 토큰을 사용하여 보호된 라우트에 접근할 수 있다.

추가 사항

  • 이 코드는 단순 예제이며 실제 환경에서 사용하기 위해서는 보안, 에러 처리, 사용자 인증 등에 대한 추가 로직이 필요하다.
  • jwt 객체가 정의되어 있지 않다면, const jwt = require('jsonwebtoken');와 같이 jsonwebtoken 패키지를 불러와야 한다. 이 패키지는 먼저 설치해야 한다(npm install jsonwebtoken).
  • ACCESS_TOKEN_SECRET 환경 변수는 애플리케이션의 안전을 위해 안전한 방법으로 저장 및 관리되어야 한다.

전문

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);
})

get post api

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'
  }
]
  • posts 상수는 두 개의 포스트 객체를 포함하는 배열이다.
  • 각 포스트 객체는 username과 title 두 개의 속성을 가지고 있다.
  • 이 배열은 예제 데이터로, 실제 애플리케이션에서는 데이터베이스 등의 데이터 저장소에서 가져올 수 있다.
app.get('/posts', (req, res) => {
  res.json(posts)
})
  • app.get 메서드를 사용하여 /posts 경로로 들어오는 GET 요청을 처리하는 라우트를 정의한다.
  • 클라이언트로부터 /posts 경로로 GET 요청이 들어오면, (req, res) 콜백 함수가 실행된다.
  • res.json(posts)는 posts 배열을 JSON 형식으로 변환하여 응답 본문에 담아 클라이언트에게 전송한다.
  • 클라이언트는 /posts URL로 GET 요청을 보냈을 때 posts 배열을 JSON 형식으로 받을 수 있다.

전문

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);
})

토큰을 이용해서 요청 보내기

인증이 된 사람만 요청을 Post를 가져갈 수 있게 만들기

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)
})
  • app.get 메서드를 사용하여 /posts 경로로 들어오는 GET 요청을 처리한다.
  • authMiddleware 는 해당 라우트에 대한 요청이 실제 핸들러에 도달하기 전에 실행되는 미들웨어 함수이다. 이 미들웨어는 요청이 인증된 경우에만 요청을 다음 핸들러로 전달한다.
function authMiddleware(req, res, next) { ... }
  • authMiddleware는 인증 미들웨어 함수를 정의한다.
const authHeader = req.headers['authorization']
  • 요청 헤더에서 'authorization' 헤더를 추출한다. 일반적으로 'authorization' 헤더에는 "Bearer {토큰}" 형태로 토큰이 포함된다.
const token = authHeader && authHeader.split(' ')[1]
  • 'authorization' 헤더가 존재하면, 헤더 값을 공백을 기준으로 분리하고 두 번째 부분(토큰)을 추출한다.
  • 보통 authHeader은 'Bearer iadfjojgoaadhah.hesgfdhgf.aehgsdhs' 형태로 되어있는데 앞에 Bearer는 필요 없으므로, 공백일 기준으로 분리하고 뒤에 토큰만 추출하는 것이다.
if (token == null) return res.sendStatus(401)
  • 토큰이 없는 경우, 401 Unauthorized 상태 코드를 응답하여 클라이언트에게 인증이 필요하다는 것을 알린다.
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { ... })
  • jwt.verify 함수를 사용하여 토큰을 검증한다. (토큰이 유효한지 검증)
if (err) return res.sendStatus(403)
  • 토큰 검증에서 에러가 발생하면 403 Forbidden 상태 코드를 응답한다.
req.user = user
  • 토큰 검증이 성공하면, 토큰의 페이로드에서 추출한 user 객체를 req.user에 할당한다.
next()
  • 요청을 다음 미들웨어 또는 라우트 핸들러로 전달한다.

토큰 없이 정보를 전달하면 위와 같이 Unauthorized가 나온다.

토큰을 입력하여 정보를 전달하면 위와 같이 잘 응답한다.

RefreshToken에 대해서

Refresh Token은 웹 애플리케이션에서 주로 사용되는 인증 방법 중 하나로, Access Token과 쌍으로 사용된다. Refresh Token의 주요 목적은 Access Token의 수명이 만료된 후에도 사용자를 다시 로그인 시키지 않고 새로운 Access Token을 발급받는 것이다.

현재 앱에서 accessToken 하나만 있다면 이것을 가지고 계속 인증이 필요한 요청을 보낼 수 있다.
로그인을 한번 더 해서 다른 토큰을 받고 그 이전에 토큰을 이용해도 아직 유효하다.

보안 문제가 발생할 수 있다.
토큰의 유효시간이 너무 짧으면, 자동으로 로그아웃돼서 너무 자주 로그인을 다시 해야한다.
토큰의 유효시간이 너무 길면, 토큰에 유효시간을 주는 이유가 사라지게 된다. 토큰이 탈취당하면 긴 유효시간이 끝날 때 까지 계속 탈취당한 토큰이 사용된다.
이러한 문제점을 보완하기 위해 RefreshToken을 사용한다.

refresh토큰은 accessToken의 유효시간은 짧게 해주며, refreshToken의 유효시간은 길게 해준다. 그래서 accessToken의 유효시간이 다 지나면 refreshToken을 이용해서 새로운 accessToken을 발급해준다.

토큰설명
accessTokenAccess Token은 리소스에 접근하기 위해서 사용되는 토큰이다.
refreshTokenRefresh Token은 기존에 클라이언트가 가지고 있던 Access Token이 만료되었을 때 Access Token을 새로 발급받기 위해 사용한다.

RefreshToken AccessToken Flow

  1. 사용자가 로그인 시도를 한다.
    • 서버에서는 로그인 회원을 DB에서 찾아본다. 또한 비밀번호가 맞는지 확인한다.
  2. 로그인이 완료되면 Access Token과 함께 Refresh Token이 발급된다.
    회원 DB에 일반적으로 Refresh Token을 저장한다.
  3. 사용자가 Refresh Token을 안전한 장소에 보관한 후, 서버에 요청을 보낼 때 Access Token을 헤더에 실어 요청을 보낸다
  4. Access Token을 서버에서 검증하게 되며 맞는 Token 이라면 요청한 데이터를 보내준다.
  5. Access Token의 유효기간이 만료되었을 때 요청을 보낸다.
  6. Access Token의 유효기간이 만료되었기 때문에 권한 없음 에러를 보낸다.
  7. 사용자는 Refresh Token을 서버로 보내며 Acces Token 발급 요청을 한다.
  8. 서버에서는 사용자가 보낸 Refresh Token과 DV에 저장되어 있는 Refresh Token을 비교한다.
    RefreshToken이 동일하고 유효기간도 지나지 않았다면 Access Token을 새로 발급한다.

RefreshToken작동 방식

  1. 토큰 발급: 사용자가 처음 로그인할 때, 서버는 Access Token과 Refresh Token을 함께 발급한다.
  2. Access Token 사용: Access Token은 짧은 수명을 가지고 있으며, 이 토큰으로 보호된 리소스에 접근한다.
  3. Access Token 만료: Access Token의 수명이 만료되면, 보호된 리소스에 접근할 수 없게 된다.
  4. Refresh Token 사용: Access Token이 만료된 경우, Refresh Token과 함께 서버에 새로운 Access Token을 요청한다.
  5. 새로운 Access Token 발급: 서버는 Refresh Token을 검증한 후, 유효하다면 새로운 Access Token을 발급한다.

Refresh Token의 특징

  1. 긴 수명: Refresh Token은 Access Token에 비해 긴 수명을 가진다.
  2. 보안: Refresh Token이 유출되면, 공격자가 유효 기간 동안 계속해서 Access Token을 발급받을 수 있으므로, Refresh Token의 보안은 매우 중요하다.
  3. 저장: 클라이언트 측에서는 안전하게 저장되어야 한다. 웹 애플리케이션의 경우, HttpOnly 쿠키를 사용하는 것이 일반적이다.

Refresh Token의 관리

  1. 리스트 관리: 발급된 Refresh Token은 서버 측에서 관리되어야 한다. 블랙리스트나 화이트리스트를 통해 유효한 토큰을 관리할 수 있다.
  2. 토큰 만료: Refresh Token에도 만료 기간을 설정하여, 정기적으로 사용자에게 재인증을 요청하는 것이 좋다.
  3. 토큰 갱신: 새로운 Access Token을 발급받을 때마다, 새로운 Refresh Token도 발급하여 교체하는 전략을 사용할 수 있다.

사용 예

  • 사용자는 이메일과 패스워드로 로그인한다.
  • 서버는 인증이 성공하면 Access Token과 Refresh Token을 발급한다.
  • 사용자는 Access Token으로 보호된 리소스에 접근한다.
  • Access Token이 만료되면, 사용자는 Refresh Token을 사용하여 새 Access Token을 요청한다.
  • 서버는 Refresh Token을 검증하고, 새 Access Token을 발급한다.

결론

Refresh Token은 사용자 경험을 향상시키기 위해 사용되며, 사용자가 반복적으로 로그인해야 하는 불편함을 줄인다. 하지만, 이를 안전하게 관리하고 적절한 보안 조치를 취하는 것이 중요하다.

RefreshToken 생성하기

로그인시에 refreshToken도 생성하기

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 });
})
  • accessToken은 유효시간을 짧게 하고, refreshToken은 그보다 길게 한다.
  • refreshToken은 쿠키에 저장하지만 주로 httpOnly옵션을 줘서 javascript를 이용해서 탈취하거나 조작할 수 없게 만든다. (XSS Cross Site Scripting 공격)
  • accessToken은 cookie나 localstorage, 메모리에 저장할 수 있다. (let 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);
})

  • 리프래쉬 토큰이 쿠키에 잘 들어갔음을 볼 수 있다.

  • 30초가 지나고 요청을 보내면 Forbidden이 나온다.
  • 이 문제를 해결하려면 RefreshToken으로 accessToken을 생성해야 한다.

RefreshToken으로 accessToken 생성하기

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) => { ... }):
  • /refresh 경로로 들어오는 GET 요청을 처리한다.
const cookies = req.cookies;:
  • 요청에서 쿠키를 추출한다. Express의 기본 req 객체에는 cookies 프로퍼티가 없으므로, cookie-parser 미들웨어를 사용해야 한다.
if (!cookies?.jwt) return res.sendStatus(401);:
  • 쿠키 중 jwt 쿠키가 없다면 401 Unauthorized 응답을 반환한다.
const refreshToken = cookies.jwt;:
  • jwt 쿠키를 refreshToken 변수에 할당한다.
if (!refreshTokens.includes(refreshToken)) return res.sendStatus(403);:
  • refreshTokens 배열에 refreshToken이 포함되어 있지 않다면, 403 Forbidden 응답을 반환한다.
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => { ... }):
  • refreshToken을 환경 변수 REFRESH_TOKEN_SECRET으로 검증한다.
if (err) return.res.sendStatus(403);:
  • 토큰 검증 중 오류가 발생하면 403 Forbidden 응답을 반환한다.
const accessToken = jwt.sign({ name: user.name }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '30s' });:
  • 검증된 user의 name을 페이로드로 사용해, 새로운 액세스 토큰을 생성한다. 이 토큰은 30초 동안 유효한다.
res.json({ accessToken });:
  • 새로 생성된 액세스 토큰을 JSON 형태로 응답한다.

전문

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);
})
profile
초보개발자. 백엔드 지망. 2024년 9월 취업 예정

0개의 댓글