[Spring] JWT Token 기본 이해하기(+실습)

728x90

안녕하세요✋

오늘은 JWT Token에 대해서 간단하게 알아보려고 합니다.

 

JWT Token은 보통 로그인 로직을 짤 때 많이 활용하고는 합니다.

Spring Security에서 JWT 를 사용하기 위해서 공부를 해보았고

아래 내용을 보면서 기본 개념이랑 어떻게 활용하는지에 대해서 더 알아보겠습니다.

 

 

📌 기본 개념

JWT(JSON Web Token) 은 웹 표준으로써 데이터의 JSON 객체를 사용하여 가볍고 자가 수용적인 방식으로 정보를 안전하게 전달할 수 있도록 전달할 수 있도록 설계된 토큰 기반의 인증 방식입니다.

 

토큰이라는 개념이 생소할 수도 있습니다. 일단 내용을 쭉 읽으시면서 자연스럽게 알 수 있습니다.

 

JWT는 URL, HTTP Header, Cookie, HTML Form 과 같은 다양한 방식으로 전달할 수 있으며, 서버와 클라이언트 간의 인증 정보를 포함합니다.

 

인증정보라 함은, 로그인시 아이디 비밀번호가 일치하는 사용자인지? 이런 류의 정보를 의미합니다.

 

그리고 JWT는 Header, Payload, Signature 세 부분으로 구성됩니다. 

1) Header 

JWT의 타입과 암호화 알고리즘 등을 포함하며, JSON 형식으로 인코딩 됩니다.

 

2) Payload

클레임 정보를 포함하며, JSON 형식으로 인코딩 됩니다. 클레임 정보는 사용자 ID, 권한 등의 정보를 포함한다.

 

3) Signature

Header + payload 를 조합하여, 비밀 키를 사용하여 생성된 서명 값입니다.

서명 값은 토큰의 무결성을 보장하여, JWT 가 조작되지 않았다는 것을 검증합니다.

(무결성 : 데이터의 정확성, 일관성, 유효성이 유지되는 것을 말함)

 

 

한번 간단하게 실습을 해보겠습니다.

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

위 사이트가 JWT 공식 사이트 입니다.

 

위 사이트를 들어가보시면 Encoded(token) -> Decoded 가 있습니다.

 

빨간색 : Header

보라색 : PAYLOAD

하늘색 : Signature 

를 의미 합니다.

 

즉 위 3가지를 다 합쳐서 만들어 진 것 입니다.

위 설명 처럼 (HEADER, PAYLOAD, Signature) 3가지로 나눠져 있습니다.

 

위 내용을 차근차근 보시면

Header 에서 알고리즘 타입은 : HS256, type: jwt 로 되어 있습니다.

payload 에는 내용들이 들어가 있고,

Signature 에는 Header + payload 를 합쳐주는 내용들이 있습니다. 

 

 

 

그리고 토큰을 검증 해보고 싶으시면 

https://www.base64decode.org/ko/

 

Base64 디코딩 및 인코딩 - 온라인

Base64 형식에서 디코딩해보세요. 아니면 다양한 고급 옵션으로 인코딩해보세요. 저희 사이트에는 데이터 변환하기에 사용하기 쉬운 온라인 도구가 있습니다.

www.base64decode.org

아래 사이트에서, Encoded 에서 Header, Payload 를 복사해서 검증을 하고 디코딩, 인코딩을 할 수 있습니다.

 

 

그러나 Signature 는 디코딩을 하려고 값을 넣으면 이상한 값이 나옵니다. 왜 일까요?

 

그 이유는

첫째 줄 : base64 로 인코딩된 Header 를 저장하고 "." 을  + 한다음에

둘째 줄 : base64로 인코딩된 Payload를 저장하고, 

셋째 줄 : 그 문자열을 내가 가지고 있는 Secret key 를 합친 다음에

