
이 글은 스프링부트3 백엔드 개발자 되기 책을 바탕으로 공부한 내용을 정리한 게시글 입니다.
사용자의 인증정보를 확인하기 위한 방법에는 세션 쿠기 기반 인증 방법과 토큰기반 인증과정이 있습니다. 세션쿠키 기반 인증방법은 서버측에서 인증정보를 저장하기 때문에 요청을 할 때마다 DB를 거치는 과정을 겪어야 한다는 단점이 있습니다. 이런 단점을 보안하기 위해 나온게 토큰 기반 인증입니다.
토큰 기반 인증 에서 주요 사용하는 방법으로는 JWT(Json Web Token)이 있습니다. JWT에 대해 SpringBoot와 Spring Security를 기반으로 직접 구현해 보도록 하겠습니다.
1. 토큰 제공자 추가하기
먼저 JWT를 이용하기 위해 의존성을 추가하겠습니다. build.gradle에 의존성을 추가하고 새로고침을 눌러 의존성을 다운로드 해줍니다.
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
이제 jwt를 사용해서 JWT를 생성하고 유효한 토큰인지 검증하는 역할을 하는 클래스를 추가하겠습니다.
먼저 JWT토큰을 만들려면 이슈발급자와 비밀키를 필수로 설정해야 합니다. application.yml에 jwt의 issuer과 secret_key를 추가해 줍니다.
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: ajufresh@gmail.com // 발급자 설정
secret_key: study-springboot // 비밀키 설정
이제 설정한 발급자와 비밀키를 변수로 접근하는 데 사용할 JwtProperties 클래스를 만들겠습니다.
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer;
private String secretKey;
}
@ConfigurationProperties("jwt") 에너테이션을 통해 자바 클래스에 프로피티 값을 가져와서 사용할 수 있습니다. 이 에너테이션을 통해 위에서 설정하였던 application.yml에 특정 접두사(jwt)로 시작하는 설정 값을 자바 객체로 바인딩 할 수 있게 해줍니다. 이를 통해 issuer필드에는 application.yml에서 설정한 jwt.issuer값이 secretKey는 jwt.secret_key 값이 매핑이 됩니다.
다음은 계속해서 토큰을 생성하고 올바른 토큰인지 유효성 검사를 하고 토큰에서 필요한 정보를 가져오는 클래스를 작성합니다. TokenProvider.java파일을 만들고 다음과 같이 로직을 작성하겠습니다.
@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);
}
// 토큰 생성 메서드
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 typ : JWT
// 내용 iss : propertise 파일에서 설정한 값(ajufresh@gmail.com)
.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();
}
// JWT 토큰 유효성 검증 메서드
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) // 비밀값으로 복호화
.parseClaimsJws(token);
return true;
} catch (Exception e) { // 복호화 과정에서 에러가 나면 유효하지 않은 토큰
return false;
}
}
// 토큰 기반으로 인증 정보를 가져오는 메서드
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);
}
// 토큰 기반으로 유저 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();
}
}
makeToken() 메서드는 토큰을 만드는 메서드 입니다. 인자는 만료 시간, 유저 정보를 받습니다. 이 메서드에서 set계열의 메서드를 이용하여 여러 값을 정해줍니다. 헤더는 typ(타입), 내용은 iss(발급자), iat(발급일시), exp(만료 일시), sub(토큰 제목)이 클레임에는 유저ID를 지정합니다. 토큰을 만들 때는 프로퍼티즈 파일에 선언해둔 비밀 값과 함께 HS256방식으로 암호화 합니다.
validToken()메서드 입니다. 토큰이 유효한지 검증하는 메서드 입니다. propertise 파일에 선언한 비밀값과 함께 토큰 복호화를 진행합니다. try catch() 문을 통해 복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로 false를 반환합니다. 토큰 복호화 과정이 성공하면 유효한 토큰이므로 true를 반환합니다.
getAuthetication()메서드 입니다. 토큰을 받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드 입니다. 프로퍼티즈 파일에 저장된 비밀값으로 토큰을 복호화 한 뒤 클레임을 가져오는 private 메서드인 getClaims()를 호출하여 클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성합니다. 이때 UsernamePasswordAuthenticationToken의 첫 인자로 들어가는 User는 프로젝트에서 만든 User클래스가 아닌 스프링 시큐리티에서 제공하는 객체인 User클래스를 임포트 해야합니다.
getUserId()메서드는 토큰기반으로 사용자 ID를 가져오는 메서드 입니다. 프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화 한 뒤 클레임을 가져오는 private메서드인 getClaims()를 호출하여 클레임 정보를 반환받고 클레임에서 id로 저장된 값을 가져와 반환합니다.
이렇게 토큰을 생성하고 유효한 토큰인지 확인하고 인증을 처리하는 로직을 작성하였습니다. 이제 이 로직이 제대로 작동하는지 확인하기 위해 테스트 코드를 작성하겠습니다.
2. 테스트 코드 작성하기
test디렉터리에 JwtFactory파일을 생성하여 테스트 코드를 작성하겠습니다.
@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();
}
}
먼저 JWT의 주체를 설정해줍니다. 그 뒤 new Date()를 사용하여 현재시간으로 토큰 발급시간을 설정해주고 토큰 만료 시간을 14일 후에 만료 되도록 설정해 줍니다.
JWT의 추가적인 클레임(데이터 저장용 필드)을 탐을 수 있는 Map을 선언해준뒤 기본값으로 emptyMap()(비어있는 Map)으로 설정해 줍니다.
@Builder를 이용하여 설정에 필요한 데이터만 선택 설정해 줍니다.
createToken(JwtProperties jwtProperties)메서드는 JWT를 생성하는 역할을 합니다.
Jwts.builder()를 사용ㅇ하여 JWT의 구성요소들을 설정해 줍니다.
이제 TokenProvider 클래스를 테스트하는 클래스를 만들겠습니다. JwtProviderTest.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();
}
@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);
}
}
generate Token() 메서드는 토큰을 생성하는 메서드를 테스트하는 메서드 입니다.
1. given: 토큰에 유저 정보를 추가하기 위한 테스트 유저를 만듭니다. User객체를 생성 한 뒤 userRepository.save()를 이용하여 DB에 저장을 합니다.
2. when: 토큰 제공자의 generateToken()메서드를 호출하여 토큰을 만듭니다.
3.then: JWT 내부의 ID값을 검증하기 위해 jjwt 라이브러리를 사용하여 토큰을 복호화 합니다. 트큰을 만들 때 클레임으로 넣어두었던 id 값이 given 절에서 만든 유저ID값과 동일한지 확인 합니다.
validToken_invalidToken() 메서드는 토큰이 유효한 토큰인지 검증하는 메서드인 validToken()메서드를 테스트 하는 메서드 입니다. 만료된 JWT가 유효하지 않음을 검증하여 검증 실패를 확인하는 vaildToken_invalidToken()메서드와 검증 성공을 확인하는 validToken_validToken()메서드가 있습니다.
[ vaildToken_invalidToken() ]
1.given: jjwt라이브러리를 사용해 토큰을 생성합니다. 이때 만료 시간은 현재시간에 이전으로 설정하여 이미 만료된 토큰으로 생성합니다.
2.when: 토큰 제공자의 validToken() 메서드를 호출하여 유효한 토큰인지 검증하여 JWT의 유효성을 검사합니다.
3.then: 만료된 토큰이므로 반환값이 false인지 확인합니다.
[ validToken_validToken() ]
1.given: jjwt라이브러리를 사용하여 토큰을 생성합니다. 이때 만료시간은 현재 시간으로부터 14일 뒤로 만료되지 않은 토큰으로 생성을 합니다.
2. when: 토큰 제공자의 validToken()메서드를 호출하여 유효한 토큰인지 검증한 뒤 결과값을 반환받습니다.
3.then: 유효한 토큰이므로 true를 반환한지 확인합니다.
getAuthentication() 메서드는 토큰을 전달받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드인 getAuthentication()을 테스트합니다.
1. given: jjwt 라이브러리를 사용해 토큰을 생성합니다. 이때 subject에 user@email.com을 설정해 줍니다.
2. when: 토큰 제공자의 getAuthentication() 메서드를 호출해 인증 객체를 반환받습니다.
3. then: 반환받은 인증 객체의 유저 이름을 authentication.getPrincipal()에서 UserDetails를 이용해 꺼내줍니다. 그 뒤 user@email.com과 비교해 같은지 확인합니다.
getUserId() 메서드는 토큰기반의 유저ID를 가져오는 메서드를 테스트 하는 메서드 입니다. 토큰을 프로퍼티즈 파일에 저장한 비밀값으로 복호화 한 뒤 클레임을 가져오는 private메서드인 getClaims()를 호출하여 클레임 정보를 반환받아 클레임에서 id키로 저장된 값을 가져와 반환합니다.
1.given : jjwt 라이브러리를 사용해 토큰을 생성합니다. 이때 클레임을 추가 합니다. 키는 "id" 값은 1이라는 유저 ID 입니다.
2. when : 토큰 제공자의 getUserID()메서드를 호출해 유저ID를 반환받습니다.
3.then: 반환받은 유저ID가 given절에서 설정한 유저ID값인 1과 같은 지 확인합니다.
이렇게 given-when-then절을 이용하여 테스트코드를 작성하였습니다. jwt를 이용하여 스프링시큐리티 기반의 토큰 인증방식을 사용하여 사용자의 인증을 처리하는 로직들을 작성하였습니다. jwt를 이용하면 보안적인 측면 뿐만 아니라 성능적인 측면에서도 기존의 방식과 비교하여 더 좋은 효과를 얻을 수 있습니다.
'개발자 취업준비 > springboot' 카테고리의 다른 글
| OAuth2.0 이해하기 (0) | 2025.04.03 |
|---|---|
| JWT 알아보기 (0) | 2025.03.30 |
| 스프링 시큐리티 실전 적용하기2 회원가입 구현하기 (0) | 2025.03.25 |
| 스프링 시큐리티 실전 적용하기1 스프링시큐리티 설정 (0) | 2025.03.25 |
| 스프링시큐리티 (0) | 2025.03.11 |