[TIL] Token, 과제

ansmeer008·2022년 11월 11일
1

Today I learn

목록 보기
52/65
post-thumbnail

해싱

해시 함수(Hash Function)을 이용해 암호화를 진행하는 방식.

다른 암화화 방식들과 다르게 해싱은 암호화만 가능하다.

해시 함수의 특징 : 항상 같은 길이 문자열 리턴, 서로 다른 문자열에 동일 해시 함수 사용시 반드시 다른 결과값, 동일 문자열에 동일 해시 함수 사용시 항상 같은 결과값



레인보우 테이블 & 솔트

  • 레인보우 테이블 : 항상 같은 결과값이 나온단 특성을 이용해 해시 함수를 거치기 이전 값을 알아낼 수 있도록 기록해둔 표

  • 솔트 : 레인보우 테이블의 값이 유출되었을 때 보안상 위협이 될 수 있으므로 해싱 이전 값에 임의의 값을 더해 데이터가 유출되더라도 해싱 이전 값 알아내기 어렵게 만드는 방법



해싱의 목적

데이터 그 자체를 사용한다기 보다 동일한 값 데이터만 사용하고 있는지 여부를 확인하는 것이 목적

민감한 데이터 다루어야 하는 상황에서 데이터 유출 위험성 줄이면서 데이터의 유효성 검증하기 위해 사용되는 ‘단방향 암호화 방식’

Ex: 해싱한 패스워드 값이 일치한다면 정확한 비밀번호를 입력했다는 뜻이므로 해싱값으로만 로그인 요청을 처리할 수 있다.



Token-based Authentication 토큰 기반 인증

what is Token ?

클라이언트에서 인증 정보를 보관하는 방법으로 클라이언트가 토큰을 가지면 서버에 요청을 하고 응답을 받을 수 있다.

민감한 정보는 클라이언트에 담으면 안 되지만, 토큰이 유저 정보를 암호화하기 때문에 가능.

대표적인 토큰 기반 인증 방법 중 하나가 JWT(JSON Web Token)


JWT의 두가지 종류 토큰

  • 액세스 토큰 (Access Token) : 보호된 유저들에 접근할 수 있는 권한 부여에 사용 클라이언트가 처음 인증 받게 될 때 (ex: 로그인시) 엑세스, 리프레시 토큰 모두 부여받지만 실제로 권한 얻을 때 사용하는 토큰은 엑세스 토큰
  • 리프레시 토큰 (Refresh Token) : 엑세스 토큰 유효기간이 만료되면 리프레시 토큰 이용해 새로운 엑세스 토큰 발급 받을 수 있음. (리프레시 토큰이 탈취될 때를 생각해 리프레시 토큰을 사용하지 않는 곳도 많다고 함)


JWT 구조

  • Header : 어떤 종류의 토큰인지, 어떤 알고리즘으로 시그니처를 암호화(sign)할 지 적혀있음.

  • Payload : 서버에서 활용할 수 있는 유저 정보가 담김. 너무 민감한 정보는 담지 않는 게 좋다.

  • Signature : base64로 인코딩된 헤더, 페이로드가 완성되면 시그니처는 이를 서버의 비밀 키(암호화에 추가할 salt)와 헤더에서 지정한 알고리즘 이용해 해싱함.

//Header 
{
	"alg" : "HS256",
	"typ" : "JWT"
}//이 json 객체를 base64 방식으로 인코딩하면 JWT의 첫번째 부분인 헤더가 완성 


//Body
{
	"sub" : "someInformation",
	"name" : "phillip",
	"iat" : 151623391
}//이 json 객체를 base64 방식으로 인코딩하면 JWT의 payload 완성 


//signature
//HMAC SHA256 알고리즘 사용시 시그니처 생성
HMAXSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);


토큰 기반 인증 절차

  1. 클라이언트가 서버에 아이디, 비밀번호 담아 로그인 요청

  2. 아이디, 비밀번호 일치 확인후 클라에게 보낼 암호화 토큰 생성
    (엑세스, 리프레시 토큰 모두 생성, 두 종류 토큰이 같은 정보 담을 필요는 없음)

  3. 서버가 토큰 클라에게 보내면 클라가 토큰 저장
    (저장 위치는 로컬 스토리지, 세션 스토리지, 쿠키 등 다양함.)
    (쿠키에는 리프레시 토큰, 헤더 또는 바디에 액세스 토큰 담는 등 다양한 방법으로 구현 가능)

  4. 서버가 토큰 해독해 발급해준 토큰이 맞다는 판단할 경우 클라 요청 처리한 후 응답 보냄



토큰 기반 인증 장점

  • Stateless & Scalability (무상태성과 확장성) 서버는 클라에 대한 정보 저장할 필요 없이 토큰이 해독 되는지만 판단 클라는 새 요청 보낼 때마다 토큰을 헤더에 포함시키면 됨 서버 여러개인 경우 더 장점이 있는데, 같은 토큰으로 여러 서버에서 인증 가능하기 때문 (세선 방식이라면 모든 서버가 해당 유저 정보 공유하고 있어야 함)
  • 안전성 암호화 한 토큰 사용, 암호화 키 노출 필요 없음
  • 어디서나 생성 가능 토큰 확인 서버가 토큰 만들지 않아도 됨 토큰용 서버 만들거나, 다른 회사에 토큰 관련 작업 맡길 수 있음
  • 권한 부여에 용이 토큰의 페이로드 안에 어떤 정보에 접근 가능한지 지정 가능 (ex: 서비스의 사진과 연락처 사용 권한만 부여)


