반응형
<이전 글 - JWT란?>
1. JWT 서비스 구현하기
실제로 JWT를 생성하고, 검증하는 서비스를 구현할 것이다.
○ 의존성 추가
↓
○ 토큰 제공자를 추가
↓
○ 리프레시 토큰 도메인 구현
↓
○ 토큰 필터 구현
1-1. 의존성 추가하기
build.gradle
dependencies {
--- 생략 ---
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.projectlombok:lombok'
implementation 'io.jsonwebtoken:jjwt:0.9.1' // 자바 JWT 라이브러리
implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML 문서와 Java 객체 간 매핑 자동화
}
자바에서 JWT를 사용하기 위한 라이브러리를 추가하고,
XML 문서와 자바 객체 간 매핑을 자동화하는 jax-api를 추가한다.
1-2. 토큰 제공자 추가하기
jwt를 사용해서 JWT를 생성하고 유효한 토큰인지 검증하는 역할을 하는 클래스를 추가할 것이다.
◆ application.yml
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
defer-datasource-initialization: true
datasource:
url: jdbc:h2:mem:testdb
username: sa
h2:
console:
enabled: true
jwt:
issuer: your_email
secret_key: study-springboot
JWT 토큰을 만들려면 이슈 발급자(issuer), 비밀키(secret_key)를 필수로 설정해야 한다.
이 값들을 선언한 설정파일을 수정한다
- your_email >> 이메일 주소를 입력한다.
◆ /config/jwt/JwtProperties.java
@Setter
@Getter
@Component
@ConfigurationProperties("jwt") // 자바 클래스에 프로피티값을 가져와서 사용하는 애너테이션
public class JwtProperties {
private String issuer;
private String secretKey;
}
- issuer
- secretKey
해당 값들을 변수로 접근하는 데 사용할 JwtProperties 클래스를 만든다.
◆ /config/jwt/TokenProvider.java
package me.jhzlo.springbootdeveloper.config.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import me.jhzlo.springbootdeveloper.domain.User;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user, Duration expiredAt){
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
// 1. JWT 토큰 생성 메서드
private String makeToken(Date expiry, User user){
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 typ: JWT
// 내용 iss : ajufresh@gamil.com(propertise 파일에서 설정한 값)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now) // 내용 iat : 현재 시간
.setExpiration(expiry) // 내용 exp : expiry 멤버 변숫값
.setSubject(user.getEmail()) // 내용 sub : 유저의 이메일
.claim("id", user.getId()) // 클레임 id : 유저 ID
// 서명 : 비밀값과 함께 해시값을 HS256 방식으로 암호화
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
// 2. JWT 토큰 유효성 검증 메서드
public boolean validToken(String token){
try{
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) // 비밀값으로 복호화
.parseClaimsJws(token);
return true;
} catch (Exception e){ // 복호화 과정에서 에러가 나면 유효하지 않은 토큰
return false;
}
}
// 3. 토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token){
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(new org.springframework.security.
core.userdetails.User(claims.getSubject(), "", authorities), token, authorities);
}
// 4. 토큰 기반으로 유저 ID를 가져오는 메서드
public Long getUserId(String token){
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
private Claims getClaims(String token){
return Jwts.parser() // 클레임 조회
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
- private String makeToken(Date expiry, User user)
"토큰을 생성하는 메서드"
인자는 만료 시간, 유저 정보를 받는다.
이 메서드에서는 set 계열의 메서드를 통해 여러 값을 지정한다.
헤더는 typ(타입),
내용은 iss(발급자), iat(발급일시), exp(만료일시), sub(토큰 제목)이,
클레임에는 유저 ID를 지정한다.
토큰을 만들 때는 프로퍼티즈 파일에 선언해둔 비밀값과 함께 HS 256방식으로 암호화한다. - public boolean validToken(String token)
"토큰이 유효한지 검증하는 메서드"
프로퍼티즈 파일에 선언한 비밀값과 함께 토큰 복호화를 진행한다.
만약 복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로 false를 반환하고,
아무 에러도 발생하지 않으면 true를 반환한다. - public Authentication getAuthentication(String token)
"토큰을 받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드"
프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 뒤
클레임을 가져오는 private 메서드인 getClaimes()를 호출해서 클레임 정보를 반환받아
사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성한다.
이때 UsernamePasswordAuthenticationToken의 첫 인자로 들어가는 User는
프로젝트에서 만든 User 클래스가 아닌, 스프링 시큐리티에서 제공하는 객체인 User 클래스를 임포트 해야한다. - public Long getUserId(String token)
"토큰 기반으로 사용자 ID를 가져오는 메서드"
프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 다음
클레임을 가져오는 pravate 메서드인 getClaims()를 호출해서 클레임 정보를 반환받고
클레임에서 id 키로 저장된 값을 가져와 반환한다.
테스트 코드 작성
test 디렉터리에 config.jwt 패키지를 만들고 JwtFactory.java 파일을 생성한다.
◆ 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();
}
// jjwt 라이브러리를 사용해 JWT 토큰 생성
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();
}
}
빌더 패턴을 사용해 객체를 만들 때 테스트가 필요한 데이터만 선택한다.
빌더 패턴을 사용하지 않으면 필드 기본값을 사용한다.
◆ TokenProvider.java
@SpringBootTest
public class TokenProviderTest {
@Autowired
private TokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@Autowired
private JwtProperties jwtProperties;
// 1. generateToken() 검증 테스트
@DisplayName("generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다.")
@Test
void generateToken(){
// given
User testUser = userRepository.save(User.builder()
.email("user@email.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());
}
// 2. 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();
}
@DisplayName("validToken(): 유효한 토큰인 때에 유효성 검증에 성공한다.")
@Test
void validToken_validToken(){
// given
String token = JwtFactory.withDefaultValues().createToken(jwtProperties);
// when
boolean result = tokenProvider.validToken(token);
// then
assertThat(result).isTrue();
}
// 3. 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);
}
// 4. 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);
}
}
1. generateToken() 메서드
given | 토큰에 유저 정보를 추가하기 위한 테스트 유저를 만든다. |
when | 토큰 제공자의 generateToken() 메서드를 호출해 토큰을 만든다. |
then | jjwt 라이브러리를 사용해 토큰을 복호화 한다. 토큰을 만들 때, 클레임으로 넣어둔 id값이 given 절에서 만든 유저 ID와 동일한지 확인한다. |
2. validToken_invalidToken() 메서드
invalidToken()
given | jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 만료 시간은 1970년 1월 1일부터 현재 시간을 밀리초 단위로 치환한 값에 1000을 빼, 이미 만료된 토큰으로 생성한다. |
when | 토큰 제공자의 validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결괏값을 반환 받는다. |
then | 반환값이 false(유효한 토큰이 아님)인 것을 확인한다. |
validToken()
given | jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 만료 시간은 현재 시간으로부터 14일 뒤로, 만료되지 않은 토큰으로 생성한다. |
when | 토큰 제공자의 validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결괏값을 반환 받는다. |
then | 반환값이 true(유효한 토큰임)인 것을 확인한다. |
3. getAuthentication() 메서드
given | jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 토큰의 제목인 subject는 "user@email.com:라는 값을 사용한다. |
when | 토큰 제공자의 getAuthentication () 메서드를 호출해 인증 객체를 반환 받는다. |
then | 반환받은 인증 객체의 유저 이름을 가져와 given절에서 설정한 subject값인 "user@email.com:과 같은지 확인한다. |
4. getUserId() 메서드
given | jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 클레임을 추가한다. 키는 "id", 값은 1이라는 유저 ID이다. |
when | 토큰 제공자의 getUserId () 메서드를 호출해 유저 ID를 반환 받는다. |
then | 반환받은 유저 ID가 given절에서 설정한 유저 ID값인 1과 같은지 확인한다. |
실행시키면 위와 같이 테스트가 모두 성공함을 확인할 수 있다.
반응형
'JAVA > SpringBoot 3' 카테고리의 다른 글
[스프링 부트 3] OAuth2 - 1 (OAuth란?, 권한 부여 코드 타입 승인의 흐름, 쿠키란?, 토큰 발급받기) (0) | 2024.08.23 |
---|---|
[스프링 부트 3] JWT - 3 (토큰 필터, 리프레시 토큰 API 구현하기, 로그인/로그아웃) (0) | 2024.08.22 |
[스프링 부트 3] JWT - 1 (토큰 기반 인증, JWT, 액세스 토큰, 리프레시 토큰) (0) | 2024.08.18 |
[스프링 부트 3] 스프링 시큐리티 - 2 (로그인/로그아웃, 회원가입 구현하기) (0) | 2024.08.16 |
[스프링 부트 3] 스프링 시큐리티 - 1 (스프링 시큐리티, 인증과 인가 ) (0) | 2024.08.14 |