JWT - 기본 개념과 사용법

choko's avatar
Jun 29, 2024
JWT - 기본 개념과 사용법
 
 

JWT 구성 요소

 
  • 헤더 - 사용된 토큰 유형 및 서명 알고리즘.
    • 토큰 유형은 "JWT"일 수 있고, 서명 알고리즘은 HMAC 또는 SHA256 등등 일 수 있음.
  • 페이로드 - 클레임이 포함된 토큰의 두 번째 부분.
    • 애플리케이션별 데이터(예: 사용자 ID, 사용자 이름), 토큰 만료 시간(exp), 발급자(iss), 제목(sub) 등이 포함됨.
  • 서명 - 인코딩된 헤더, 인코딩된 페이로드 및 사용자가 제공한 비밀이 서명을 작성하는 데 사용
 
JWT를 HttpOnly 쿠키에 저장하는 것을 적극 권장한다. → 그렇지 않으면 보안 취약점이 있을 수 있음.
 
 
notion image
 

Header

 
notion image
  • Header의 alg와 typ는 각각 정보를 암호화할 해싱 알고리즘 및 토큰의 타입을 지정한다.
 

Payload

notion image
  • Payload는 토큰에 담을 정보를 지니고 있다. 주로 클라이언트의 고유 ID 값 및 유효 기간 등이 포함되는 영역이다.
  • key-value 형식으로 이루어진 한 쌍의 정보를 Claim이라고 한다.
 

Signature

notion image
  • Signature는 인코딩된 Header와 Payload를 더한 뒤 비밀키로 해싱하여 생성된다.
  • Header와 Payload는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없다. 따라서 Signature는 토큰의 위변조 여부를 확인하는데 사용된다.
 
 

JWT의 인증 과정

  1. 클라이언트 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유 ID 등의 정보를 Payload에 담음.
  1. 암호화할 비밀키를 사용해 Access Token(JWT)을 발급.
  1. 클라이언트는 전달받은 토큰을 저장해두고, 서버에 요청할 때 마다 토큰을 요청 헤더 Authorization에 포함시켜 함께 전달.
  1. 서버는 토큰의 Signature를 비밀키로 복호화한 다음, 위변조 여부 및 유효 기간 등을 확인.
  1. 유효한 토큰이라면 요청에 응답.
 
 

JWT 토큰 타입

  1. Access Toeken
    1. 인증이 필요한 요청에는 access token을 사용한다.
    2. access token은 15분 정도로 짧은 수명을 가질 것을 권장한다(탈취되더라도, 심각한 공격을 방지할 수 있음).
  1. Refresh Token
    1. 새로운 access token을 발급하는데 사용된다.
    2. 일반적으로 7일 정도로, access token보다 긴 인증시간을 갖는다.
    3.  
       
 
 

구현 및 실습

 
사용 라이브러리
type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token Claims Claims // The second segment of the token Signature string // The third segment of the token. Valid bool // Populated when you Parse/Verify a token }
 
  • ACCESS_SECRET & REFRESH_SECRET KEY는 .env 파일에서 불러와 사용하도록 한다.
 
 
  1. Login
A. Post로 admin, password를 입력받는다.
  • ID → s.mariaDBHandler.GetUser(id)
  • PW → utils.PasswordJsonUnmashal([]byte(user.Password)), utils.CompareHashAndPassword([]byte(passwordJson.Sum), []byte(password))
    • 현재 mysql에 user의 id / pw가 저장되어 있음, pw의 경우 Encryption 되어 db에 저장됨.
    • 따라서 비밀번호 비교 시 PasswordJsonUnmashal, CompareHashAndPassword 를 사용해야 비교할 수 있음.
B. 토큰 생성
  • TokenDetails 생성
    • Access Token과 Refresh Token에 UUDI 사용 → AccessUuid, RefreshUuid
      • uuid를 사용해야, 사용자는 두 가지 이상의 기기에서 개별적으로 로그인/로그아웃 할 수 있음.
type TokenDetails struct { AccessToken string RefreshToken string AccessUuid string RefreshUuid string AtExpires int64 RtExpires int64 }
  • 토큰 생성 sdk
    • func NewWithClaims 사용자 지정으로 토큰을 생성함
    • func (*Token) SignedString 토큰에 서명
      • func (t *Token) SignedString(key interface{}) (string, error)
      • 토큰을 서명 후 jwt 토큰을 string으로 리턴
