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

Header

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

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

- Signature는 인코딩된 Header와 Payload를 더한 뒤 비밀키로 해싱하여 생성된다.
- Header와 Payload는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없다. 따라서 Signature는 토큰의 위변조 여부를 확인하는데 사용된다.
JWT의 인증 과정
- 클라이언트 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유 ID 등의 정보를 Payload에 담음.
- 암호화할 비밀키를 사용해 Access Token(JWT)을 발급.
- 클라이언트는 전달받은 토큰을 저장해두고, 서버에 요청할 때 마다 토큰을 요청 헤더
Authorization
에 포함시켜 함께 전달.
- 서버는 토큰의 Signature를 비밀키로 복호화한 다음, 위변조 여부 및 유효 기간 등을 확인.
- 유효한 토큰이라면 요청에 응답.
JWT 토큰 타입
- Access Toeken
- 인증이 필요한 요청에는 access token을 사용한다.
- access token은 15분 정도로 짧은 수명을 가질 것을 권장한다(탈취되더라도, 심각한 공격을 방지할 수 있음).
- Refresh Token
- 새로운 access token을 발급하는데 사용된다.
- 일반적으로 7일 정도로, access token보다 긴 인증시간을 갖는다.
구현 및 실습
사용 라이브러리
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 파일에서 불러와 사용하도록 한다.
- 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 NewWithClaims(method
SigningMethod
, claims
Claims
) *
Token
- 서명 메소드명과 정의한 claims를 넣음.
- func (*Token) SignedString ¶ → 토큰에 서명
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의 만료시간이 되면 자동으로 삭제됨.
- token validator
- 인증이 필요한 요청의 경우, jwt 토큰을 사용하여 인증을 진행하여야 한다.
- 토큰의 메타데이터 추출
- 토큰 검증 → 추출한 헤더 string에서 jwt를 파싱한다
- 가져온 토큰에서 signing method을 검증(jwt.SigningMethodHMAC)한다.
- 검증이 통과되면, token의 Claims를 검사한다. → access_uuid & user ID가 맞아야 한다.
- logout
- 토큰의 http header을 추출 → token validator 할 때와 동일하게 진행한다.
- 가져온 토큰에서 signing method 검증(jwt.SigningMethodHMAC)
- 검증이 통과되면, token의 Claims를 검사한다. → access_uuid & user ID가 맞아야 한다.
- 검사에 통과하면, redis client의 해당 key를 제거한다. →
client.Del(givenUuid)
- AccessUuid로 redis의 key값을 불러온다.
- 불러온 key값의 ID와, 저장된 db(mysql?)의 ID 값을 비교한다.
- ID가 맞을 경우, 인증에 통과된다.
- ID가 맞지 않을 경우, 인증 실패 메세지와 함께 요청을 거부한다.
A. 토큰 검사
B. redis로 사용자 검증
- token validator(middleware)
- 토큰의 검증 → 마찬가지로 토큰을 추출하여 (ExtractToken) jwt의 SigningMethodHMAC 값들을 변경한다.
- 현재 session validation이 적용되어 있는 메서드들 → TokenAuthMiddleware로 변경
- 변경한 validator에는 user의 권한 계층 기능도 적용(user, admin)
- 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 이라는 컬럼으로 권한 관리중

reference
Share article