개발 언어/기타 웹개발 지식

웹 인증의 미로를 헤쳐나가기: 2024년 종합 가이드

jjiiiinn 2024. 8. 21. 21:34
728x90

웹 인증의 미로를 헤쳐나가기: 2024년 종합 가이드

안녕하세요, 오늘은 웹 애플리케이션의 핵심인 인증 방식에 대해 깊이 있게 살펴보려고 합니다. 세션 기반, JWT, SSO, OAuth 2.0 등 다양한 인증 방식의 장단점을 비교하고, 각각의 사용 사례를 상세히 알아보겠습니다. 이 글을 통해 여러분의 프로젝트에 가장 적합한 인증 방식을 선택하는 데 도움이 되길 바랍니다.

1. 세션 기반 인증: 전통의 힘

세션 기반 인증은 오랫동안 사용되어 온 방식으로, 그 안정성과 단순함으로 여전히 많은 개발자들의 사랑을 받고 있습니다.

작동 원리

세션 기반 인증의 작동 과정을 자세히 살펴봅시다:

  1. 사용자 로그인: 사용자가 아이디와 비밀번호를 입력합니다.
  2. 서버 검증: 서버는 입력된 정보를 검증하고, 올바르면 세션을 생성합니다.
  3. 세션 ID 생성: 서버는 고유한 세션 ID를 생성합니다. 이 ID는 보통 랜덤한 문자열입니다.
  4. 세션 저장: 서버는 생성된 세션 정보를 메모리나 데이터베이스에 저장합니다.
  5. 쿠키에 세션 ID 저장: 서버는 세션 ID를 클라이언트의 쿠키에 저장합니다.
  6. 후속 요청: 클라이언트는 이후의 모든 요청에 이 쿠키를 함께 전송합니다.
  7. 서버 인증: 서버는 쿠키의 세션 ID를 확인하여 사용자를 인증합니다.
sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant S as 서버
    participant DB as 데이터베이스

    U->>C: 1. 아이디/비밀번호 입력
    C->>S: 2. 로그인 요청
    S->>DB: 2. 사용자 정보 확인
    DB-->>S: 2. 사용자 정보 반환
    S->>S: 3. 세션 ID 생성
    S->>DB: 4. 세션 정보 저장
    S-->>C: 5. 세션 ID를 쿠키에 설정
    C-->>U: 로그인 성공 표시

    Note over C,S: 이후 요청...

    U->>C: 6. 다른 페이지 요청
    C->>S: 6. 요청 (쿠키와 함께)
    S->>DB: 7. 세션 ID 확인
    DB-->>S: 7. 세션 정보 반환
    S-->>C: 요청한 페이지 반환
    C-->>U: 페이지 표시

"세션 기반 인증은 마치 도서관의 회원 카드와 같습니다. 한 번 발급받으면, 그 카드로 계속 서비스를 이용할 수 있죠."

장단점

장점:

  • 구현이 간단함: 대부분의 웹 프레임워크가 기본적으로 지원합니다.
  • 서버에서 세션 완전 제어 가능: 필요시 즉시 세션을 무효화할 수 있습니다.
  • 보안성: 세션 ID만 클라이언트에 저장되므로, 중요 정보 노출 위험이 적습니다.

단점:

  • 서버 부하: 동시 접속자가 많을 경우 서버에 부담이 될 수 있습니다.
  • 확장성 문제: 여러 서버를 사용하는 경우, 세션 정보 공유에 추가 설정이 필요합니다.
  • CSRF 공격에 취약: 적절한 대책이 필요합니다.

실제 구현 예시

Express.js를 사용한 세션 기반 인증 구현의 상세 예시를 살펴보겠습니다:

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

// 세션 미들웨어 설정
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true, maxAge: 24 * 60 * 60 * 1000 } // 24 hours
}));

// 데이터베이스 대신 사용할 간단한 사용자 객체
const users = {
  'user@example.com': {
    id: 1,
    passwordHash: '$2b$10$Bqs4tZ5Ht3pZsfG0ebI9uexq2lmb88bV9Q/E2w5iOY.4emiBEOHK'  // 'password123'의 해시
  }
};

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users[email];

  if (user && await bcrypt.compare(password, user.passwordHash)) {
    req.session.userId = user.id;
    res.send('로그인 성공!');
  } else {
    res.status(401).send('로그인 실패');
  }
});

app.get('/dashboard', (req, res) => {
  if (req.session.userId) {
    res.send('대시보드에 오신 것을 환영합니다');
  } else {
    res.status(401).send('로그인이 필요합니다');
  }
});