마지막으로 Hash 알고리즘을 통해서 암호화를 하기 때문 입니다.

 

즉 나만의 Key 가 필요하다는 이유 때문 입니다. 

 

간단하게 해쉬 알고리즘을 알아보면

 

A + key = 결과 값 

해쉬 알고리즘은 어떠한 값을 A 라는 값을 어떠한 Key 를 통해 암호화하게 되면 항상 동일한 값이 나옵니다.

즉 A 라는 값은 항상 같은 key 로 해쉬 알고리즘을 하게되면 결과 값은 항상 같습니다.

 

 

이제 본론으로 들어가서 JWT Token을 이용한 인증 방식을 알아보겠습니다

 

1) 클라이언트가 서버에 로그인 요청을 보냅니다 (ID, PassWord)

 

2) 서버는 로그인 요청을 검증하고, 유효한 사용자라면 JWT를 생성하여 클라이언트에게 반환 됩니다

- 반환하는 방법은 여러가지 입니다 (쿠키, 세션 etc)

 

3) 클라이언트는 이후 요청에 JWT를 포함시켜 전송합니다.

- 요청을 할 때, 쿠키, 헤더, url 에 붙여서 서버로 전송을 한다.

 

4) 서버는 JWT를 검증하여, 클라이언트의 인증 여부를 판단합니다.

- 서버가 내가 Issue 했던 Token 을 그대로 가져왔는지를 검증합니다.

 

 

그렇다면 JWT Token 을 왜 써야할까요?

 

1) 토큰 기반의 인증 방식 이므로, 서버 측에서 별도의 세션 저장소를 유지할 필요가 없습니다 -> 클라이언트쪽 에서 관리한다.

2) JSON 형식으로 인코딩 되므로, 다양한 플랫폼 간에 쉽게 전송 및 구현할 수 있습니다.

3) Signature 를 사용하여 무결성을 보장하므로, 토큰이 변조되었는지 여부를 쉽게 검증할 수 있습니다 (=해쉬 알고리즘)

 

 

이번에는 단점에 대해서 알아 보겠습니다.

 

1) JWT의 크기가 커질 경우, 네트워크 대역폭이 증가하게 된다. 

2) JWT는 한 번 발급된 후에는 내부 정보를 수정할 수 없으므로, 만료 시간을 짧게 설정해야 한다.

3) JWT를 탈취당하면, 해당 토큰을 사용한 모든 요청이 인증되므로, 보안 위협이 있을 수 있습니다.

-> HTTPS 보안 프로토콜을 이용하여 JWT를 전송해야 합니다.

 

이제는 코드를 보면서 이해를 해보겠습니다.

 

Java17 + SpringBoot3.2 + Gradle 환경에서 프로젝트를 진행 중이니 참고해주시길 바랍니다!

 

JWT를 사용하기 위해서는 Gradle 에 의존성을 추가해줘야 합니다.

implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

 

위 라이브러리를 추가하고 Gradle 을 Reload 하면 라이브러리가 추가 된 것을 확인할 수 있습니다. 

 

build.Gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.5'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // JWT
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

 

✅ JwtService

