로그인 - 토큰 기반 인증 방식

이강용·2024년 6월 24일
0

CS

목록 보기
67/109

토큰 기반 인증 방식이란?

  • 사용자 인증과 권한 부여를 위한 방법 중 하나로, 주로 JSON Web Token(JWT)를 사용하여 액세스토큰과 리프레시 토큰을 발급하는 방식
  • 이는 세션 기반 인증 방식과는 다르게 서버가 상태를 저장하지 않기 때문에 확장성과 성능에 유리함

구성 요소

  1. 액세스 토큰(Access Token)
  • 사용자가 인증된 후, 서버가 발급하는 짧은 유효기간을 가진 토큰
    • 사용자가 보호된 리소스에 접근할 때 이 토큰을 사용하여 권한을 증명
    • 보통 짧게 설정되어 보안성을 높임 (예: 15분에서 1시간 사이)
  1. 리프레시 토큰(Refresh Token)
  • 액세스 토큰이 만료되었을 때, 새로운 액세스 토큰을 발급받기 위해 사용하는 토큰
    • 액세스 토큰이 유효기간이 만료된 후에도 사용자가 다시 로그인하지 않고 새로운 액세스 토큰을 발급받을 수 있게 함
    • 상대적으로 긴 유효기간을 가짐 (예: 며칠 ~ 몇 주)

토큰 관리 서버의 필요성

  • 토큰 기반 인증 방식은 상태를 모두 토큰 자체로 처리하며 토큰을 처리하는 서버를 별도로 두고 다른 컨텐츠를 제공하는 서버들은 모두 stateless하게 만드는 이론이 담긴 방식임

    그렇다면, 왜 토큰을 관리하는 서버를 따로 두어야 할까?

    • 이는 여러 개의 서버를 운영할 때, 토큰 기반 인증과 특정 도메인을 처리하는 서버를 함께 두는 경우 해당 도메인에서 에러가 발생하면 인증 기능이 마비되어 다른 도메인(예: B, C, D 등) 기능이 연쇄적으로 마비될 수 있기 때문임
    • 이러한 설계는 MSA 환경에 매우 적합함, MSA에서는 각 서비스가 독립적으로 운영되기 때문에 한 서비스의 장애가 다른 서비스에 영향을 미치지 않도록 토큰 관리 서버를 별도로 두는 것이 중요함

1. 로그인(Authentication)
- 사용자 인증 : 사용자가 사용자명과 비밀번호를 서버로 전송하여 로그인 요청을 함
- 서버 검증 : 서버는 자격 증명을 확인하고 유효한 경우 액세스 토큰과 리프레시 토큰을 발급

2. 토큰 발급(Token Issuance)
- 액세스 토큰 : 서버는 짧은 유효기간을 가진 액세스 토큰을 생성하여 클라이언트에게 반환
- 리프레시 토큰 : 서버는 긴 유효기간을 가진 리프레시 토큰을 생성하여 클라이언트에게 반환

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR...",
  "refreshToken": "dXNlcm5hbWU6cGFzc3dvcmQ..."
}

3. 인증된 요청(Authenticated Requests)
- 액세스 토큰 사용 : 클라이언트는 보호된 리소스에 접근할 때 HTTP 요청 헤더에 액세스 토큰을 포함하여 서버로 전송

GET /protected-resource
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

4. 토큰 검증(Token Validation)
- 서버 검증 : 서버는 액세스 토큰을 검증하여 유효성을 확인
   - 토큰이 유효하면 요청을 처리하고 보호된 리소스에 접근

5. 토큰 갱신(Token Renewal)
- 액세스 토큰 만료 : 액세스 토큰이 만료되면 클라이언트는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 요청
 
POST /refresh-token
Authorization: Bearer dXNlcm5hbWU6cGFzc3dvcmQ...
- 서버 검증 및 발급 : 서버는 리프레시 토큰을 검증하고 유효하면 새로운 액세스 토큰을 발급
   - 리프레시 토큰을 함께 발급할 수도 있음

6. 로그아웃(Logout)
- 토큰 폐기 : 사용자가 로그아웃하면 클라이언트는 저장된 토큰을 삭제
   - 서버 측에서는 리프레시 토큰을 무효화하여 보안을 강화할 수 있음

