WEB - 로그인과 세션 그리고 토큰 (feat. JWT)

Yuni·2023년 6월 25일
1

WEB

목록 보기
8/12

서론

우리가 사이트를 이용할 때 로그인을 하면 그 다음부턴 술술 마이페이지 가서 내 정보도 변경하고, 나만의 장바구니에 물건도 담고 로그인이 필요한 페이지에 접근이 계속해서 가능하죠.

근데 생각해보면, 난 로그인을 한 번 밖에 하지 않았는데 내가 움직인 페이지는 모두 로그인이 필요합니다. 어떻게 이 사이트는(서버는) 내가 로그인한 걸 아는 걸까요?!

웹 환경에서는 반복적으로 사용되는 데이터나 정보를 종류와 특성에 맞게 저장하고 재활용하기 위해 쿠키, 캐시, 세션, 토큰 등 여러가지 방법을 사용합니다.


저는 팀 프로젝트를 하면서 유저기능을 맡아 처음으로 세션과 JWT, 쿠키에 대해 많은 생각을 해야 했습니다. 그래서 오늘은 세션 그리고 토큰(JWT)에 대해 이야기 해볼까 합니다.



🔑 세션: 아이디만 있으면 너를 알아볼 수 있어

사용자가 사이트에 한 번 로그인하면 일정 유효기간동안은 더 이상 아이디와 비밀번호를 입력하지 않아도 되도록 사용자가 이미 서버로부터 인증받았음을 증명해 주는 세션이라는 증서를 이용합니다.

사용자가 서버로 로그인을 성공하면 서버는 세션ID 라는 데이터를 만듭니다. 세션ID는 랜덤하고 수십자에서 수백자까지 되는 긴 길이의 영문과 숫자의 혼합된 형식을 갖고 있습니다. 후에 서버는 클라이언트에게 인가 후 영수증을 주듯 세션ID를 사용자에게 전달하고, 메모리에 세션ID와 함께 어떤 사용자의 것인지 기록해 놓습니다.

서버는 클라이언트로부터 로그인이 필요한 요청을 받으면 해당 요청에 세션ID가 적혀있는지를 확인합니다. 없다면 돌아가 라고 할 것이고 있다면 서버가 보관하고 있는 세션ID 중에 동일한 ID가 있는지 찾아보고 그것이 누구의 계정인지도 확인할 수 있겠죠.

예시코드

아래는 정말 간단하게 세션방식이 어떻게 작동하는지 보여주는 코드입니다.

const express = require('express');
const session = require('express-session');



const app = express();

// 세션 설정
app.use(session({
  secret: 'mysecretkey', // 세션 암호화에 사용되는 비밀키
  resave: false,
  saveUninitialized: false,
}));


app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// 미들웨어: 로그인 체크
const checkLoggedIn = (req, res, next) => {
  if (req.session.user) {
    // 세션에 사용자 정보가 있는 경우
    next();
  } else {
    res.status(401).json({ message: '로그인이 필요합니다.' });
  }
};

// 로그인 요청 처리
app.post('/login', (req, res) => {
  // 예시: 사용자 인증 로직
  const { username, password } = req.body;

  // 사용자 인증 로직 수행
  // (여기서는 간단히 username과 password가 'admin'인 경우에만 로그인 성공으로 가정)
  if (username === 'admin' && password === 'admin') {
    // 세션에 사용자 정보 저장
    req.session.user = {
      username: username,
    };
    res.json({ message: '로그인 성공' });
  } else {
    res.status(401).json({ message: '로그인 실패' });
  }
});

// 로그인한 사용자만 접근 가능한 API
app.get('/protected', checkLoggedIn, (req, res) => {
  res.json({ message: '인증된 사용자만 접근 가능한 페이지' });
});

// 서버 시작
app.listen(3000, () => {
  console.log('서버가 3000번 포트에서 실행 중입니다.');
});

