사이먼's 코딩노트
[SpringBoot] JWT 토큰 발급 본문
[JWT 토큰 발급]
- SpringBoot를 통해 JWT 인증 구현 코드를 구현하기 전에 다시 한번 JWT 인증의 내용을 정리해봅시다.
- 서버가 최초로 접속하는 브라우저에게 쿠키의 세션 키 정보를 담는 방식으로 각각의 사용자를 구분한다.
- 초창기 어플리케이션은 쿠키 기능이 없어 ID와 PW를 저장해두었다가, 모든 요청에 한 번에 응답했다.
- 서버에서 신원확인용 토큰을 발급하여, 앱에서 사용하도록하면 해킹 발생 시에 PW를 바꾸지 않아도 된다.
- JWT 토큰은 정보를 담을 수 있고, 유효성 체크에 DB 조회를 수반하지 않는다.
- JWT 토큰을 만들 때 Secret key로 흔적을 남기고, JWT 토큰에서 정보를 얻을 때, Secret key로 그것이 오명되지 않았음을 인증한다.
- JWT 토큰은 한 번 만들면 만료시킬 수 없고, 그 안의 정보를 갱신할 수도 없다.
- 구현된 코드는 제 깃허브 리포티터리에서 모두 확인하실 수 있습니다.
- 리포지터리 URL 주소 : https://github.com/psm817/jwt_review
[초기 세팅 및 secret key 생성]
- 먼저, Spring Initializr를 통해 프로젝트를 생성하는데, 필자는 일단 Dependencies에 Spring Web, Dev Tools, Lombok만 포함하였다.
- 프로젝트 생성 완료 후 개발환경을 통해 OPEN을 하면 application.yml에 secret key를 생성한다.
- 원래는 application-secret.yml을 새로 생성하여 해당 값을 숨기는 절차가 맞지만, 테스트이기 때문에 일단 application.yml에 아래와 같이 secret key을 사용자 임의로 설정해준다.
custom:
jwt:
secretKey: alskdjvocijioq23450lckjvclxzk3430415lkzdlkjacv043121235lkjdazdflv3papopw
- 작성된 secret key가 실제로 존재하는지 테스트하기 위해서는 TestApplication에서 아래와 같이 assertThat을 이용한 코드를 작성한 후 오류없이 실행이 잘되는지 확인한다.
@SpringBootTest
class JwtReviewApplicationTests {
@Value("${custom.jwt.secretKey}")
private String secretKey;
@Test
@DisplayName("시크릿 키 존재 여부 체크")
void test1() {
assertThat(secretKey).isNotNull();
}
}
[JWT 관련 라이브러리 추가]
- 1차 테스트가 완료되었다면 build.gradle에 JWT와 관련된 라이브러리를 추가하여 Load를 해준다.
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
- 라이브러리 추가가 완료되었다면, 이번에는 TestApplication을 통해서 secret key를 통해 HMAC 암호화 방식에 맞는 secret key 객체를 만들어보는 테스트를 진행한다.
@SpringBootTest
class JwtReviewApplicationTests {
@Value("${custom.jwt.secretKey}")
private String secretKeyPlain;
@Test
@DisplayName("시크릿 키 존재 여부 체크")
void test1() {
assertThat(secretKeyPlain).isNotNull();
}
@Test
@DisplayName("시크릿 키를 이용하여 암호화 알고리즘 SecretKey 객체 만들기")
void test2() {
String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
SecretKey secretKey = Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
assertThat(secretKey).isNotNull();
}
}
- 위 코드는 application.yml에 작성한 secretKey 원문을 64비트로 인코딩한 후에 HMAC 키를 이용해서 다시 해시 값으로 만들고 해당 객체가 존재하는지 테스트를 구현한 코드이다.
[JwtProvider 생성]
- 이번에는 JWT 토큰을 생성해주는 역할을 하는 JwtProvider 클래스를 생성해준다.
- 2차 테스트에서 구현한 코드를 활용하여 JwtProvider.java 클래스에서 cachedSecretKey라는 변수에 암호화된 알고리즘 키를 가져오도록 한다.
- 아래는 JwtProvider.java 클래스를 새로 생성한 뒤 작성된 코드이다.
@Component
public class JwtProvider {
private SecretKey cachedSecretKey;
@Value("${custom.jwt.secretKey}")
private String secretKeyPlain;
private SecretKey _getSecretKey() {
// 64비트로 인코딩
String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
// HMAC 키로 암호화 객체 생성
return Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
}
public SecretKey getSecretKey() {
if (cachedSecretKey == null) cachedSecretKey = _getSecretKey();
return cachedSecretKey;
}
}
- JwtProvider을 생성했다면 다시 한번 TestApplication을 통해 getSecretKey() 메서드가 제대로 동작하는 지 확인하고, 여러개의 secretKey 객체를 생성했을 때 한 번만 처리되는지 확인한다.
@Test
@DisplayName("JwtProvider 객체를 활용하여 SecretKey 객체 생성")
void test3() {
SecretKey secretKey = jwtProvider.getSecretKey();
assertThat(secretKey).isNotNull();
System.out.println(secretKey);
}
@Test
@DisplayName("SecretKey 객체 생성을 한 번만 하도록 처리")
void test4() {
SecretKey secretKey1 = jwtProvider.getSecretKey();
SecretKey secretKey2 = jwtProvider.getSecretKey();
assertThat(secretKey1 == secretKey2).isTrue();
}
[Access Token 발급]
- 이제 본격적으로 토큰을 발급하기 위해서 Util이라는 클래스를 하나 생성하여 아래와 같이 코드 작성을 해준다.
public class Util {
public static class json {
public static Object toStr(Map<String, Object> map) {
try {
return new ObjectMapper().writeValueAsString(map);
} catch (JsonProcessingException e) {
return null;
}
}
}
}
- 위 코드는 암호화된 토큰을 JSON 형태로 바꿔주거나, 기존 배열 값을 String으로 변경하기 위한 메서드이다.
- 간단히 말해서 토큰을 가공해주는 역할을 Util.java에서 진행한다.
- Util.java 클래스 작성이 끝났다면 JwtProvider에 실제로 access token을 발급하는 genToken() 메서드를 아래와 같이 작성해준다.
public String genToken(Map<String, Object> claims, int seconds) {
long now = new Date().getTime();
Date accessTokenExpiresIn = new Date(now + 1000L * seconds);
return Jwts.builder()
.claim("body", Util.json.toStr(claims))
.setExpiration(accessTokenExpiresIn)
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
.compact();
}
- now 변수를 통해 날짜를 가져오고, accessTokenExpiresIn 변수를 통해 토큰의 유효 기간을 생성한다.
- return 값에서 .claim("body", Util.json.toStr(claims))을 통해 데이터를 Util에서 가공한 다음 Jwts에 생성한다.
- 모든 코드 작성이 완료되었다면 TestApplication에서 테스트로 토큰이 발급이 되는지 아래 코드 작성과 함께 확인한다.
@Test
@DisplayName("access Token 발급")
void test5() {
Map<String, Object> claims = new HashMap<>(); // claims는 Token에 payload에 저장되는 정보
claims.put("id", 2L);
claims.put("username", "psm817");
String accessToken = jwtProvider.genToken(claims, 60 * 60 * 5);
System.out.println("accessToken : " + accessToken);
assertThat(accessToken).isNotNull();
}
- 해당 코드를 실행하면 builder를 통해 실제로 access token이 발급되는 것을 확인할 수 있고, JWT Debugger 홈페이지(https://token.dev/)에 가서 발급된 토큰을 작성해보면 코드에서 작성한 id와 username 정보를 확인할 수 있다.
[Access Token 인증 및 claims 얻기]
- Access Token 발급에 성공하였다면, 마지막으로 생성된 토큰이 유효한지 아닌지 판단해보고, TestApplication에서 작성한 id와 username이 암호화에 성공한 데이터 정보를 가져올 수 있도록 확인해봅시다.
- 먼저 JwtProvider.java 클래스에서 아래와 같이 코드를 추가한다.
// 토큰이 유효한지 아닌지 판단
public boolean verify(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token);
} catch (Exception e) {
return false;
}
return true;
}
public Map<String, Object> getClaims(String token) {
String body = Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody()
.get("body", String.class);
return Util.toMap(body);
}
- getCliams() 메서드에서 return 값에 작성된 Util 클래스의 toMap() 메서드는 아래와 같이 코드를 작성하면 된다.
public static Map<String, Object> toMap(String jsonStr) {
try {
return new ObjectMapper().readValue(jsonStr, LinkedHashMap.class);
} catch (JsonProcessingException e) {
return null;
}
}
- 이제는 TestApplication을 통해 access token을 이용해서 claims 정보를 가져와보고, 만료된 토큰이 유효하지 않은지 테스트를 해봅시다.
@Test
@DisplayName("access Token을 이용하여 claims 정보 가져오기")
void test6() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1L);
claims.put("username", "admin");
// 10분
String accessToken = jwtProvider.genToken(claims, 60 * 10);
System.out.println("accessToken :" + accessToken);
assertThat(jwtProvider.verify(accessToken)).isTrue();
Map<String, Object> claimsFromToken = jwtProvider.getClaims(accessToken);
System.out.println("claimsFromToken : " + claimsFromToken);
}
@Test
@DisplayName("만료된 토큰이 유효하지 않은지")
void test7() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1L);
claims.put("username", "admin");
// 10분
String accessToken = jwtProvider.genToken(claims, 60 * 10);
System.out.println("accessToken :" + accessToken);
assertThat(jwtProvider.verify(accessToken)).isFalse();
}
- test6 에서는 10분의 유효기간인 토큰을 발급받고, verify() 메서드를 통해 발급된 토큰이 유효한지 확인하고, getClaims() 메서드를 통해 암호화된 정보를 가져오도록 하였다.
- test7 에서는 토큰의 유효기간에 따라서 토큰이 유효하지 않은지 verify() 메서드를 통해 확인한다.
반응형
'Java > SpringBoot' 카테고리의 다른 글
[SpringBoot] REST API (2) (0) | 2024.06.16 |
---|---|
[SpringBoot] REST API (1) (0) | 2024.06.16 |
[SpringBoot] Cookie / Session / JWT (2) | 2024.06.15 |
[SpringBoot] 이메일 발송 (0) | 2024.05.28 |
[SpringBoot] 소셜 로그인(카카오톡) (0) | 2024.05.27 |