app.post('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      res.status(500).send('로그아웃 중 오류 발생');
    } else {
      res.send('로그아웃 성공');
    }
  });
});

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

이 예제는 로그인, 대시보드 접근, 로그아웃 기능을 구현하며, 비밀번호 해싱을 위해 bcrypt를 사용합니다.
참고. BCrypt 심층 분석

2. JWT: 현대적인 무상태 해결책

JSON Web Token (JWT)은 최근 많은 인기를 얻고 있는 인증 방식입니다. 특히 RESTful API와 단일 페이지 애플리케이션(SPA)에서 자주 사용됩니다.

JWT의 구조

JWT는 세 부분으로 구성되며, 각 부분은 점(.)으로 구분됩니다:

  1. 헤더 (Header):
    • 토큰 유형 (typ)
    • 해시 알고리즘 (alg)
      예: { "alg": "HS256", "typ": "JWT" }
  2. 페이로드 (Payload):
    • 클레임(claims)이라 불리는 사용자 데이터나 메타 정보
      예: { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
  3. 서명 (Signature):
    • 헤더의 인코딩 값과 페이로드의 인코딩 값을 합친 후 비밀키로 해시하여 생성

작동 원리

  1. 사용자 로그인
  2. 서버는 JWT 생성 (페이로드에 사용자 정보 포함)
  3. 서버가 JWT를 클라이언트에 전송
  4. 클라이언트는 JWT를 저장 (보통 로컬 스토리지나 쿠키에)
  5. 이후 요청 시 클라이언트는 Authorization 헤더에 JWT를 포함하여 전송
  6. 서버는 JWT의 서명을 검증하고 요청을 처리
sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant S as 서버

    U->>C: 1. 로그인 정보 입력
    C->>S: 1. 로그인 요청
    S->>S: 2. JWT 생성 (페이로드에 사용자 정보 포함)
    S-->>C: 3. JWT 전송
    C->>C: 4. JWT 저장 (로컬 스토리지 또는 쿠키)
    C-->>U: 로그인 성공 표시

    Note over C,S: 이후 요청...

    U->>C: 5. 보호된 리소스 요청
    C->>S: 5. 요청 (Authorization 헤더에 JWT 포함)
    S->>S: 6. JWT 서명 검증
    S-->>C: 요청한 리소스 반환
    C-->>U: 리소스 표시

장단점

장점:

  • 무상태성: 서버가 클라이언트의 상태를 저장할 필요가 없습니다.
  • 확장성: 서버 간 사용자 정보 공유가 필요 없어 수평적 확장이 용이합니다.
  • 크로스 도메인 / CORS: 여러 도메인에서 토큰을 사용할 수 있습니다.
  • 디코딩 용이성: 페이로드에서 필요한 정보를 쉽게 얻을 수 있습니다.

단점:

  • 토큰 크기: 세션 ID에 비해 크기가 더 큽니다.
  • 보안: 한번 발급된 토큰은 만료되기 전까지 계속 유효합니다.
  • 저장: 클라이언트 측에서 안전하게 저장해야 합니다.

JWT 실전 예제

Express.js와 jsonwebtoken 라이브러리를 사용한 JWT 인증의 상세 구현:

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

app.use(express.json());

const SECRET_KEY = 'your-secret-key';

// 데이터베이스 대신 사용할 간단한 사용자 객체
const users = {
  'user@example.com': {
    id: 1,
    passwordHash: '$2b$10$Bqs4tZ5Ht3pZsfG0ebI9uexq2lmb88bV9Q/E2w5iOY.4emiBEOHK'  // 'password123'의 해시
  }
};

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users[email];

  if (user && await bcrypt.compare(password, user.passwordHash)) {
    const token = jwt.sign(
      { userId: user.id, email: email },
      SECRET_KEY,
      { expiresIn: '1h' }
    );
    res.json({ token });
  } else {
    res.status(401).send('로그인 실패');
  }
});

app.get('/dashboard', (req, res) => {
  const token = req.headers['authorization']?.split(' ')[1];

  if (!token) {
    return res.status(401).send('토큰이 제공되지 않았습니다');
  }

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    res.send(`환영합니다, ${decoded.email}님의 대시보드입니다`);
  } catch (err) {
    res.status(401).send('유효하지 않은 토큰');
  }
});

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