장점

  • 서버 측 상태 유지(높은 보안): 세션 방식은 서버 측에서 사용자의 로그인 상태를 유지합니다. 서버에 세션 데이터를 저장하므로 클라이언트에 저장되는 토큰과는 달리 서버가 상태를 관리하기 때문에 클라이언트가 조작하거나 손상시키는 등의 행동을 방지할 수 있는데 이렇게 되면 보안과 제어의 측면에서 큰 이점을 기대할 수 있습니다.

  • 추가적인 데이터 저장: 세션은 사용자에 대한 추가 정보를 저장하기에 용이합니다. 로그인된 사용자의 권한이나 프로필과 같은 정보를 세션에 저장하여 필요한 경우 쉽게 액세스할 수 있습니다.

단점

  • 서버 자원 사용 및 관리: 세션은 서버에 저장되므로 서버 자원을 사용합니다. 장점이었던 '추가적인 데이터를 저장할 수 있다'는 양날의 검이라 결국 세션의 데이터 크기가 커지거나 많은 수의 사용자가 동시에 접속하는 경우 서버 자원 부하가 증가할 수 있습니다. 또한 서버에서 세션을 관리하기 때문에 만료 기간, 세션 데이터 삭제나 공유 등 세션에서 관리가 필요한 부분들을 모두 서버에서 처리해 추가적인 관리가 필요합니다.

  • 로드 밸런싱 문제: 여러 서버로 확장할 때 세션 데이터의 일관성과 로드 밸런싱 문제가 발생할 수 있습니다. 세션 데이터를 특정 서버에 종속되지 않도록 설계해야 하는 어려움이 있습니다.



💵 토큰: 위조 방지처리가 되있는 지폐같은 날 알아보면 돼

세션은 데이터를 빠르게 확인할 수 있다는 장점이 있지만 서버의 공간은 유한합니다. 서버에 동시 접속하는 사용자가 많아지면 메모리 공간이 부족해져서 서버에 부하가 걸리는 문제가 발생할 수 있겠죠. 메모리 공간을 많이 차지하는 세션 방식의 대안으로 요즘 웹사이트들은 로그인한 사용자에게 세션 아이디 대신 토큰을 발급해 주는 추세입니다.

토큰에는 특수한 수학적 원리가 적용되어 있어서 마치 위조 방지 처리된 지폐처럼 서버만이 유효한 토큰을 발행할 수 있고, 누가 조작한다면 바로 유효하지 않은 토큰이라고 돌아가라고 내쫓습니다. 토큰은 발급해준 토큰을 메모리에 저장해두지 않으니 서버 부담도 훨씬 줄일 수 있구요. 그저 이 토큰이 유효한지만 검사해주면 되는 것이죠.

요즘 유명한 JWT. 대표적인 토큰인증 방식 예시 중 하나입니다.

예시코드

아래는 정말 간단하게 jwt가 어떻게 작동하는지 보여주는 코드입니다.

const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

// 사용자 정보 (예시를 위해 하드코딩)
const users = [
  { id: 1, username: 'user1', password: 'password1' },
  { id: 2, username: 'user2', password: 'password2' }
];

// 로그인 엔드포인트
app.post('/login', (req, res) => {
  // 클라이언트로부터 받은 사용자 정보
  const { username, password } = req.body;

  // 사용자 인증
  const user = users.find(user => user.username === username && user.password === password);
  if (!user) {
    return res.status(401).json({ message: '인증 실패' });
  }

  // JWT 토큰 생성
  const token = jwt.sign({ userId: user.id }, '비밀키');

  // 토큰을 클라이언트에게 전송
  res.json({ token });
});

// 보호된 엔드포인트
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: '인증된 엔드포인트' });
});

// 토큰 인증 미들웨어
function authenticateToken(req, res, next) {
  // 헤더에서 토큰 추출
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (token == null) {
    return res.status(401).json({ message: '토큰 없음' });
  }

  // 토큰 검증
  jwt.verify(token, '비밀키', (err, user) => {
    if (err) {
      return res.status(403).json({ message: '토큰 인증 실패' });
    }

    // 요청에 사용자 정보 추가
    req.user = user;
    next();
  });
}

