build.gradle
* jjwt 라이브러리 0.11.5버전 기준
* jjwt설명 참고 링크 ( https://radpro.tistory.com/307 )
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// 여기부터 추가 ----------------------------------------------------------
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
JwtTokenizer.class
1. SecretKey 인코더 : 사용자가 입력한 비밀키를 가지고 BASE64에 맞춤
public String encodeBase64SecretKey(String secretKey) {
return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
2. SecretKey 디코더 : Base64에 맞춰 인코딩된 secretKey 디코딩 후 리턴해줌. 토큰 생성 싸인용
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); // Base64 형식으로 인코딩 된 Secret Key를 디코딩 한 후, byte array를 반환
Key key = Keys.hmacShaKeyFor(keyBytes); // 적절한 HMAC 알고리즘을 적용한 java.security.Key 객체 생성
// jjwt 0.9.x 버전에서는 서명 과정에서 HMAC 알고리즘을 직접 지정해야 했지만, 최신 버전에서는 내부적으로 적절한 HMAC 알고리즘을 지정해 준다
return key;
}
3. AccessTokenizer : 파라미터로는 내용(Claims), 제목(Header), 만료일, 인코딩한 비밀키값
public String generateAccessToken(Map<String, Object> claims, // Custom Claims(내용물).
String subject, // 헤더부분
Date expiration, // 만료일 (java.util.Date 타입)
String base64EncodedSecretKey) { //(1)로 인코딩한 비밀키
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); // (1)로 인코딩해서 저장했던 키값을 디코딩해서 가져오는 부분
return Jwts.builder() // JWT 빌드 시작
.setClaims(claims) // 내용넣어(Custom claims. 주로 인증된 사용자와 관련된 정보 추가)
.setSubject(subject) // 헤더넣어
.setIssuedAt(Calendar.getInstance().getTime()) // 작성일 달력에서 뽑아와 넣어 (java.util.Date 타입)
.setExpiration(expiration) // 만료일넣어 (java.util.Date 타입)
.signWith(key) // 디코더로 받아온 키값으로 싸인해 ( java.security.Key 객체)
.compact(); // JWT 생성 및 직렬화
}
4. RefreshTokenizer : 어차피 AccessToken 만기 갱신용이라, 내용(Claims)은 없어도 됨
public String generateRefreshToken(String subject, // 헤더
Date expiration, // 만기일
String base64EncodedSecretKey) { // 비밀키
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(expiration)
.signWith(key)
.compact();
}
JwtTokenizerTest.class
: TestCase작성. 반드시 test 디렉토리에서 작성할 것.(@Test 애너테이션은 Test에서만 작동하기때문)
1. 사용 라이브러리
package com.example.jwtTest.auth;
import io.jsonwebtoken.io.Decoders;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import java.util.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
2. TestInstance : 테스트별 단 하나의 인스턴스 생성 + @BeforeEash나 @BeforeAll 필요 ( 참고링크 )
- 개별 테스트의 독립성을 보장하고, 테스트 사이의 상호관계에서 발생하는 부작용 방지를 위한 애너테이션
- 개별 테스트 메서드의 실행 전 새로운 인스턴스를 생성한다.
- 만약 모든 테스트 메서드를 동일한 인스턴스 환경에서 동작시키고 싶다면, @TestInstance
- 인스턴스를 클래스 단위로 생명주기를 주고 싶을 땐, @TestInstance(Lifecycle.PER_CLASS)
혹은 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- 단, 해당 클래스가 인스턴스 속성을 가진다면, @BeforeEach나 @BeforeAll로 초기화 필요
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JwtTokenizerTest {
...
}
3. 필드선언 및 초기화 선언
: 현재 비밀키 인코더는 JWT HMAC-SHA 알고리즘을 사용하기 때문에, Security문제로 반드시 256bits 이상의 사이즈를 가져야한다. (오류 해결 링크)
private static JwtTokenizer jwtTokenizer;
private String secretKey;
private String base64EncodedSecretKey;
// 초기 세팅 : jwtTokenizer 생성자 생성, 스텁 비밀키 선언, 스텁 비밀키로 인코딩 객체 생성
@BeforeAll
public void init() {
jwtTokenizer = new JwtTokenizer();
secretKey = "nyong1234123412341234123412341234";
base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(secretKey);
}
4. 인코더 테스트 케이스
@Test
public void encodeBase64SecretKeyTest() {
// given
// when
System.out.println(base64EncodedSecretKey); // 인코딩 잘 됬나 확인용. 꼭 여기 아니어도 됨
// then
assertThat(secretKey, is(new String(Decoders.BASE64.decode(base64EncodedSecretKey))));
}
5. AccessTokenizer 테스트 케이스
@Test
public void generateAccessTokenTest() {
// given
// stub 파라미터
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", 1);
claims.put("roles", List.of("USER"));
String subject = "test access token";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 10);
Date expiration = calendar.getTime();
// stub Access 토큰 생성
String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);
// when
System.out.println(accessToken);
// then
assertThat(accessToken, notNullValue());
}
6. RefreshTokenizer 테스트 케이스
@Test
public void generateRefreshTokenTest () {
// given
// stub 파라미터
String subject = "test refresh token";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, 24);
Date expiration = calendar.getTime();
// stub Refresh 토큰 생성
String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
// when
System.out.println(refreshToken);
// then
assertThat(refreshToken, notNullValue());
}
7. 검증 :
Access 토큰 획득 메서드
private String getAccessToken(int timeUnit, int timeAmount) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId",1);
claims.put("roles", List.of("USER"));
String subject = "test access token";
Calendar calendar = Calendar.getInstance();
calendar.add(timeUnit, timeAmount);
Date expiration = calendar.getTime();
String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);
return accessToken;
}
1) 생성된 JWT를 verifySignature()로 전달해서 Exception이 발생하지 않으면 검증이 잘 수행되는 것
// Signature 테스트
@DisplayName("does not throw any Exception when jws verify")
@Test
public void verifySignatureTest() {
// given
String accessToken = getAccessToken(Calendar.MINUTE,10);
// when
// then
assertDoesNotThrow(()->jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
}
2) JWT 생성시 지정한 만료일시가 지나면 JWT가 정말 만료되는지 확인
// Expiration 테스트
@DisplayName("throw ExpirationJwtException when jws verify")
@Test
public void verifyExpiationTest() throws InterruptedException {
// given
String accessToken = getAccessToken(Calendar.SECOND, 1);
// when
assertDoesNotThrow(() -> jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
// then
TimeUnit.MILLISECONDS.sleep(1500);
assertThrows(ExpiredJwtException.class, ()->jwtTokenizer.verifySignature(accessToken, base64EncodedSecretKey));
}'Codestates [Back-end] > 데일리 로그 [TIL]' 카테고리의 다른 글
| 22.09.27 JWT - SpringSecurity 인증 (step 2. 로그인 인증 구현) [진행중] (0) | 2022.09.27 |
|---|---|
| 22.09.27 JWT - SpringSecurity 인증 (step 1. 사전 작업) (2) | 2022.09.27 |
| 22.09.26 JWT 이론 (0) | 2022.09.26 |
| 22.09.23 SpringSecutiry - DelegatingPasswordEncoder (0) | 2022.09.23 |
| 22.09.23 SpringSecurity - 인가 (0) | 2022.09.23 |