사이먼's 코딩노트

[SpringBoot] JWT 토큰 발급 본문

Java/SpringBoot

[SpringBoot] JWT 토큰 발급

simonpark817 2024. 6. 15. 20:09

[JWT 토큰 발급]

  • SpringBoot를 통해 JWT 인증 구현 코드를 구현하기 전에 다시 한번 JWT 인증의 내용을 정리해봅시다.
  1. 서버가 최초로 접속하는 브라우저에게 쿠키의 세션 키 정보를 담는 방식으로 각각의 사용자를 구분한다.
  2. 초창기 어플리케이션은 쿠키 기능이 없어 ID와 PW를 저장해두었다가, 모든 요청에 한 번에 응답했다.
  3. 서버에서 신원확인용 토큰을 발급하여, 앱에서 사용하도록하면 해킹 발생 시에 PW를 바꾸지 않아도 된다.
  4. JWT 토큰은 정보를 담을 수 있고, 유효성 체크에 DB 조회를 수반하지 않는다.
  5. JWT 토큰을 만들 때 Secret key로 흔적을 남기고, JWT 토큰에서 정보를 얻을 때, Secret key로 그것이 오명되지 않았음을 인증한다.
  6. JWT 토큰은 한 번 만들면 만료시킬 수 없고, 그 안의 정보를 갱신할 수도 없다.
 

GitHub - psm817/sb_jwt_2406

Contribute to psm817/sb_jwt_2406 development by creating an account on GitHub.

github.com

 

[초기 세팅 및 secret key 생성]

  • 먼저, Spring Initializr를 통해 프로젝트를 생성하는데, 필자는 일단 Dependencies에 Spring Web, Dev Tools, Lombok만 포함하였다.
  • 프로젝트 생성 완료 후 개발환경을 통해 OPEN을 하면 application.yml에 secret key를 생성한다.
  • 원래는 application-secret.yml을 새로 생성하여 해당 값을 숨기는 절차가 맞지만, 테스트이기 때문에 일단 application.yml에 아래와 같이 secret key을 사용자 임의로 설정해준다.
custom:
  jwt:
    secretKey: alskdjvocijioq23450lckjvclxzk3430415lkzdlkjacv043121235lkjdazdflv3papopw

 

  • 작성된 secret key가 실제로 존재하는지 테스트하기 위해서는 TestApplication에서 아래와 같이 assertThat을 이용한 코드를 작성한 후 오류없이 실행이 잘되는지 확인한다.
@SpringBootTest
class JwtReviewApplicationTests {

    @Value("${custom.jwt.secretKey}")
    private String secretKey;

    @Test
    @DisplayName("시크릿 키 존재 여부 체크")
    void test1() {
       assertThat(secretKey).isNotNull();
    }
}

 

[JWT 관련 라이브러리 추가]

  • 1차 테스트가 완료되었다면 build.gradle에 JWT와 관련된 라이브러리를 추가하여 Load를 해준다.
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

 

  • 라이브러리 추가가 완료되었다면, 이번에는 TestApplication을 통해서 secret key를 통해 HMAC 암호화 방식에 맞는 secret key 객체를 만들어보는 테스트를 진행한다.
@SpringBootTest
class JwtReviewApplicationTests {

    @Value("${custom.jwt.secretKey}")
    private String secretKeyPlain;

    @Test
    @DisplayName("시크릿 키 존재 여부 체크")
    void test1() {
       assertThat(secretKeyPlain).isNotNull();
    }

    @Test
    @DisplayName("시크릿 키를 이용하여 암호화 알고리즘 SecretKey 객체 만들기")
    void test2() {
       String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
       
       SecretKey secretKey = Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
       
       assertThat(secretKey).isNotNull();
    }
}
  • 위 코드는 application.yml에 작성한 secretKey 원문을 64비트로 인코딩한 후에 HMAC 키를 이용해서 다시 해시 값으로 만들고 해당 객체가 존재하는지 테스트를 구현한 코드이다.

 

[JwtProvider 생성]

  • 이번에는 JWT 토큰을 생성해주는 역할을 하는 JwtProvider 클래스를 생성해준다.
  • 2차 테스트에서 구현한 코드를 활용하여 JwtProvider.java 클래스에서 cachedSecretKey라는 변수에 암호화된 알고리즘 키를 가져오도록 한다.
  • 아래는 JwtProvider.java 클래스를 새로 생성한 뒤 작성된 코드이다.
@Component
public class JwtProvider {
    private SecretKey cachedSecretKey;

    @Value("${custom.jwt.secretKey}")
    private String secretKeyPlain;

    private SecretKey _getSecretKey() {
        // 64비트로 인코딩
        String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());

        // HMAC 키로 암호화 객체 생성
        return Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
    }

    public SecretKey getSecretKey() {
        if (cachedSecretKey == null) cachedSecretKey = _getSecretKey();

        return cachedSecretKey;
    }
}

 

  • JwtProvider을 생성했다면 다시 한번 TestApplication을 통해 getSecretKey() 메서드가 제대로 동작하는 지 확인하고, 여러개의 secretKey 객체를 생성했을 때 한 번만 처리되는지 확인한다.
@Test
@DisplayName("JwtProvider 객체를 활용하여 SecretKey 객체 생성")
void test3() {
    SecretKey secretKey = jwtProvider.getSecretKey();
    assertThat(secretKey).isNotNull();

    System.out.println(secretKey);
}

@Test
@DisplayName("SecretKey 객체 생성을 한 번만 하도록 처리")
void test4() {
    SecretKey secretKey1 = jwtProvider.getSecretKey();
    SecretKey secretKey2 = jwtProvider.getSecretKey();

    assertThat(secretKey1 == secretKey2).isTrue();
}

 

