본문 바로가기
프로그래밍/database

[220302] JWT란? JWT 만드는법.

by 한코코 2022. 3. 2.

JWT(Jason Web Token)란?

  • 1) 토큰이란?
    • 1-1) 일련의 문자열을 구분할 수 있는 단위이자, 시스템에서 보안 객체의 접근 관리에 사용되는 객체 또는 장치다.
    • 1-2) 토큰은 크게 접근(access) 토큰, 보안(security) 토큰, 세션(session) 토큰 등으로 분류할 수 있다.
    • 1-3) 접근 토큰(access token)이 가장 많이 사용되는 토큰 형식으로 시스템이나 소프트웨어에서 어떤 특정한 기능이나 데이터에 접근하는 대상에게 권한을 부여하는 데 사용된다. 
  • 2) JWT이란?
    • 사용하고 싶은 정보를 객체에 담아서 해시로 들고, 그 해시값도 같은 객체에 담은 것.
    • 즉, 필요한 정보를 자체적으로 지니고 있어서 자가수용적인 특성을 가졌다.
    • 자가수용적이므로 두 개체에서 전달되기 쉽다. http 헤더에 넣거나 ,url의 파라미터로 전달이 가능하다.
    • jwt에서 발급된 토큰은 토큰에 대한 기본정보, 전달할 정보, 토큰이 검정되었다는것을 증명할 서명을 포함하고있다.
  • 3) jwt의 사용
    • 회원인증 
      • 유저가 로그인을 하면 서버가 유저의 정보에 기반한 토큰을 발급해 유저에게 전달한다.
      • 유저는 서버에 요청을 할때마다 jwt를 포함해 전달한다.
      • 서버는 요청을 받을때마다 토큰이 유효하고 인증되었는지 검증 후, 유저가 요청한 작업에 권한이 있는지 확인하고 작업을 처리한다.
      • 서버측에서 유저의 세션을 유지할 필요가 없는 장점. 새션관리 필요없음.
      • 유저가 요청했을때 토큰만 확인하면 됨. 자원 아낄 수 있음.
    • 정보 교류
      • 정보가 서명이 되어있어서 보낸이가 바뀌지는 않았는지, 정보 조작유무를 검증할 수 있따.

 

JWT 형태

1) header : 암호화할때 사용할 알고리즘, 사용할 타입

2) payload : 쿠키에 저장할 내용이기 때문에 민감한 정보는 저장하지 않는다. 예) 아이디나 이름 정도

3) signature : (header+payload)를 해시로 만든 값

 


 

JWT  만드는 법

1.   header와 payload를 먼저 만든다

header : 서명을 위해 어느 알고리즘을 선택할지, 어느 타입을 취할지 결정한다.

payload : 토큰의 목적에 따라 정보를 저장한다.

const header = {
    alg : 'sha256', //너 알고리즘이 뭐야
    tpy : 'JWT' //너 타입이 뭐야
}

const payload = {
    userid:'web7722',
    name:'ingoo'
}

 

 

 

2.   header와 payload를 인코딩한다.

2-1)   header가 인코딩되는 과정 : header를 쿠키에 넣기 위해 string 형태로 바꾸는 것

const header = {alg:'sha256', tpy:'JMT'}
const encodingHeader = JSON.stringify(header)
const encodingHeader1 = Buffer.from(JSON.stringify(header)).toString('base64')

console.log(header)
console.log(encodingHeader)//객체를 string으로 만듬
console.log(encodingHeader1)//길이를 줄이기 위해 64bit로 만듬

//출력값
//{ alg:'sha256', tpy: 'JWT' }
//{ "alg":"sha256", "tpy":"JWT"}
//eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ==

 

2-2)   payload가 인코딩되는 과정

2-2-1) header 객채 > string > buffer 16진수 변환시킴 > 너무 길어서 64진수 변환시킴

const encodingPayload = Buffer.from(JSON.stringify(payload)).toString('base64')
console.log(encodingHeader,encodingPayload)
//eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ== //헤더
//eyJ1c2VyaWQiOiJ3ZWI3NzIyIiwibmFtZSI6ImluZ29vIn0= //페이로드
2-2-2) 64진수로 바꾼 값에서 비어있는 비트값을 = 으로 대체함 > 그 =을 지우는 코드 추가
const encodingHeader = Buffer.from(JSON.stringify(header))
.toString('base64')
.replace(/[=]/g,'') 
//결과 eyJhbGciOiJzaGEyNTYiLCJ0cHkiOiJKV1QifQ

const encodingPayload = Buffer.from(JSON.stringify(payload))
.toString('base64')
.replace(/[=]/g,'')
console.log(encodingHeader,encodingPayload)
//결과 eyJ1c2VyaWQiOiJ3ZWI3NzIyIiwibmFtZSI6ImluZ29vIn0

2-3) 알아두면 좋은 디코딩하는 법

const decodingHeader = Buffer.from(encodingHeader,'base64').toString()
console.log(decodingHeader)
console.log(JSON.parse(decodingHeader))//객체화

