9.2 JWT 서비스 구현 - 테스트 코드 작성

SummerToday·2024년 3월 2일
1
post-thumbnail
post-custom-banner

테스트 코드 작성

// test - config - jwt - JwtFactory.java

@Getter
public class JwtFactory {
    
    // 토큰 기본 값 설정
    private String subject = "test@email.com";

    private Date issuedAt = new Date();

    private Date expiration = new Date(new Date().getTime() + Duration.ofDays(14).toMillis());

    private Map<String, Object> claims = emptyMap();

    @Builder 
    public JwtFactory(String subject, Date issuedAt, Date expiration,
                      Map<String, Object> claims) {
        this.subject = subject != null ? subject : this.subject;
        this.issuedAt = issuedAt != null ? issuedAt : this.issuedAt;
        this.expiration = expiration != null ? expiration : this.expiration;
        this.claims = claims != null ? claims : this.claims;
    }

    public static JwtFactory withDefaultValues() {
        return JwtFactory.builder().build();
    }

    
    public String createToken(JwtProperties jwtProperties) {
        return Jwts.builder()
                .setSubject(subject)
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(issuedAt)
                .setExpiration(expiration)
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }
}
  • private Map<String, Object> claims = emptyMap();
    Map<String, Object>은 키가 문자열이고 값이 임의의 객체인 Map을 나타낸다.
    emptyMap() 메서드는 빈 Map을 생성하여 반환한다.

    Map은 Java에서 키-값 쌍을 저장하는 자료구조이다. 각 키는 고유해야 하며, 해당 키에 대응하는 값에 접근할 수 있다.

  • public JwtFactory(String subject, Date issuedAt, Date expiration, Map<String, Object> claims){~}
    빌더 패턴을 사용해 설정이 필요한 데이터만 선택하여 설정한다.

  • public String createToken(JwtProperties jwtProperties){~}
    jjwt 라이브러리를 사용해 JWT 토큰을 생성한다.

    • Jwts.builder()
      JWT 토큰을 빌드한다.

    • setSubject()
      토큰의 주제를 설정한다. 값은 JwtFactory 클래스의 subject 필드에서 가져온다.

    • setHeaderParam()
      토큰의 헤더 파라미터를 설정한다. 해당 코드에서는 토큰의 유형을 지정하기 위해 Header.TYPE을 Header.JWT_TYPE으로 설정한다.

    • setIssuer()
      토큰의 발급자를 설정한다. 값은 기존 JwtProperties에서 getIssuer() 메서드를 통해 가져온다.

    • setIssuedAt()
      토큰의 발급 시간을 설정한다. 값은 JwtFactory 클래스의 issuedAt 필드에서 가져온다.

    • setExpiration()
      토큰의 만료 시간을 설정한다. 값은 JwtFactory 클래스의 expiration 필드에서 가져온다.

    • addClaims()
      토큰에 클레임을 추가한다. 클레임은 JwtFactory 클래스의 claims 필드에서 가져온다.

    • signWith()
      토큰을 서명한다. 기존 JwtProperties에서 getSecretKey() 메서드를 통해 가져온 SecretKey를 사용하여 HS256 알고리즘으로 서명합니다.

    • compact()
      JWT는 헤더, 페이로드, 서명 세 부분으로 구성되어 있는데 이 세 부분을 .으로 구분되는 하나의 문자열 형태로 직렬화하여(합쳐) 반환한다.


// test - config - jwt - test - config - jwt - TokenProviderTest.java

@SpringBootTest
class TokenProviderTest {

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private JwtProperties jwtProperties;

    // generateToken() 검증 테스트
    @DisplayName("generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다.")
    @Test
    void generateToken() {
        // given
        User testUser = userRepository.save(User.builder()
                .email("user@gmail.com")
                .password("test")
                .build());

        // when
        String token = tokenProvider.generateToken(testUser, Duration.ofDays(14));

        // then
        Long userId = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody()
                .get("id", Long.class);

        assertThat(userId).isEqualTo(testUser.getId());
    }

    // validToken() 검증 테스트
    @DisplayName("validToken(): 만료된 토큰인 경우에 유효성 검증에 실패한다.")
    @Test
    void validToken_invalidToken() {
        // given
        String token = JwtFactory.builder()
                .expiration(new Date(new Date().getTime() - Duration.ofDays(7).toMillis()))
                .build()
                .createToken(jwtProperties);

        // when
        boolean result = tokenProvider.validToken(token);

        // then
        assertThat(result).isFalse();
    }

