개요

이 글에서는 JSON Web Tokens (JWT)에 대해 이야기하려고 한다. JWT는 JSON 객체로 정보를 안전하게 주고받는 표준으로 이 정보들은 디지털 서명되어 있어서 검증하고 신뢰할 수 있다. 주로 인증이나 정보 교환에 사용된다.

 

JWT 제작 시 주의점

1. HTTPS는 필수

프로덕션에서는 항상 HTTPS를 사용해야 한다. JWT가 전송 중에 암호화되므로, 도청 공격으로부터 안전해진다.

2. 비밀키 보안 강화

JWT를 서명하는 데 사용하는 비밀 키는 항상 안전하게 보관해야한다. 만약 노출된다면 공격자가 토큰을 위조할 수 있어서 위험하다.

3. 토큰 만료 시간

 토큰에 만료 시간을 설정하는 것은 꼭 필요하다. 토큰이 노출되더라도 만료 시간이 설정되어 있다면 피해를 최소화할 수 있다.

4. 민감한 정보는 패스

JWT의 페이로드에 민감한 데이터를 넣지 않는 것을 추천한다. 만약 토큰이 노출되면 그 안의 데이터도 노출되기 때문이다.

5. 클라이언트에서 토큰 처리 잘하기

브라우저의 로컬 스토리지나 세션 스토리지에 토큰을 저장하는 것은 피해야 한다. JavaScript로 접근할 수 없는 HttpOnly 쿠키를 사용하는 것이 제일 좋다.

6. 에러 처리 꼼꼼하게

JWT가 유효하지 않거나 만료되었거나 존재하지 않는 경우, 서버는 반드시 적절하게 응답해야 한다.

구현

그럼 이제 TypeScript와 Express를 사용해서 Node.js 서버에서 JWT 인증을 어떻게 구현하는지 알아보자.

먼저, 필요한 npm 패키지를 설치해야 한다.

npm install jsonwebtoken express
npm install @types/jsonwebtoken @types/express --save-dev


다음은 JWT 인증 구현 예시이다.

import express, { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const app = express();
const SECRET_KEY = "YOUR_SECRET_KEY";  // 비밀 키

// JWT를 확인하는 미들웨어
function authenticateToken(req: Request, res: Response, next: NextFunction) {
  // 헤더에서 토큰을 가져옵니다
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) return res.sendStatus(401); // 토큰이 없는 경우

  jwt.verify(token, SECRET_KEY, (err: any, user: any) => {
    if (err) return res.sendStatus(403);

    req.user = user;
    next();
  });
}

app.post('/login', (req, res) => {
  // 사용자 인증
  // 사용자 인증 로직 구현
  // 사용자 이름과 비밀번호를 req.body에서 가져오기 
  // 데이터베이스와 대조 (ex. bcrypt 활용)

  const username = req.body.username;
  const user = { name: username };

  // 액세스 토큰 생성
  const accessToken = jwt.sign(user, SECRET_KEY);
  res.json({ accessToken: accessToken });
});

app.get('/protected', authenticateToken, (req, res) => {
  // 이 경로는 JWT로 보호됩니다
  // 'authenticateToken'을 호출한 후에는 'req.user'에 사용자 정보가 있습니다
  res.json({ title: 'Protected content', user: req.user });
});

app.listen(3000, () => console.log('Server started on port 3000'));

 

토큰 저장

마지막으로, 액세스 토큰을 어디에 저장할지 결정하는 것도 중요하다. 이는 애플리케이션의 맥락과 보안 고려사항에 따라 달라진다. 각 방법의 장단점을 보고 판단하면 된다.

1. Cookie

쿠키에 토큰을 저장하는 것은 일반적인 방법이다. HttpOnly 쿠키는 JavaScript에서 접근할 수 없으므로 XSS 공격에 대해 안전하다. 하지만 CSRF 공격에 취약할 수 있으며, 이는 동기 토큰 패턴이나 SameSite 쿠키 속성과 같은 기법으로 완화할 수 있다.

2. LocalStorage

LocalStorage에 토큰을 저장하는 것은 쉽고 편리하며, CSRF 공격에 대해 안전하다. 하지만 JavaScript를 통해 접근할 수 있으므로 XSS 공격에 취약하다.

3. Private variable

JavaScript 클로저 내의 변수에 토큰을 저장하는 것은 가장 안전한 옵션이다. 이 방법은 토큰이 전역적으로 노출되지 않으며 XSS를 통해 접근할 수 없다. 하지만 이 방법은 페이지 간에 지속되지 않는다 즉, 사용자가 페이지를 이동하거나 새로 고침하면 토큰이 소실된다.
단일 페이지 애플리케이션 (SPA)의 맥락에서는 일반적인 패턴은 메모리에 액세스 토큰을 저장하고 (예: private variable), 만료될 때 새로운 액세스 토큰을 얻기 위해 HttpOnly 쿠키에 저장된 새로 고침 토큰을 사용하는 것이다. 이 방법은 사용성과 보안 사이에서 좋은 균형을 제공한다.

결론

어떤 솔루션도 100% 보안은 아니다. 애플리케이션의 특정 필요에 따라 최선의 방법이 결정된다. 패키지를 최신 상태로 유지하고, 사용자 입력을 sanitize하여 XSS 공격을 방지하며(ex. sanitize-html), 쿠키에 대해 HttpOnly와 Secure 플래그를 사용하고, CSRF 보호를 활성화하여 애플리케이션의 보안을 강화해야한다.

반응형

+ Recent posts