이 예제는 JWT를 사용한 로그인 및 인증된 라우트 접근을 구현합니다.
참고. JWT 완벽 가이드

3. SSO와 OAuth 2.0: 연결의 시대

Single Sign-On (SSO)과 OAuth 2.0은 여러 서비스 간의 인증을 효율적으로 관리하는 방법을 제공합니다.

SSO: 하나의 열쇠로 모든 문을 열다

SSO는 사용자가 한 번의 인증으로 여러 관련 시스템에 접근할 수 있게 해줍니다.

SSO 작동 원리

  1. 사용자가 서비스 A에 접근 시도
  2. 서비스 A가 사용자를 SSO 서버로 리다이렉트
  3. 사용자가 SSO 서버에 로그인 (이미 로그인 되어 있다면 이 단계 생략)
  4. SSO 서버가 인증 토큰을 생성하고 서비스 A로 리다이렉트
  5. 서비스 A가 SSO 서버에 토큰 유효성 확인
  6. 인증 성공, 서비스 A 이용 가능
  7. 사용자가 서비스 B에 접근 시, 단계 4-6 반복 (새로운 로그인 불필요)
sequenceDiagram
    participant U as 사용자
    participant SA as 서비스 A
    participant SSO as SSO 서버
    participant SB as 서비스 B

    U->>SA: 1. 서비스 A 접근 시도
    SA-->>U: 2. SSO 서버로 리다이렉트
    U->>SSO: 3. SSO 서버에 로그인
    SSO->>SSO: 4. 인증 토큰 생성
    SSO-->>U: 4. 서비스 A로 리다이렉트 (토큰 포함)
    U->>SA: 5. 토큰과 함께 다시 접근
    SA->>SSO: 5. 토큰 유효성 확인
    SSO-->>SA: 5. 토큰 유효성 응답
    SA-->>U: 6. 서비스 A 접근 허용

    Note over U,SB: 나중에 서비스 B 접근 시...

    U->>SB: 7. 서비스 B 접근 시도
    SB-->>U: 7. SSO 서버로 리다이렉트
    U->>SSO: 7. 이미 인증됨
    SSO->>SSO: 7. 새 토큰 생성
    SSO-->>U: 7. 서비스 B로 리다이렉트 (새 토큰 포함)
    U->>SB: 7. 새 토큰과 함께 다시 접근
    SB->>SSO: 7. 토큰 유효성 확인
    SSO-->>SB: 7. 토큰 유효성 응답
    SB-->>U: 7. 서비스 B 접근 허용

SSO의 장단점

장점:

  • 사용자 경험 향상 (한 번의 로그인으로 여러 서비스 이용)
  • 보안 강화 (중앙화된 인증 시스템)
  • 관리 효율성 (하나의 계정으로 여러 서비스 관리)

단점:

  • 구현 복잡성
  • 단일 실패 지점 (SSO 서버 장애 시 모든 서비스 영향)

"SSO는 디지털 시대의 만능 열쇠와 같습니다. 한 번의 인증으로 여러 문을 열 수 있지만, 그만큼 그 열쇠를 잘 관리해야 합니다."

OAuth 2.0: 권한 부여의 프레임워크

OAuth 2.0은 사용자의 비밀번호를 공유하지 않고도 제3자 애플리케이션에 제한된 접근 권한을 부여할 수 있게 해줍니다.

OAuth 2.0 주요 개념

  • Resource Owner: 보호된 자원의 소유자 (일반적으로 사용자)
  • Client: 보호된 자원에 접근하려는 애플리케이션
  • Resource Server: 보호된 자원을 호스팅하는 서버
  • Authorization Server: 인증을 처리하고 액세스 토큰을 발급하는 서버

OAuth 2.0 그랜트 타입

  1. Authorization Code:
    • 가장 일반적으로 사용되는 플로우
    • 서버 사이드 애플리케이션에 적합
    • 보안성이 높음
    • 과정:
      1. 클라이언트가 사용자를 인증 서버로 리다이렉트
      2. 사용자가 로그인하고 권한 부여
      3. 인증 서버가 인증 코드를 클라이언트로 전송
      4. 클라이언트가 인증 코드로 액세스 토큰 요청
      5. 인증 서버가 액세스 토큰 발급
  2. Implicit:
    • 클라이언트 사이드 애플리케이션(예: 단일 페이지 앱)용
    • 인증 코드 단계 없이 바로 액세스 토큰 발급
    • 보안상 권장되지 않으며, 점차 사용이 줄어들고 있음
  3. Client Credentials:
    • 클라이언트 자체 인증에 사용 (사용자 컨텍스트 없음)
    • 서버 간 API 통신에 적합
    • 가장 단순한 플로우: 클라이언트 ID와 시크릿으로 직접 액세스 토큰 요청
  4. Resource Owner Password Credentials:
    • 사용자의 username과 password를 직접 사용
    • 높은 신뢰도의 애플리케이션에서만 사용 권장
    • 레거시 시스템 통합 시 사용될 수 있음
  5. Refresh Token:
    • 액세스 토큰 만료 시 새로운 액세스 토큰을 얻기 위해 사용
    • 사용자 재인증 없이 세션 연장 가능