    // validToken() 검증 테스트
    @DisplayName("validToken(): 유효한 토큰인 경우에 유효성 검증에 성공한다.")
    @Test
    void validToken_validToken() {
        // given
        String token = JwtFactory.withDefaultValues()
                .createToken(jwtProperties);

        // when
        boolean result = tokenProvider.validToken(token);

        // then
        assertThat(result).isTrue();
    }


    // getAuthentication() 검증 테스트
    @DisplayName("getAuthentication(): 토큰 기반으로 인증정보를 가져올 수 있다.")
    @Test
    void getAuthentication() {
        // given
        String userEmail = "user@email.com";
        String token = JwtFactory.builder()
                .subject(userEmail)
                .build()
                .createToken(jwtProperties);

        // when
        Authentication authentication = tokenProvider.getAuthentication(token);

        // then
        assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(userEmail);
    }


    // getUserId() 검증 테스트
    @DisplayName("getUserId(): 토큰으로 유저 ID를 가져올 수 있다.")
    @Test
    void getUserId() {
        // given
        Long userId = 1L;
        String token = JwtFactory.builder()
                .claims(Map.of("id", userId))
                .build()
                .createToken(jwtProperties);

        // when
        Long userIdByToken = tokenProvider.getUserId(token);

        // then
        assertThat(userIdByToken).isEqualTo(userId);
    }
}
  • void generateToken(){~}
    토큰을 생성하는 메서드를 검증하는 테스트 코드이다.
    • given
      토큰에 유저 정보를 추가하기 위한 테스트 유저 생성
    • when
      tokenProvider의 generateToken() 메서드를 호출해 주어진 유저 정보와 만료 기간을 전달하여 토큰을 생성한다.
    • then
      jjwt 라이브러리를 사용해 토큰을 복호화한다.토큰을 만들때 클레임으로 넣어둔 id 값이 given 절에서 만든 유저 ID와 동일한지 확인하여 토큰이 잘 생성되었는지 확인한다.
      • Jwts.parser()
        JJWT 라이브러리에서 제공하는 JWT 파서를 생성한다. 이를 통해 JWT 토큰을 파싱할 수 있습니다.

        • Jwts 클래스
          Jwts 클래스는 JJWT 라이브러리에서 JWT를 생성하고 다루는 데 사용되는 클래스이다. Jwts 클래스를 통해서 필요한 클레임 셋을 만들고 secretKey와 함께 서명해서 JWS를 생성할 수 있다.

        • JJWT(Java Json Web Token)
          Java에서 JWT(JSON Web Token)를 생성하고 검증하는 데 사용되는 라이브러리이다.

      • setSigningKey(jwtProperties.getSecretKey())
        파싱할 토큰의 서명을 확인하기 위해 사용되는 비밀 키를 설정한다. jwtProperties.getSecretKey()는 주어진 jwtProperties에서 비밀 키를 가져오는 메서드이다.

        • cf. JWT의 서명은 헤더와 페이로드를 해싱한 후에 비밀 키를 사용하여 생성된다.
          따라서 JWT를 검증할 때는 해당 토큰이 유효한지 확인하기 위해 서명을 검증해야 한다. 이를 위해선 토큰을 생성할 때 사용된 비밀 키와 동일한 비밀 키를 사용하여 서명을 검증한다.
          그래서 Jwts.parser().setSigningKey(jwtProperties.getSecretKey()) 메서드를 사용하여 파서 객체에 검증할 때 사용할 비밀 키를 설정한다.

      • parseClaimsJws(token)
        주어진 토큰을 파싱하여 Jws (JSON Web Signature) 객체를 생성한다. JWS 객체는 JWT 토큰의 내용을 포함하고 있다.

        • JJWT 라이브러리에서 parseClaimsJws() 메서드를 사용하여 JWT를 파싱할 때, 실제로는 JWS를 파싱하고 서명을 검증하는 것이다. 만약 JWT가 암호화되어 있다면 JWE를 파싱하고, 복호화하는 과정이 추가될 것이다.

      • getBody()
        Jws 객체에서 클레임을 포함하는 토큰의 본문을 가져온다.

      • get("id", Long.class)
        클레임 중에서 키가 "id"에 해당하는 값을 추출한다. 여기서 Long.class는 해당 값을 Long 형식으로 가져오기를 요청한다.

  • void validToken_invalidToken(){~}
    유효하지 않은 토큰을 제시했을 때 validToken() 메서드가 유효하지 않은 토큰인지를 잘 판별하는지 테스트 하는 메서드이다.

    • given
      jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 만료 시간은 1970년 1월 1일부터 현재 시간을 밀리초 단위로 치환한 값 (new Date().getTime())에 1000을 빼, 이미 만료된 토큰을 생성한다.

      • new Date()
        현재 시간을 나타내는 Date 객체를 생성한다.

      • getTime()
        Date 객체의 시간 값을 반환합니다. 이는 1970년 1월 1일 자정부터 경과된 시간을 밀리초 단위로 나타낸다.

      • Duration.ofDays(7)
        7일의 기간을 나타내는 Duration 객체를 생성한다.

      • toMillis()
        Duration 객체의 값을 밀리초 단위로 변환합니다. 위 코드에서는 7일의 기간을 밀리초 단위로 변환한다.

      • new Date(현재 시간 - 7일의 기간)
        현재 시간으로부터 7일 이전의 시간을 나타내는 Date 객체를 생성한다.

      • build()
        설정이 완료된 JwtFactory 객체를 생성한다.

      • createToken(jwtProperties)
        JwtFactory 객체의 createToken 메서드를 호출하여 JWT 토큰을 생성한다. 이 때 jwtProperties 객체를 사용하여 토큰의 속성을 설정한다.
    • when
      tokenProvider 클래스의 vaildToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결괏값을 반환받는다.

    • then
      반환값이 false(유효한 토큰이 아님)인 것을 확인한다.


  • void validToken_validToken(){~}
    유효한 토큰을 제시했을 때 validToken() 메서드가 유효한 토큰인지를 잘 판별하는지 테스트 하는 메서드이다.

    • given
      jjwt 라이브러리를 사용해 토큰을 생성한다. 만료 시간은 현재 시간으로부터 14일 뒤로, 만료되지 않은 토큰으로 생성한다.

    • when
      tokenProvider의 validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결괏값을 반환한다.

    • 반환값이 true(유효한 토큰)인 것을 확인한다.


  • void getAuthentication(){~}
    토큰을 전달 받아 인증 정보를 담은 객체 Authentication를 반환하는 메서드인 getAuthentication()를 테스트한다.

    • given
      jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 토큰의 제목은 "user@email.com"으로 설정한다.

    • when
      tokenProvider의 getAuthentication() 메서드를 호출해 인증 객체를 반환한다.

    • then
      반환받은 인증 객체의 유저 이름을 가져와 given절에서 설정한 subject값인 "user@email.com"과 같은지 확인한다.

      • authentication.getPrincipal()
        Authentication 객체의 getPrincipal() 메서드를 호출하여 사용자의 인증 주체(Principal)를 가져온다. Spring Security에서는 주로 UserDetails 인터페이스를 구현한 객체가 사용자의 인증 주체로 사용된다.

        • ((UserDetails) authentication.getPrincipal()).getUsername()
          가져온 사용자의 인증 주체를 UserDetails 객체로 형변환하여 사용자의 이메일 주소를 확인한다. UserDetails 객체는 사용자의 인증 정보를 나타내며, 주로 사용자의 식별자(여기서는 이메일 주소)를 포함한다.

        • isEqualTo(userEmail)
          가져온 사용자의 이메일 주소가 예상한 이메일 주소와 일치하는지 확인한다. 이를 통해 테스트가 예상한 대로 동작하는지를 검증한다.

  • void getUserId(){~}
    토큰 기반으로 유저 ID를 가져오는 메서드를 테스트하는 메서드이다.
    토큰을 프로퍼티즈 파일에 저장한 비밀값으로 복호화한 뒤 클레임을 가져오는 getClaims()를 호출해서 클레임 정보를 반환 받아 클레임에서 id키로 저장된 값을 가져와 반환한다.

    • given
      jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 클레임을 추가한다. 키는 "id", 값은 1이라는 유저 ID를 설정한다.

    • when
      tokenProvider의 getUserId() 메서드를 호출해 유저 ID를 반환 받는다.
      • .claims(Map.of("id", userId))
        생성된 빌더를 사용하여 클레임(claims)을 설정한다. 사용자 ID를 나타내는 "id" 클레임에 주어진 사용자 ID 값을 설정한다.
    • then
      반환 받은 유저 ID가 given 절에서 설정한 유저 ID값인 1과 같은지 확인한다.



해당 글은 다음 도서의 내용을 정리하고 참고한 글임을 밝힙니다.
신선영, ⌜스프링 부트 3 벡엔드 개발자 되기 - 자바 편⌟, 골든래빗(주), 2023, 384쪽
profile
IT, 개발 관련 정보들을 기록하는 장소입니다.
post-custom-banner

0개의 댓글