Security를 활용한 로그인 구현을 위해 여러 블로그들을 참고하여 작성한 글입니다.
내용에 잘못된 점이 있다면 지적해주시면 감사하겠습니다!
먼저 로그인 구현에 앞서 JWT가 뭔지 개념을 정리해보도록 하자.
JWT란?
JWT는 JSON Web Token의 약자로, Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다.
언제?
- 권한 부여 : JWT를 사용하는 가장 일반적인 방식. 사용자가 로그인하면 각 후속 요청에 JWT가 포함되어 사용자가 해당 토큰으로 서비스 및 리소스에 접근할 수 있다.
- 정보 교환 : 공개/개인 키 쌍을 사용해 JWT에 서명이 가능하기 때문에 발신자가 누구인지 식별 가능하다. 또한 헤더와 페이로드를 사용하여 서명을 계산하므로 콘텐츠 변조여부를 식별할 수 있다.
토큰 사용의 장점
- JSON으로 생성된 토큰은 용량이 작기 때문에 매우 빠르게 전달될 수 있다.
- HMAC 알고리즘 또는 RSA 또는 ECDSA를 사용하는 공개/개인 키 쌍을 사용하여 서명할 수 있기 때문에 무결성이 보장된다.
- 토큰을 서버가 아닌 클라이언트 측에서 관리하므로 stateless하다.
- 세션을 사용하면 모든 서버에서 세션 내용을 공유해야 하기 때문에 서버가 다수 존재하는 환경에서 유용하다.
- 매 요청시마다 DB 조회를 하지 않고 토큰 자체만으로 사용자의 정보와 권한을 알 수 있기에 효율적이다.
토큰 사용의 단점
- 토큰의 페이로드(Payload)에 3종류의 클레임을 저장하기 때문에, 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있다.
- 페이로드(Payload) 자체는 암호화 된 것이 아니라, BASE64Url로 인코딩 된 것이다. 중간에 Payload를 탈취하여 디코딩하면 데이터를 볼 수 있으므로, JWE로 암호화하거나 Payload에 중요 데이터를 넣지 않아야 한다.
- Stateless: JWT는 상태를 저장하지 않기 때문에 한번 만들어지면 제어가 불가능하다. 즉, 토큰을 임의로 삭제하는 것이 불가능하므로 토큰 만료 시간을 꼭 넣어주어야 한다.
- Tore Token: 토큰은 클라이언트 측에서 관리해야 하기 때문에, 토큰을 저장해야 한다.
JWT의 구조
header.payload.signature 세가지로 이루어져 있다. (헤더, 내용, 서명)
각 부분은 Base64로 인코딩되어있다.
헤더(Header)
토큰의 타입(type)과 해싱 알고리즘(alg)를 지정
헤더는 토큰의 유형 + 서명에 대한 해싱 알고리즘(보통 HMAC SHA256 또는 RSA)를 지정하는 정보를 가지고 있음.
- SHA : 해쉬를 사용하는 암호화 방식, 복호화 x
- HS512 해싱 알고리즘 : HMAC + SHA 시크릿 키
내용(Payload)
토큰에 담을 정보
토큰에 담을 하나의 정보, 한 조각의 정보를 클레임(Claim)이라고 하는데, 클레임의 종류에는 3가지가 있다.
등록된 클레임 (registered claim)
- 서비스에 필요한 정보가 아니라 토큰에 대한 정보들을 담기 위해 이름이 이미 정해져 있는 클레임.
- 필수는 아니지만 권장되는 클레임
- iss(발급자), sub(제목), aud(대상자), exp(만료시간), nbf(토큰의 활성날짜), iat(발급된시간), jti(JWT 고유식별자, 일회용 토큰에 사용)
공개 클레임 (public claim)
- 공개 클레임은 사용자 정의 클레임으로, 공개용 정보 전달을 위해 사용. 충돌을 방지하기 위해 URI 포맷을 이용
비공개 클레임 (private claim)
- 비공개 클레임은 등록된 클레임도 아니고, 공개 클레임도 아닌 당사자간에 정보를 공유하기 위해 만들어진 사용자 지정 클레임.
- 이곳에 인증 정보 등의 서버와 클라이언트간 필요한 정보를 넣어두는 형식
}
"sub": "accessToken", // 등록된 클레임 (제목)
"name": "Do Hyun", // 비공개 클레임
"email" : "dh1010a@naver.com" // 비공개 클레임
"iat": 1516239022 // 등록된 클레임 (발급된 시간)
}
서명(signature)
토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드
서명은 위에서 만든 헤더와 페이로드의 값을 각각 BASE64로 인코딩하고, 인코딩 한 값을 비밀 키를 이용하여 헤더에서 정한 알고리즘으로 해싱을 한 후 다시 BASE64로 인코딩 하여 생성한다.
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
서명은 HS256 방식의 경우 우리가 만든 헤더와 페이로드, 그리고 나만 아는 개인 키를 넣어서 HS256 암호화 알고리즘을 사용하여 암호화를 하여서 사용한다.
이는 서버에서 클라이언트로부터 JWT를 받았을 때, JWT의 헤더와 페이로드를 서버에서 똑같이 HS256으로 암호화하여, 클라이언트가 보낸 JWT의 서명고 같으면 요청된 것으로 알고 검증한다.
RSA의 경우, 시크릿 키를 넣지 않고, 서버의 개인 키로 잠군 후, 토큰을 전송한다. 클라이언트는 해당 토큰을 받아 다시 서버에 전송시, 그냥 서버의 공개 키로 열어보기만 하면 된다.
토큰 분류
1. Access Token
인증된 사용자가 특정 리소스에 접근할 때 사용되는 토큰
- 클라이언트는 Access Token을 사용하여 인증된 사용자의 신원을 확인하고, 서비스 또는 리소스에 접근
- 유효 기간이 지나면 만료 (expired)
- 만료된 경우, 새로운 Access Token을 얻기 위해 Refresh Token 사용
2. Refresh Token
Access Token의 갱신을 위해 사용되는 토큰
- 일반적으로 Access Token과 함께 발급
- Access Token이 만료되면 Refresh Token을 사용하여 새로운 Access Token 발급
- 사용자가 지속적으로 인증 상태를 유지할 수 있도록 도와줌 (매번 로그인 다시 하지 않아도 됨)
- 보안 상의 이유로 Access Token보다 긴 유효 기간 가짐
토큰 기반 인증의 문제점
서버 쪽에서 관리가 되고 있지 않기 때문에 올바른 사용자인지, 아니면 누군가 토큰을 탈취해 악의적인 의도로 접근해 온 사용자인지 알지 못한다는 것.
때문에 액세스 토큰의 유효 기간을 아예 짧게 설정하는 것으로 나름 보안을 강화할 수 있지만 그러면 사용하는 입장에선 불편함을 느낄 수 밖에 없다. 이런 문제점을 해결하기 위해 나타난 것이 Refresh Token!
Refresh Token
Refresh Token은 access Token이 만료되었을 때 access Token을 새로 발급하기 위해 필요한 토큰이다.
access Token과 다른 점은 Refresh Token은 access Token보다 유효 기간이 길고, 데이터베이스에 저장한다는 것.
RefreshToken 인증 과정
- 클라이언트 측에서 서버에 인증 요청
- 서버는 사용자를 확인하기 위해 DB를 조회한다.
- 사용자가 유효한 경우, 서버측에서 액세스 토큰과 리프레시 토큰 두 개를 클라이언트 측에 전송
- 클라이언트는 전송받은 두 토큰을 로컬 스토리지에 저장, 서버에서는 리프레시 토큰을 데이터베이스에 저장
- 클라이언트 측에서 인증이 필요한 API 요청을 서버측에 보낼 때마다 액세스 토큰을 같이 전송
- 서버 측에서는 전달받은 액세스 토큰에 대한 유효성 검사를 진행한다.
- 액세스 토큰이 유효하다면 클라이언트 요청을 처리한다.
- 액세스 토큰이 만료된 상황.
- 사용자가 만료된 액세스 토큰과 함께 서버에 요청을 보낸다.
- 서버는 액세스 토큰에 대한 유효성 검사를 진행하고, 토큰이 만료되었다는 에러를 클라이언트에게 전달
- 에러를 전달받은 클라이언트는 리프레시 토큰과 함께 새 액세스 토큰을 서버 측에 요청
- 서버에서는 리프레시 토큰 유효성 검사를 위해 데이터베이스에서 리프레시 토큰을 조회 후 비교
- 유효하다면 새 액세스 토큰을 발급하여 클라이언트에게 전달한다. 클라이언트는 새 액세스 토큰을 가지고 원래 요청하려던 API를 다시 요청
장점
Session에 비해 RefreshToken이 만료된 경우에만 DB에 접근하므로 I/O가 줄어 성능이 향상됨.
JWT 주의점
- 토큰을 필요 이상으로 오래 보관하면 안된다.
- 보안에 취약할 수 있기에 민감한 세션 데이터를 브라우저 저장소에 저장해서는 안된다.
- 클라이언트는 관습적으로 Bearer 스키마를 사용하여 Authorization 헤더에서 JWT를 보내야 한다.
Authorization: Bearer <token>
참고
'Back-end > Spring' 카테고리의 다른 글
[Spring] @NotNull vs @Column(nullable = false) (0) | 2024.08.19 |
---|---|
[Spring] 무한 redirection (0) | 2024.08.14 |
[Spring] WebClient를 통한 공공데이터 Open API 호출 (0) | 2024.07.17 |
[Spring] H2 DataBase 사용법 (0) | 2024.06.27 |
[Spring] Spring만의 유효성 검사 @Valid, @Validated 정리 (1) | 2024.06.27 |