sequenceDiagram
    participant U as 사용자
    participant C as 클라이언트
    participant AS as 인증 서버
    participant RS as 리소스 서버

    C->>U: 서비스 이용 시도
    C->>AS: 1. 인증 서버로 리다이렉트
    AS->>U: 로그인 페이지 표시
    U->>AS: 2. 로그인 및 권한 부여
    AS->>C: 3. 인증 코드 전송
    C->>AS: 4. 인증 코드로 액세스 토큰 요청
    AS->>C: 5. 액세스 토큰 발급
    Note over C,RS: 이후 리소스 접근
    C->>RS: 액세스 토큰으로 리소스 요청
    RS->>C: 리소스 제공
    C->>U: 서비스 제공

OAuth 2.0 구현 예시

다음은 Express.js를 사용한 OAuth 2.0의 Authorization Code 플로우 구현 예시입니다:

const express = require('express');
const axios = require('axios');
const app = express();

const clientId = 'your-client-id';
const clientSecret = 'your-client-secret';
const redirectUri = 'http://localhost:3000/callback';

app.get('/login', (req, res) => {
  const authUrl = `https://oauth-provider.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=read_user`;
  res.redirect(authUrl);
});

app.get('/callback', async (req, res) => {
  const { code } = req.query;

  try {
    // 액세스 토큰 요청
    const tokenResponse = await axios.post('https://oauth-provider.com/token', {
      code,
      client_id: clientId,
      client_secret: clientSecret,
      redirect_uri: redirectUri,
      grant_type: 'authorization_code'
    });

    const { access_token } = tokenResponse.data;

    // 액세스 토큰을 사용하여 사용자 정보 요청
    const userResponse = await axios.get('https://oauth-provider.com/user', {
      headers: { Authorization: `Bearer ${access_token}` }
    });

    res.json(userResponse.data);
  } catch (error) {
    res.status(500).send('인증 과정에서 오류 발생');
  }
});

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

이 예제는 OAuth 2.0 제공자와의 인증 과정을 시작하고, 콜백을 처리하여 액세스 토큰을 얻은 후 사용자 정보를 요청하는 과정을 보여줍니다.

결론: 2024년, 당신의 프로젝트에 맞는 인증 방식은?

지금까지 우리는 다양한 인증 방식을 깊이 있게 살펴보았습니다. 각 방식은 고유한 장단점을 가지고 있으며, 선택은 여러분의 프로젝트 요구사항에 따라 달라질 것입니다.

  • 세션 기반:
    • 적합한 경우: 전통적인 웹 애플리케이션, 보안이 중요한 뱅킹 시스템
    • 장점: 구현 간단, 즉시 세션 무효화 가능
    • 단점: 서버 부하, 분산 시스템에서의 확장성 문제
  • JWT:
    • 적합한 경우: RESTful API, 단일 페이지 애플리케이션 (SPA), 모바일 앱
    • 장점: 무상태성, 확장성 좋음, 크로스 도메인에서 사용 용이
    • 단점: 토큰 크기, 즉시 무효화 어려움
  • SSO:
    • 적합한 경우: 대기업, 여러 관련 서비스를 가진 조직
    • 장점: 사용자 경험 향상, 중앙화된 인증 관리
    • 단점: 구현 복잡, 단일 실패 지점 위험
  • OAuth 2.0:
    • 적합한 경우: 서드파티 인증, API 접근 권한 관리
    • 장점: 유연성, 광범위한 지원, 다양한 시나리오 대응 가능
    • 단점: 구현 복잡성, 올바른 설정의 중요성

마지막으로, 어떤 인증 방식을 선택하든 보안은 지속적인 과정임을 명심하세요. 정기적인 보안 감사, 최신 보안 패치 적용, 그리고 새로운 위협에 대한 대비가 항상 필요합니다.

728x90