// 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);
}
}
- 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키로 저장된 값을 가져와 반환한다.
- .claims(Map.of("id", userId))
생성된 빌더를 사용하여 클레임(claims)을 설정한다. 사용자 ID를 나타내는 "id" 클레임에 주어진 사용자 ID 값을 설정한다.