[Access Token 발급]

  • 이제 본격적으로 토큰을 발급하기 위해서 Util이라는 클래스를 하나 생성하여 아래와 같이 코드 작성을 해준다.
public class Util {
    public static class json {

        public static Object toStr(Map<String, Object> map) {
            try {
                return new ObjectMapper().writeValueAsString(map);
            } catch (JsonProcessingException e) {
                return null;
            }
        }
    }
}
  • 위 코드는 암호화된 토큰을 JSON 형태로 바꿔주거나, 기존 배열 값을 String으로 변경하기 위한 메서드이다.
  • 간단히 말해서 토큰을 가공해주는 역할을 Util.java에서 진행한다.

 

  • Util.java 클래스 작성이 끝났다면 JwtProvider에 실제로 access token을 발급하는 genToken() 메서드를 아래와 같이 작성해준다.
public String genToken(Map<String, Object> claims, int seconds) {
    long now = new Date().getTime();
    Date accessTokenExpiresIn = new Date(now + 1000L * seconds);

    return Jwts.builder()
            .claim("body", Util.json.toStr(claims))
            .setExpiration(accessTokenExpiresIn)
            .signWith(getSecretKey(), SignatureAlgorithm.HS512)
            .compact();
}
  • now 변수를 통해 날짜를 가져오고, accessTokenExpiresIn 변수를 통해 토큰의 유효 기간을 생성한다.
  • return 값에서 .claim("body", Util.json.toStr(claims))을 통해 데이터를 Util에서 가공한 다음 Jwts에 생성한다.

 

  • 모든 코드 작성이 완료되었다면 TestApplication에서 테스트로 토큰이 발급이 되는지 아래 코드 작성과 함께 확인한다.
@Test
@DisplayName("access Token 발급")
void test5() {
    Map<String, Object> claims = new HashMap<>();     // claims는 Token에 payload에 저장되는 정보

    claims.put("id", 2L);
    claims.put("username", "psm817");

    String accessToken = jwtProvider.genToken(claims, 60 * 60 * 5);

    System.out.println("accessToken : " + accessToken);

    assertThat(accessToken).isNotNull();
}
  • 해당 코드를 실행하면 builder를 통해 실제로 access token이 발급되는 것을 확인할 수 있고, JWT Debugger 홈페이지(https://token.dev/)에 가서 발급된 토큰을 작성해보면 코드에서 작성한 id와 username 정보를 확인할 수 있다.

access token 발급 성공

 

[Access Token 인증 및 claims 얻기]

  • Access Token 발급에 성공하였다면, 마지막으로 생성된 토큰이 유효한지 아닌지 판단해보고, TestApplication에서 작성한 id와 username이 암호화에 성공한 데이터 정보를 가져올 수 있도록 확인해봅시다.
  • 먼저 JwtProvider.java 클래스에서 아래와 같이 코드를 추가한다.
// 토큰이 유효한지 아닌지 판단
public boolean verify(String token) {
    try {
        Jwts.parserBuilder()
                .setSigningKey(getSecretKey())
                .build()
                .parseClaimsJws(token);
    } catch (Exception e) {
        return false;
    }

    return true;
}

public Map<String, Object> getClaims(String token) {
    String body = Jwts.parserBuilder()
            .setSigningKey(getSecretKey())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .get("body", String.class);

    return Util.toMap(body);
}
  • getCliams() 메서드에서 return 값에 작성된 Util 클래스의 toMap() 메서드는 아래와 같이 코드를 작성하면 된다.
public static Map<String, Object> toMap(String jsonStr) {
    try {
        return new ObjectMapper().readValue(jsonStr, LinkedHashMap.class);
    } catch (JsonProcessingException e) {
        return null;
    }
}

 

  • 이제는 TestApplication을 통해 access token을 이용해서 claims 정보를 가져와보고, 만료된 토큰이 유효하지 않은지 테스트를 해봅시다.
@Test
@DisplayName("access Token을 이용하여 claims 정보 가져오기")
void test6() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1L);
    claims.put("username", "admin");

    // 10분
    String accessToken = jwtProvider.genToken(claims, 60 * 10);

    System.out.println("accessToken :" + accessToken);

    assertThat(jwtProvider.verify(accessToken)).isTrue();

    Map<String, Object> claimsFromToken = jwtProvider.getClaims(accessToken);
    System.out.println("claimsFromToken : " + claimsFromToken);
}

@Test
@DisplayName("만료된 토큰이 유효하지 않은지")
void test7() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1L);
    claims.put("username", "admin");

    // 10분
    String accessToken = jwtProvider.genToken(claims, 60 * 10);

    System.out.println("accessToken :" + accessToken);

    assertThat(jwtProvider.verify(accessToken)).isFalse();
}
  • test6 에서는 10분의 유효기간인 토큰을 발급받고, verify() 메서드를 통해 발급된 토큰이 유효한지 확인하고, getClaims() 메서드를 통해 암호화된 정보를 가져오도록 하였다.
  • test7 에서는 토큰의 유효기간에 따라서 토큰이 유효하지 않은지 verify() 메서드를 통해 확인한다.

테스트 성공

반응형

'Java > SpringBoot' 카테고리의 다른 글

[SpringBoot] REST API (2)  (0) 2024.06.16
[SpringBoot] REST API (1)  (0) 2024.06.16
[SpringBoot] Cookie / Session / JWT  (2) 2024.06.15
[SpringBoot] 이메일 발송  (0) 2024.05.28
[SpringBoot] 소셜 로그인(카카오톡)  (0) 2024.05.27