7. 보안 관리(Security Management)
- 토큰 유효성 관리 : 서버는 정기적으로 리프레시 토큰의 유효성을 검토하고 
  필요시 토큰을 무효화하거나 재발급함
- 위험 탐지 : 비정상적인 사용 패턴이나 보안 위협을 탐지하면 서버는 
  해당 토큰을 즉시 무효화하여 추가적인 보안을 강화
  

JWT(JSON Web Token)이란?

  • JSON 객체를 사용하여 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)

  • 주로 사용자 인증 및 권한 부여를 위해 사용

  • JWT는 세 부분으로 구성되며 각 부분은 점(.)으로 구분

    • 헤더(Header), 페이로드(Payload), 서명(Signature)

1. 헤더(Header)
- 두 부분으로 구성
   - 토큰 타입(JWT)
   - 해싱 알고리즘(예 : HMAC SHA256 또는 RSA)
   {
      "alg": "HS256",
      "typ": "JWT"
   }
2. 페이로드(Payload)
- 클레임(claims)이라고 하는 여러 가지 정보가 담김
   - 등록된 클레임(Registered Claims) : 토큰의 표준화된 정보
   (예 : iss, exp, sub, aud 등)
   - 공개 클레임(Public Claims) : 충돌 방지를 위해 IANA JSON Web Token Registry에 등록하거나 URI(Uniform Resource Identifier) 형식으로 정의된 클레임
   - 비공개 클레임(Private Claims) : 양 당사자 간에 정의된 클레임
   {
     "sub": "1234567890",
     "name": "John Doe",
     "admin": true
   }
3. 서명(Sinature)
- 헤더와 페이로드를 인코딩한 후 비밀 키 또는 RSA 개인 키로 서명
   - 예시(HMAC SHA256)
   HMACSHA256(
  	base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret)

JWT 공식 사이트

https://jwt.io/

JWT 토큰의 장・단점

장점단점
자기 포함: 필요한 모든 정보를 포함토큰 크기: 많은 정보를 포함하면 크기가 커짐
안전성: 서명되어 있어 변경 불가보안 취약점: 탈취 시 만료 기간 동안 악용 가능
확장성: 다양한 환경에서 사용 가능무효화 어려움: 이미 발급된 토큰을 무효화하기 어려움
상태 비저장: 서버에 상태 저장 필요 없음
JSON 기반: 쉽게 직렬화 및 역직렬화 가능

실습

토큰 기반 인증 방식을 구현할 때는 refresh 토큰과 access 토큰 두개를 기반으로 구현함

  • access 토큰의 수명은 짧게, refresh 토큰의 수명은 길게함
  • refresh 토큰은 access 토큰이 만료되었을 때 다시 access 토큰을 얻기 위해 사용되는 토큰, 이를통해 access토큰이 만료됐을 때마다 인증에 관한 비용이 줄어들게 됨
  • 로그인을 하게 되면 access 토큰과 refresh 토큰 두개를 얻음
  • access 토큰이 만료되거나, 사용자가 새로고침을 할 때 refresh 토큰을 기반으로 새로운 access token을 얻음

npm install express cookie-parser jsonwebtoken cors

SHA256 해싱 알고리즘

https://emn178.github.io/online-tools/sha256.html

// 토큰에 관한 내용은 따로 .env에 저장
const ACCESS_TOKEN_SECRET =
  "a94fdc14161c28e4fed5d168ab4c0555dec116d22bd318655a77b4e046239afd";
const REFRESH_TOKEN_SECRET =
  "a94fdc14161c28e4fed5d168ab4c0555dec116d22bd318655a77b4e046239afd";

const PORT = 12010;
//라우팅설정을 쉽게, 미들웨어 설정을 쉽게 도와주는 express 프레임워크 const express = require('express');
// 쿠키 핸들링을 쉽게 해주는 모듈
const cookieparser = require("cookie-parser");
//jwt에 관한 로직을 설정할 수 있게 하는 모듈.
const jwt = require("jsonwebtoken");
// browser - postman에서 요청을 하게 되면 CORS가 걸리는데 이를 가능하게 해주는 모듈.
// CORS의 자세한 설명은 큰돌의 터전 - CORS를 참고.
const cors = require("cors");

const app = express();
// 미들웨어를 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieparser());