Code
func CreateToken(userid uint64) (*TokenDetails, error) { td := &TokenDetails{} td.AtExpires = time.Now().Add(time.Minute * 15).Unix() td.AccessUuid = uuid.NewV4().String() td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix() td.RefreshUuid = uuid.NewV4().String() var err error //Creating Access Token os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") atClaims := jwt.MapClaims{} atClaims["authorized"] = true atClaims["access_uuid"] = td.AccessUuid atClaims["user_id"] = userid atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix() at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET"))) if err != nil { return nil, err } os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file rtClaims := jwt.MapClaims{} rtClaims["refresh_uuid"] = td.RefreshUuid rtClaims["user_id"] = userid rtClaims["exp"] = td.RtExpires rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims) td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET"))) if err != nil { return nil, err } return td, nil } func CreateAuth(userid uint64, td *TokenDetails) error { at := time.Unix(td.AtExpires, 0) //converting Unix to UTC rt := time.Unix(td.RtExpires, 0) now := time.Now() errAccess := client.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err() if errAccess != nil { return errAccess } errRefresh := client.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err() if errRefresh != nil { return errRefresh } return nil }
 
 
C. redis에 토큰 메타데이터 저장
  • redis에 해당 user의 access token과 refresh token의 uuid를 key로, ID를 value로 각각 저장 → client.Set()
  • 해당 key들은 token의 만료시간이 되면 자동으로 삭제됨.
 
 
  1. token validator
  • 인증이 필요한 요청의 경우, jwt 토큰을 사용하여 인증을 진행하여야 한다.
  • 토큰의 메타데이터 추출
    • 토큰의 http header을 추출 → http.Request.Header.Get("Authorization”)하여 파싱
  • 토큰 검증 → 추출한 헤더 string에서 jwt를 파싱한다
    • 가져온 토큰에서 signing method을 검증(jwt.SigningMethodHMAC)한다.
  • 검증이 통과되면, token의 Claims를 검사한다. → access_uuid & user ID가 맞아야 한다.
 
 
  1. logout
    1. A. 토큰 검사
      • 토큰의 http header을 추출 → token validator 할 때와 동일하게 진행한다.
      • 가져온 토큰에서 signing method 검증(jwt.SigningMethodHMAC)
      • 검증이 통과되면, token의 Claims를 검사한다. → access_uuid & user ID가 맞아야 한다.
      • 검사에 통과하면, redis client의 해당 key를 제거한다. → client.Del(givenUuid)
       
      B. redis로 사용자 검증
      • AccessUuid로 redis의 key값을 불러온다.
      • 불러온 key값의 ID와, 저장된 db(mysql?)의 ID 값을 비교한다.
        • ID가 맞을 경우, 인증에 통과된다.
        • ID가 맞지 않을 경우, 인증 실패 메세지와 함께 요청을 거부한다.
 
 
  1. token validator(middleware)
    1. 토큰의 검증 → 마찬가지로 토큰을 추출하여 (ExtractToken) jwt의 SigningMethodHMAC 값들을 변경한다.
    2. 현재 session validation이 적용되어 있는 메서드들 → TokenAuthMiddleware로 변경
    3. 변경한 validator에는 user의 권한 계층 기능도 적용(user, admin)
    4.  
       
  1. token refresh
      • 토큰 검증 → REFRESH_SECRET를 사용하여 서명 확인(jwt.SigningMethodHMAC) & token의 valid 검사, 만료날짜 확인
      • 토큰으로부터 uuid와 user의 ID를 가져옴( claims["refresh_uuid"].(string), claims["user_id"]) → jwt.Parse 이용.
      • 해당 access, refresh 토큰 삭제(DeleteAuth)
        • redis의 refresh_token UUID 값을 삭제 → client.del(access_token_uuid), client.del(refresh_token_uuid)
      • 토큰 재생성 (CreateToken) → 로그인 할때의 토큰 생성과 동일
      • token의 user & TokenDetails 값을 redis에 저장 (CreateAuth)
       
 

JWT 토큰 권한 계층 모델링

  • 현재 UserSessionValidation() 이라는 미들웨어로 권한을 관리하고 있다.
  • user table의 role 이라는 컬럼으로 권한 관리중
notion image
 
 
 

reference

 
 
Share article

Tom의 TIL 정리방