토큰 학습하기

arrrrrr·2023년 3월 8일

토큰의 등장 배경

세션 기반 인증은 서버에서 유저의 상태를 관리하기 때문에 서버 부하나 메모리 부족의 문제가 있다. 토큰 기반 인증은 이러한 세션 기반 인증의 한계를 극복하기 위해 고안 되었다.

(토큰 학습 전에) 해싱 먼저 알아보기

해싱이란 ?

  • 복호화가 불가능하고 암호화만 가능한 방식이다.
  • 해싱에는 해시함수를 이용한다. 즉 해시함수의 특징을 모두 가진다.

해시함수의 특징은 ?

  • 항상 같은 길이의 문자열을 리턴한다.
  • 서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나온다.
  • 동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나온다.
    여기에서 보안의 문제가 발생한다.

레인보우 테이블과 솔트 알아보기

  • 레인보우 테이블
    동일한 문자열에 대해 항상 같은 결과 값이 나온다는 특성을 이용해, 해시 이전의 값을 탈취할 수 있는 레인보우 테이블이 존재한다. 즉 이는 보안상의 위협이다.
  • 솔트
    이 문제를 해결하기 위해 고안된 방법이다. 말 그대로 속므을 치듯 해싱 이전의 값에 임의 값을 더해 해싱값이 유출되더라도 이전의 원본 문자열은 알아내기 어렵게 만드는 방법이다.
    → (문자열 + 솔트)를 해싱한다.

해싱의 목적

해싱의 목적은 데이터 그 자체를 사용하기 위해 원본 문자열을 알아내는데 있는 것이 아니라, 동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이다. 따라서 복호화가 불가능한 것이 오히려 해싱의 이점이 된다.

  • 해싱방식에서는 사이트 관리자가 사용자의 비밀번호를 알 필요가 없다. 그래서 비밀번호를 DB에 저장하지 않고 해싱한 값을 저장한다.
  • 비밀번호 일치여부는 해싱한 값을 비교한다. (문자열 + 솔트)를 해싱한 결과가 같다면 정확한 비밀번호를 입력했다는 뜻이 된다.

정리하기

  • 해싱은 민감한 데이터를 다룰때 정보 유출의 위험성을 낮출 수 있는 방식이다.
  • 데이터 유효성을 검증하는데 단방향 암호화 방식만을 사용한다.

토큰 학습하기

토큰 개념

  • 토큰은 세션 기반 인증의 한계를 극복하고자 고안된 방법이다.
    • 세션 : 서버에서 유저 상태 관리 → 서버의 부담 높음
    • 토큰 : 클라이언트에 유저 상태 관리 → 서버의 부하, 메모리 부담을 낮춤
  • 토큰은 일종의 증표이다. 웹 보안에서의 토큰은 인증과 권한 정보를 담고 있는 암호화된 문자열이다.

토큰 인증방식의 흐름

장점

  • 무상태성 → 서버 증설에 용이하다.
  • 확장성 → 서버 확장에 유연함을 의미한다.
  • 어디서나 토큰 생성이 가능하다.
  • 권한 부여에 용이 → 사용자 권한 부여에 용이 : 어드민 접근 권한 설정등이 용이하다.

JWT (JSON Web Token)

  • 토큰 기반 인증 구현 시 대표적으로 사용되는 기술이다.
  • JSON 객체에 정보를 담고 이를 토큰으로 암호화하여 전송한다.
  • . 으로 나누어진 세 부분으로 구성되어 있다. → Header, Payload, Signature

토큰 자체를 설명하는 데이터가 담겨있다.

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

전달하려는 내용물을 담고있는 부분

{
  "sub": "someInformation",
  "name": "phillip",
  "iat": 151623391
}

Signature

Header + Payload + Secret key로 토큰의 무결성을 유지한다. 누군가 권한을 속이기 위해 payload를 변조해도 Secret을 정확하게 알지 못한다면 해당 요청은 올바르지 않은 토큰임을 알아낼 수 있다.

// HMAC SHA256 알고리즘을 사용한 경우 예시
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

한계 알아보기

  • 토큰 자체가 탈취되는 경우에는 한계가 존재한다.
    • 무상태성 → 강제 만료 불가능
    • 유효기간 설정의 문제 → 탈취 상황 vs. 유저 경험 사이의 조율 필요
    • 토큰의 크기가 불필요하게 크다.

한계 극복 방법

Access Token과 Refresh Token 함께 사용한다.

  • Access Token : 접근을 위한 토큰 → 유효기간이 짧다.
  • Refresh Token : 엑세스 토큰이 만료되었을때 새로운 액세스 토큰을 발급받기 위해 사용하는 토큰이다. → 유효기간 상대적으로 길다.
  • 즉, 액세스 토큰이 만료되어도 리프레쉬 토큰 유효기간이 남아있다면 사용자는 재로그인 필요 없이 인증상태 유지가 가능하다.

토큰 실습

어려웠던 부분

  • 토큰 검증 결과가 없을 때 연속되는 처리 단계를 생각하는 것이 어려웠다.
    → 토큰 인증방식 흐름에 대한 개념이 아직 제대로 잡히지 않아서 그런 것 같다.
module.exports = (req, res) => {
    // 브라우저에서 쿠키를 가져온다.
    // 쿠키에 토큰을 담았기 때문에 쿠키에서 토큰을 가져온다. 
    const { access_jwt, refresh_jwt } = req.cookies;
    // 액세스 토큰이 있다면 유저 정보를 DB에서 조회하여 응답해준다.
    if (access_jwt) {
        // 유효한 키인지 확인하는 과정
        const accessTokenPayload = verifyToken("access", access_jwt);
        // 액세스 토큰이 유효하다면,
        if (accessTokenPayload) {
            // 유저정보 찾고
            const exUser = {
                ...USER_DATA.find((user) => user.id === accessTokenPayload.id),
            };
            // 유저 정보 반환해준다. 
            delete exUser.password;
            return res.send(exUser);
        }
        // -------- refresh-------- //
        // 액세스 토큰이 없다면 리프레시 토큰을 확인해야한다.
       	// 즉 바로 404(Not found) 에러를 주면 안된다.
        if (refresh_jwt) {
            const refreshTokenPayload = verifyToken("refresh", refresh_jwt);
            // 받아온 리프레스 토큰을 검증한다. 브라우저에서 넘어온 정보가 유효한가?
            // 리프레시 토큰 검증이 실패했다면 에러를 준다. 
            if (!refreshTokenPayload)
                return res.status(401).send("Not Authorized");
            // 토큰 검증에 성공했다면 유저를 찾는다.  
            const exUser = {
                ...USER_DATA.find((user) => user.id === refreshTokenPayload.id),
            };
            // 유저를 찾지 못했다면 에러를 준다. 
            if (!refreshTokenPayload)
                return res.status(404).send("Not Authorized");

            // 유저를 찾은 경우에는 엑세스 토큰을 갱신해준다. 
            const cookieOption = {
                domain: "localhost",
                path: "/",
                httpOnly: true,
                sameSite: "none",
                secure: true,
            };
            const { accessToken } = generateToken(exUser, false);
            res.cookie("access_jwt", accessToken, cookieOption);
        }
        // 위 조건에 안걸리는 마지막의 경우는
      	// Access Token과 Refresh Token 모두 만료된 경우를 의미한다.
        return res.status(401).send("Not Authorized");
    }

0개의 댓글