// DB에 있는 유저 정보를 흉내내는 객체
const userInfo = {
  username: "leesfact",
  password: "1234",
  email: "leesfact@gmail.com",
};
//토큰을 만들 때 쓰는 유저 객체
const user = {
  username: userInfo.username,
  email: userInfo.email,
};
// 두개의 토큰에 대한 만료기한 옵션 : access토큰은 짧게. refresh토큰은 길게
const accessOpt = {
  expiresIn: "10m",
};
const refreshOpt = { expiresIn: "1d" };

// 쿠키 옵션
const cookieOpt = {
  httpOnly: true,
  sameSite: "Strict",
  secure: true,
  maxAge: 24 * 60 * 60 * 1000,
};

const isAuthenticated = (req, res, next) => {
  if (!req.headers.authorization) {
    return next("route");
  }
  let auth = req.headers.authorization;
  if (auth.startsWith("Bearer ")) {
    auth = auth.substring(7, auth.length);
  }
  const user = jwt.verify(auth, ACCESS_TOKEN_SECRET);
  //인증된 유저가 있다면 다음 미들웨어로, 그게 아니라면 다음 라우팅으로, 제어권을 넘긴다.
  if (user) return next();
  else return next("route");
};
app.get("/", isAuthenticated, function (req, res) {
  return res.status(200).send("허용된 요청입니다.");
});
app.get("/", (req, res) => {
  return res.status(401).send("허용되지 않은 요청입니다.");
});
app.post("/login", (req, res) => {
  const { username, password } = req.body;
  console.log(req.body);
  console.log(username, password);
  if (username === userInfo.username && password === userInfo.password) {
    const accessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, accessOpt);
    const refreshToken = jwt.sign(user, REFRESH_TOKEN_SECRET, refreshOpt);
    // cookie에는 refresh토큰을 담습니다.
    // 이후 요청시에 refresh토큰을 기반으로 accesstoken을 갱신받습니다.
    console.log("jwt토큰이 생성되었습니다.");
    console.log(refreshToken);
    console.log(accessToken);
    res.cookie("jwt", refreshToken, cookieOpt);
    return res.json({ accessToken, refreshToken });
  } else {
    // 만약 인증을 하지 않은 경우
    return res.status(401).json({ message: "인증되지 않은요청입니다." });
  }
});

// access토큰 요청 전에 먼저 refresh 토큰 요청을 먼저 함.
app.post("/refresh", (req, res) => {
  console.log("REFRESH요청");
  console.log(req.cookies);
  if (req.cookies.jwt) {
    const refreshToken = req.cookies.jwt;
    jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, decoded) => {
      if (err) {
        return res.status(401).json({ message: "인증되지 않은 요청입니다." });
      } else {
        console.log(decoded);
        const accessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, accessOpt);
        return res.json({ accessToken });
      }
    });
  } else {
    return res.status(401).json({ message: "인증되지 않은 요청입니다." });
  }
});
app.listen(PORT, () => {
  console.log(`서버시작 : http://localhost:${PORT}`);
  console.log(`로그인요청 : http://localhost:${PORT}/login`);
  console.log(`refresh요청 : http://localhost:${PORT}/refresh`);
});

/*
서버시작 : http://localhost:12010
로그인요청 : http://localhost:12010/login refresh요청 : http://localhost:12010/refresh {
    "username": "leesfact",
    "password": "1234",
    "email": "leesfact@gmail.com"
}
curl -X POST http://localhost:12010/refresh -H 'Content-Type:
application/json' --cookie
"jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imt1bmRv
bCIsImVtYWlsIjoia3VuZG9sQGdtYWlsLmNvbSIsImlhdCI6MTY3MjI3MzgxNSwiZX
hwIjoxNjcyMzYwMjE1fQ.rAhD3tF11APHaSoxUp1F5DtNUte-5mt8HjawHoVVbns"
*/

구현 시 주의점

  • 이렇게 Access 토큰을 얻었다면 그 이후에 요청을 할 때는 HTTP Header - Authorization 또는 HTTP Header - Cookie에 담아 요청을 하게 됨, 이ㄷ 떄 당므과 같은 규칙을 지키는 것이 좋음

✔︎ Bearer <token>으로 Bearer을 앞에 둬서 토큰 기반 인증 방식이라는 것을 알려야 함
✔︎ HTTPS를 사용해야함 (예제는 HTTP로 진행)
✔︎ 쿠키에 저장한다면 sameSite:'Strict'으로
✔︎ 수명이 짧은 AccessToken을 발급해야함
✔︎ url에 토큰을 전달하면 안됨