//결과
//{"alg":"sha256","tpy":"JWT"} //텍스트
//{ alg: 'sha256', tpy: 'JWT' } //객체화

 

3.   signature 생성

signature : 토큰을 안전하게 확인하는 과정. base64을 통해 헤더와 페이로드를 인코딩해 구분자 .을 이용해 연결시켜 계산한다.

3-1) header와 payload의 해시값을 합쳐서 signature 해시값 생성

const salt = 'web7722'
const signature = crypto.createHmac('sha256',Buffer.from(salt)) //알고리즘,16진수값
.update(`${encodingHeader},${encodingPayload}`) //내용넣기
.digest('base64') //16비트보다 짧게 쓰기 위해 64비트로 반환
.replace(/[=]/g,'') //=제거
console.log('signature : ',signature)

//결과
//signature :  UihUkDogPglZlf1ENGM73TnouXgs3JzRvHi2Wh4xhy8

 

3-2) 만든 signature의 해시값을( = const jwt) 쿠키에 넣음.

jwt를 몰랐을때는 로그인 여부를 확인하려면 세션 생성 여부를 체크해야했다.

그리고 쿠키값이 유효하냐, 유효하지 않냐-를 묻는 미들웨어를 줘야했다.

쿠키는 누구나 임의로 만들 수 있으니까.

jwt는 쿠키값이 토큰으로 바뀐것뿐. 토큰이 정확하냐 안하냐-를 검증한다.

const jwt = `${encodingHeader}.${encodingPayload}.${signature}`//해시값
const cookie={ token : jwt } //쿠키에 넣기
const jwt_arr = cookie.token.split('.') //.을 제외하고 배열로 반환한다
const jwt_arr의 출력값

 

해시값을 잘 만들었는지 확인하려면 JWT 홈페이지(https://jwt.io/ )에 가서 만든 해시값을 넣어서 체크가 가능하다.

 

 

3-3) 쿠키값에 잘 들어갔나 확인

const cookie={ token : jwt } //쿠키에 넣기
const [head,pay,sign] = cookie.token.split('.') //구분하기

const designature = crypto.createHmac('sha256',Buffer.from('web7722'))
.update(`${head},${pay}`)
.digest('base64')
.replace(/[=]/g,'')

console.log(designature === sign)

//결과 true

 

 

3-4) 쿠키로 가져온 header, payload, signature가 정확한건지 확인하기

header, payload의 해시값을 새로 만들어서 쿠키로 가져온 header, payload 해시값과 비교한다.

const [head,pay,sign] = cookie.token.split('.')
const decodingHeader = Buffer.from(head,'base64').toString()
console.log(header, decodingHeader)
//결과
//암호화되어있는 header의 해시값
//{"alg":"sha256","tpy":"JWT"} 텍스트 형태로 되어있는 JSON
//JSON.parse()를 썼으면 객체형태로 나왔을 것


const decodingPayload = JSON.parse(Buffer.from(pay,'base64')).toString()
console.log(payload,decodingPayload)
//결과
//암호화되어있는 payload의 해시값
//{ userid:'web7722', name:'ingoo'}


const designature = crypto.createHmac('sha256',Buffer.from('web7722'))
.update(`${head},${pay}`)
.digest('base64')
.replace(/[=]/g,'')
console.log(designature===sign)
//결과
//true

 


 

단순히 쿠키/세션을 가진것보다 보안성이 높은이유

signature가 헤더와 페이를 합친 후 자신만의 규칙(salt)를 적용해 만들어지는 해시값을 가지고 있기 때문이다.

헤더나 페이의 내용이 조금만 바뀌어도 해시값이 완전히 달라지게 하는 salt의 특성덕분에

클라이언트가 쿠키 내용을 바꿔서 접속할 수가 없다.

즉, salt값을 모르면 수정할 수 없다.

서버가 바뀐 쿠키의 내용과 기존에 갖고있던 해시값을 각각 해시화 해서 비교해 다르면 거부하기 때문이다.

 

 

이 방식의 장점

  • 1) 데이터의 주체가 클라이언트기 때문에 사용자가 로그인을 많이해도 서버에게는 부담이 없다.
  • (서버가 터질 일이 없다.)
  • 2) 클라이언트가 이미 갖고있어서 db.query를 날리지 않아도 된다.
  • 3) 사용자가 정보를 가지고 있기때문에 서버가 달라도 로그인이 유지될 수 있다. 

 

알고리즘의 취약성

보안 상담가 팀 메클레인(Tim McLean)은 alg 필드를 사용하여 토큰의 유효성을 잘못 확인하는 일부 JWT 라이브러리의 취약성을 보고하였다. 이 취약점들이 패치가 되었으나 메클레인은 비슷한 구현체 혼동을 예방하기 위해 alg 필드를 구식 처리하는 것을 제안하였다.

 

취약성 보완법

  1. JWT 헤더만으로 유효성을 확인하지 않을 것
  2. 알고리즘을 인지할 것
  3. 적절한 키 크기를 사용할 것

댓글