과제 : JWT를 이용하여 로그인 기능 구현


🤔 환경 변수를 설정하라는데... 환경 변수가 뭔데?

: 환경 변수 자체는 '프로세스가 컴퓨터에서 동작하는 방식에 영향을 미치는, 동적인 값들의 모임'(wiki)이라고 하는데...

환경 변수들은 프로세스를 동작시킬 때 필요한 어떤 참조 값들이다.
어떤 복잡한 경로를 거쳐야 참조할 수 있는 응용프로그램을 쉽게 꺼내쓰기 위해서 미리 변수로 등록해 놓는 것 정도로 이해할 수 있겠다.

위 개념 공부할 때 배웠듯이 JWT 토큰을 인증하기위해서는 비밀 키가 필요하기 때문에 .env 파일에 비밀 키를 정의하는 것이 필요하다.

지금은 연습하는 용도로 사용해서 아래처럼 마음대로 아무 문자나 넣었지만, 실제로는 라이브러리 등을 사용해서 무작위로 생성해서 사용하는 듯.

//access token과 refresh token의 secret key를 저장하는 환경 변수 파일 

ACCESS_SECRET= MyAccessKey1$0$0$4
REFRESH_SECRET= MyRefreshKey1$0$0$4

위처럼 만들어준 환경변수를 사용하고 싶은 곳에서

require("dotenv").config();

위 코드를 통해 dotenv를 부를 수 있고

process.env.ACCESS_SECRET
process.env.REFRESH_SECRET

위와 같은 형식으로 불러와 사용한다


🤪 userInfo.js를 어느정도 작성했는데도 로그인 기능이 제대로 작동하지 않았다.

: 알고보니 async 함수들에 맞춰 await를 해주지 않았기 때문에 애초에 쿠키가 제대로 만들어지지 않고 있었다.

(아래 이미지가 실제 과제할 때 콘솔창은 아니지만, 저런 경로로 들어가면 쿠키가 잘 들어갔는지 확인 할 수 있다.)

과제 내부에서 사용하는 함수 중에 generateToken이나 verifyToken 같은 토큰 관련 함수들은 jsonwebtoken 라이브러리에서 sign, verify라는 매서드를 가져와 내부에서 사용한다.

//token 생성, 검증하는 함수가 들어있는 js파일 

require("dotenv").config();
const { sign, verify } = require("jsonwebtoken");

//jwt.sign()은 Jwt 토큰을 생성하는 매소드로 비동기적으로 호출되었을 때 에러와 스트링화 된 jwt 토큰을 리턴함
//동기적으로 호출되었을 때 jwt 토큰만 리턴
//jwt.verify()는 토큰을 검증하는 매서드 (우리 서버가 만든 토큰이 맞는지 여부와 만료된 건 아닌지 검증)

module.exports = {
  generateToken: async (user, checkedKeepLogin) => {
    const payload = {
      id: user.id,
      email: user.email,
    };
    let result = {
      accessToken: sign(payload, process.env.ACCESS_SECRET, {
        expiresIn: "1d", // 1일간 유효한 토큰을 발행합니다.
      }),
    };

    //이 아래는 로그인 정보 저장 부분을 체크한 경우에 분기 됨
    if (checkedKeepLogin) {
      result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
        expiresIn: "7d", // 일주일간 유효한 토큰을 발행합니다.
      });
    }
    return result;
  },
  verifyToken: async (type, token) => {
    let secretKey, decoded;
    switch (type) {
      case "access":
        secretKey = process.env.ACCESS_SECRET;
        break;
      case "refresh":
        secretKey = process.env.REFRESH_SECRET;
        break;
      default:
        return null;
    }

    try {
      decoded = await verify(token, secretKey);
    } catch (err) {
      console.log(`JWT Error: ${err.message}`);
      return null;
    }
    return decoded;
  },
};

위 매서드들은 사실 동기로 작성할 수도 있는데,
비동기로 호출 하는 경우는

  1. sign, verify 등 jwt 라이브러리 매서드의 전달인자로 콜백 함수가 추가로 들어간 경우 (콜백 함수는 토큰 발급 중 에러가 발생할 것을 대비해 에러 처리를 해주는 함수)

  2. 토큰 발급이 오래 걸려 정상적 처리가 어려울 것으로 예상되는 경우

와 같이 두 가지 정도의 경우를 실시간 세션에서 설명해주셨다.

위 내용을 참고해 login 파일과 userInfo 파일 내부에 해당 함수들 앞에 await를 붙여주었더니 잘 해결되었다.

콘솔창이나 콘솔창 내부의 application 속 쿠키 부분을 참고할 줄 알았다면 쿠키에 아무것도 생성되지 않았다는 걸 발견하고 좀 더 시간을 단축해서 해결할 수 있었을 것이다. 문제가 생기면 어느부분이 문제인지 차근차근 찾아나가는 연습은 아무리 해도 모자라지 않은듯.



profile
예술적인 코드를 짜는 프론트 엔드 개발자가 꿈입니다! (나는야 코드 아티스트! 🤭)

0개의 댓글