// 서버 시작
app.listen(3000, () => {
  console.log('서버가 3000 포트에서 실행 중입니다.');
});

장점

  • 확장성: 토큰 인증 방식은 서버 측에 세션 데이터를 유지할 필요가 없으므로 서버 확장성이 향상됩니다. 서버는 토큰을 발급하고 유효성을 확인하기만 하면 되기 때문에 여러 서버 간에 상태를 공유할 필요가 없어 서버의 부담 또한 줄어듭니다.

  • 분리된 인증 및 인가: 토큰은 자체적으로 인증 및 인가 정보를 포함하고 있으므로, 클라이언트가 토큰을 가지고 있다면 서버에 추가적인 요청을 하지 않고도 인증 및 인가를 수행할 수 있습니다. 고로 간단한 인증 및 인가 프로세스만 필요한 경우 클라이언트에서 직접 처리해 서버와의 불필요한 접촉을 줄일 수 있고, 사용자에게도 빠른 웹 환경을 제공할 수 있겠죠.

  • 범용성: 토큰 인증 방식은 토큰을 클라이언트 측에서 관리하기 때문에 웹 애플리케이션 뿐만 아니라 모바일 앱 등 다양한 플랫폼에서 사용될 수 있습니다.

단점

  • 토큰 관리: 모든 것은 양날의 검이죠. 토큰은 클라이언트 측에서 관리되는 그 장점이 또한 단점이 될 수 있는데요. 클라이언트 측에서 관리하기 때문에 토큰이 탈취되기도 쉽습니다. 토큰이 탈취되면 해당 사용자의 권한이 남용될 수 있으므로, 적절한 보안 조치는 꼭 필요하겠죠. (짧은 만료 시간 등..)

  • 토큰 만료: 토큰은 일정 기간 또는 특정 조건에 따라 만료될 수 있고, 만료가 되어야만 하죠. 만료된 토큰은 다시 인증을 거쳐야 하므로, 클라이언트는 계속해서 만료된 토큰을 감지하고 새로운 토큰을 요청해야 합니다. 결국 클라이언트와 서버 간의 추가적인 네트워크 통신을 필요로 하므로, 일부 상황에서는 추가적인 오버헤드가 발생할 수 있습니다. 또 자주 토큰이 만료되 로그아웃이 되고 재로그인을 하는 등 추가적인 인증 절차를 거친다면 사용자는 짜증나서 웹사이트를 꺼버릴 수도 있습니다.



결론

기존의 세션 인증 방식은 서버 측에서 세션을 생성하고 관리하여 세션 식별자를 클라이언트에게 쿠키로 전송하는 방식이었습니다. 그러나 이 방식은 서버 측에서 세션 데이터를 유지하고 관리해야 하기 때문에 서버의 부하와 확장성에 영향을 줄 수 있는 큰 단점이 있었습니다.

토큰 인증 방식은 세션의 단점을 극복하기 위해 등장한 방식으로, 서버가 상태를 관리하지 않고 클라이언트가 토큰을 갖고 인증을 수행합니다. 이를 위해 토큰은 클라이언트에게 발급되고, 클라이언트는 매 요청에서 토큰을 전송하여 인증을 완료해 서버확장성과 부담 두마리 토끼를 모두 잡았습니다. 다만 탈취당할 수 있다는 단점이 너무 크리티컬했던 것 같습니다.

그래서 최근에는 토큰을 쿠키에 담는 방식이 많이 사용되고 있습니다. 쿠키의 자동전송과 보안성, 지속성등의 대한 이유라고 합니다. (이 부분에 대해선 다음 포스트에서 더 깊게 이야기 해보도록 할게요!)

오늘 이야기한 세션과 토큰 각각의 특징과 장, 단점을 이용하여 각각의 상황과 요구사항에 따라 선택해서 사용하는게 좋겠습니다. 🫡

profile
Look at art, make art, show art and be art. So does as code.

1개의 댓글

comment-user-thumbnail
2023년 7월 14일

글 잘 읽었습니다! :)

답글 달기