포스트맨 - API 테스트 플랫폼

https://web.postman.co/

로그인이 안된 상태, 요청 보낼 경우

POST 요청, http://localhost:12010/login

accessToken 복사 후, authorization key의 value에 붙여넣기

refresh 토큰으로 access 토큰 발급 받기

  • 로그인 시, 발급받은 refresh Token으로 cmd curl 명령어로 accessToken 재발급

curl -X POST http://localhost:12010/refresh -H 'Content-Type: application/json' --cookie "jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxlZXNmYWN0IiwiZW1haWwiOiJsZWVzZmFjdEBnbWFpbC5jb20iLCJpYXQiOjE3MTkyMjYwNjgsImV4cCI6MTcxOTMxMjQ2OH0.sju7nbdc0csswiN_1NZ34j50olwpQorthXs7sKbwSDg"

post 요청(refresh 토큰 요청), http://localhost:12010/refresh

번외

MSA 아키텍처 기반(Microservices Architecture)

  • MSA는 단일 애플리케이션을 여러 개의 작은 서비스로 분리하여 개발하고 운영하는 소프트웨어 아키텍처 스타일
  • 각 서비스는 독립적으로 배포 가능하며 특정 기능이나 비즈니스 로직을 수행함

특징

  1. 독립적 배포 : 각 서비스는 독립적으로 배포, 확장, 업데이트가 가능
  2. 자율적 팀 : 각 서비스는 자율적인 개발 팀에 의해 관리되며 팀들은 서로 독립적으로 작업할 수 있음
  3. 작은 코드베이스 : 각 서비스는 상대적으로 작은 코드베이스를 가지며 유지보수가 용이함
  4. 다양한 기술 스택 : 각 서비스는 필요한 기술 스택을 독립적으로 선택하여 사용할 수 있음
  5. 폴리글랏(Polyglot) 프로그래밍 : 서로 다른 프로그래밍 언어와 데이터베이스를 사용할 수 있음

장・단점

장점단점
확장성: 각 서비스가 독립적으로 확장 가능복잡성 증가: 서비스 간 통신과 데이터 일관성 유지 필요
유연성: 새로운 기술 도입 용이관리 오버헤드: 여러 서비스의 배포, 모니터링, 관리 필요
신뢰성: 한 서비스의 장애가 다른 서비스에 미치는 영향 적음데이터 관리: 데이터 일관성과 트랜잭션 관리 어려움
빠른 배포: 부분적인 변경이 전체 시스템에 영향 없음테스트 복잡성: 통합 테스트와 서비스 간 상호작용 테스트 복잡
독립적 개발: 자율적 팀이 독립적으로 작업 가능성능 문제: 서비스 간 통신 오버헤드 발생 가능

모노리식 아키텍처 기반(Monolithic Architecture)

  • 모든 기능이 하나의 애플리케이션 내에서 통합되어 있는 소프트웨어 아키텍처 스타일
  • 애플리케이션은 하나의 큰 코드베이스로 구성되어 있으며 모든 기능과 로직이 이 안에 포함됨

특징

  1. 단일 코드베이스 : 애플리케이션의 모든 기능이 하나의 코드베이스에 포함됨
  2. 통합 배포 : 전체 애플리케이션이 한 번에 빌드되고 배포됨
  3. 통합 데이터 관리 : 모든 기능이 동일한 데이터베이스를 공유함

장・단점

장점단점
확장성: 각 서비스가 독립적으로 확장 가능복잡성 증가: 서비스 간 통신과 데이터 일관성 유지 필요
유연성: 새로운 기술 도입 용이관리 오버헤드: 여러 서비스의 배포, 모니터링, 관리 필요
신뢰성: 한 서비스의 장애가 다른 서비스에 미치는 영향 적음데이터 관리: 데이터 일관성과 트랜잭션 관리 어려움
빠른 배포: 부분적인 변경이 전체 시스템에 영향 없음테스트 복잡성: 통합 테스트와 서비스 간 상호작용 테스트 복잡
독립적 개발: 자율적 팀이 독립적으로 작업 가능성능 문제: 서비스 간 통신 오버헤드 발생 가능
profile
HW + SW = 1

0개의 댓글