package org.example.delivery.jwt;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Map;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class JwtService {

	private static final String SECRET_KEY = "java17springbootJWTTokenIssuedExample"; // 256bit 이상의 Key를 넣어야함

	// Token 생성
	public String createToken (Map<String, Object> claims, LocalDateTime expireAt) {
		// 외부에서 claim 을 받아온다
		// 만료일자 또한 외부에서 받아온다.

		// custom key 를 생성한다.
		SecretKey secretKey = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
		// 날짜 형식 맞추기
		Date _expiredAt = Date.from(expireAt.atZone(ZoneId.systemDefault()).toInstant());

		return Jwts.builder()
			.signWith(secretKey, SignatureAlgorithm.HS256) // 어떤 알고리즘을 사용하여 키를 암호화 할 것인지
			.setClaims(claims) // 어떠한 내용을 body 에 넣을지
			.setExpiration(_expiredAt) // 만료시간 지정
			.compact();
	}

	// Token 검증 -> 검증은 서버에서 한다.
	public void validToken (String token) { // 클라이언트에서 token 이 넘어오면 그것을 검증한다.

		SecretKey secretKey = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());

		// Token 파싱하기 위한 기초 작업.
		JwtParser parser = Jwts.parserBuilder()
			.setSigningKey(secretKey)
			.build();

		// 파싱 로직 + 유효성 예외 처리
		try {
			var result = parser.parseClaimsJws(token);

			result.getBody().entrySet().forEach(value -> {
				log.info("key : {}, Value : {}", value.getKey(), value.getValue());
			});
		} catch (Exception e) {
			if (e instanceof SignatureException) {
				throw new RuntimeException("JWT Token Not Valid Exception");
			}
			else if (e instanceof ExpiredJwtException) {
				throw new RuntimeException("JWT Token Expired Exception");
			} else {
				throw new RuntimeException("JWT Token Validation Exception");
			}
		}

	}

}

 

토큰을 생성하고, 생성된 토큰을 검증하는 것 까지의 로직입니다.

 

다음으로는 위 로직이 잘 동작하나 테스트를 진행해보겠습니다.

테스트 환경은 Junit5 입니다.

 

✅ Test

package org.example.delivery;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.example.delivery.jwt.JwtService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DeliveryApplicationTests {

	@Autowired
	private JwtService jwtService;

	@Test
	void contextLoads () {
	}

	@Test
	@DisplayName("토큰 생성 확인 테스트")
	void tokenCreate () {
		// given
		Map<String,Object> claims =  new HashMap<String, Object>();
		claims.put("user_id",923); // 데이터 담음
		LocalDateTime expiredAt = LocalDateTime.now().plusMinutes(10);

		// when
		String token = jwtService.createToken(claims, expiredAt);

		// then
		System.out.println(token);
	}

}

 

 

위 생성 단위 테스트를 하여 JWT Token이 잘 만들어진 것을 확인하였습니다.

 

 

이런식으로 생성이 되었습니다.

그러면 위 키를 가지고 처음에 설명했던 jwt.io 로 들어가서 위 키가 맞는지 체크를 해보겠습니다.

위 Token Key 를 복사해서 Encoded 에 넣으면 Decoded 에 내가 넣은 값들이 알맞게 뜹니다. 

 

 

 

Header 와 Payload 는 알맞은 값을 가지고 있지만

왼쪽 하단에 Invalid Signature 라고 뜹니다.

 

왜 유효하지 않다고 뜨는 걸까요?

 

왜냐면 지금은 Secret Key 가 일치하지 않기 때문입니다. 

 

위 토큰을 유효하게 만들기 위해서는 제가 미리 설정해 두었던 JwtService 클래스에

Secret key를 가져와서 넣어줘야 합니다. 

 

Secret key 

java17springbootJWTTokenIssuedExample

 

 

이제 유효한 토큰이라고 뜨네요 

 

이제 위 토큰이 유효한 토큰인지 검증하는 테스트를 진행해보겠습니다.

	@Test
	@DisplayName("토큰 검증 확인 테스트")
	void tokenValidation () {

		// given
		String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo5MjMsImV4cCI6MTcxMzkzNjQxNH0.F-1CfdR5q6wL23QhxXApPF6cYjIUUaO5SFtYvUZT_IE";

		// then
		jwtService.validToken(token);
	}

 

 

위 테스트를 진행해보면 테스트가 잘 진행이 되는 것을 확인할 수 있습니다.

 

 

 

이상으로 JWT Token 에 대한 이야기를 마치겠습니다. 

감사합니다👍🏽

 

실습 Git : https://github.com/Hyeonqz/TIL/tree/main/Practice/JWT-Token/JWT

728x90