💽 Backend/Spring Boot

💽 Spring Security 없이 JJWT로 JWT 인증 구현하기 (JAVA)

김비니 2025. 5. 9. 20:16

 

Spring-Boot Java

 

대중적으로 많이 쓰인다는 JJWT를 활용해서, JWT를 구현해 보겠습니다.

기존에 작성했던 이론을 기초 지식으로 알고 계시면 좋아요!

 

>> 이론 포스팅 주소 <<

https://devbini.tistory.com/24

 

JWT? Json Web Token이란?

들어가며안녕하세요, 개발자 비니입니다.웹 암호학의 시작이자, 웹 인증 시스템을 공부할 때가장 먼저 나오는 내용 중 하나인 JWT를 들고 왔습니다. 사실 사람들은 JWT를 사용할 줄만 알지, 실제

devbini.tistory.com


0. 들어가며

개발 환경은 다음과 같습니다.

  • JAVA 17
  • Spring Boot 3.4.5
  • jjwt 의존성 필요, gradle 기준
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
	runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5')

1. JwtUtil 클래스 생성 및 키 생성

JWT 토큰을 생성하고, 검증하는 JWT Utility 클래스를 만들 겁니다.

Spring Boot 프로젝트에서, "JwtUill" 클래스를 새로 만들어줍니다.

 

package org.devbini.jwtnonesecurity;

public class JwtUtil {

}

 

위의 블록과 같은 상태가 되었을 텐데,

우선 JWT 토큰을 생성하기 위한 메타 정보를 만들어줍시다.

 

우리가 만들어야 하는 정보는 다음과 같습니다. (필수)

이름 비고
Key JWT 토큰을 만들 때 사용하는 비밀키입니다.
Ex_Time 해당 토큰이 얼마나 유효할지 설정하는 변수입니다.

 

코드로 구현하면 간단합니다.

변경 없이 정적 변수로 사용하기 위해 static으로 만들어 주겠습니다.

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;

public class JwtUtil {
    private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS384);
    private static final long Ex_Time = 1000 * 60;
}
  • Keys.secretKeyFor : jjwt 라이브러리에서 지원하는 메서드입니다. 매개변수로 시그니처 알고리즘값이 들어가야 해요.
  • SignatureAlgorithm : JJWT에서 지원하는 암호화 알고리즘입니다. 
    위 예시에서는 HS384 해시를 사용하나, 다른 암호화 알고리즘도 많으니 원하시는 거로 테스트해 보시면 좋을 것 같아요.

Keys.secretKeyFor(SignatureAlgorithm.HS384) 를 수행하면 얻는 값 :

1회차 : javax.crypto.spec.SecretKeySpec@58833b2
2회차 : javax.crypto.spec.SecretKeySpec@fa77fa34
3회차 : javax.crypto.spec.SecretKeySpec@fa77f1cd
4회차 : javax.crypto.spec.SecretKeySpec@5882a06
...

 

위와 같이, 당연히 새로 실행을 하는 경우마다 새로운 값이 나옵니다.

하지만, 우리가 지금 만든 건 전역 정적 변수로 Key를 생성하였기 때문에

서버가 다시 켜 지는 게 아닌 이상, 동일한 비밀키가 유지됩니다.

 

이렇게 하였을 때 좋은 점!

  • 서버의 비밀키가 노출될 일이 없습니다. (보안성 증가)
  • 별도로 비밀키를 관리할 필요가 없습니다.

물론, 커스텀 비밀키를 사용할 수 없다는 것은 단점이 될 수 있어요.


2. jwt 토큰 생성 메서드 만들기

위에서 만든 토큰을 기반으로, JWT 토큰을 생성하는 메서드를 만들어 주겠습니다.

    public static String generateToken() {
        return Jwts.builder()
                .signWith(key)
                .compact();
    }
  • signWith(Key) : 아까 만들었던 암호화 비밀키(Key)로 토큰을 암호화합니다.
  • compact() : 문자열로 변환합니다.

 

정말 간단하죠, 위 JWT 생성 구조는 필수 구조입니다.

이제 위 토대를 기반으로 살을 붙이는 겁니다.

 

옵션으로 추가할 수 있는 메서드 중 자주 쓰이는 건 다음과 같아요.

  • setSubject(String) : 토큰에 주체 정보를 담습니다.
  • setIssuedAt(Date) : 토큰 발급 시간을 지정합니다.
  • setExpiration(Date) : 만료 시간을 지정합니다.

위에서 말한 주체는, 토큰에 넣으려는 정보를 말해요.

보통 복호화하여 유저 정보를 읽어오기 위한 정보를 넣으니, ID나 이름 정도가 되겠네요.

발급 시간과 만료시간은 자유입니다. 보통 권장되고 있으니, 적용해 주면 좋겠지요.

적용하게 되면 코드는 아래와 같이 변화합니다.

public static String generateToken(String id) {
    return Jwts.builder()
            .setSubject(id)
            .setExpiration(new Date(System.currentTimeMillis() + Ex_Time))
            .signWith(key)
            .compact();
}

 

위 코드는,

  • user의 이름을 주체로 담고,
  • 현재 시간으로부터 1분간 유효하는 토큰을
  • HS384로 암호화하여 발급하는 코드입니다.

이대로 실행하면 어떤 값이 나올까요?

generateToken("TEST")를 실행하면 얻는 값 :

1회차 : eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzQ2Nzg2NzY2fQ.c3X9r4SYYb_1QNvWXSs7Vu7K_GgZC6w6Jac8cgMsjVFzvI-El3jjpnZxQjt2mokW
2회차 : eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzQ2Nzg2ODU4fQ.EFzCgQ2yGlaOVZtR1vPl6DRaSq9vQX_uWzbeZBGwobeNCI3b_AUSHViQOnw8_Zn-
3회차 : eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzQ2Nzg2ODc0fQ.YDffn0R_V7L_0xWEuxw5ZLvUihqV5RIwSO0M6ukQSyMML8tq-ysd6kG5vNmgTG-O
4회차 : eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzQ2Nzg2ODgyfQ.QY8gRo5-gs2iAOPY0Kuyzz95ahGdWlgsgrr0EjtqCzMkrJ7GskCVdPVSJpyToxbP
...

 

보면, 기존 이론 파트에서 다룬 것과 같이

토큰의 시그니처만 변화하는 것을 알 수 있습니다.

 

자, 이제 토큰을 검증해 봅시다.


3. JWT 토큰 검증 메서드 만들기

요청이 올 때마다 이 토큰이 유효한지 서버가 검증해야 합니다.
결론은, 토큰을 파싱(parse)해서 만료시간이 지나지 않았는지 체크해 볼게요.

    public static boolean validateToken(String token) {
        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);

            return true;
        } catch (JwtException | IllegalArgumentException e) {
        	// 만료!
        	return false;
        }
    }

 

JwtException이 발생하지 않으면, 해당 토큰은 정상이라 판단할 겁니다.

만약 위에서 Exception이 발생하면, 서명 위조/유효기간 만료/잘못된 토큰이란 뜻이므로 false를 반환합니다.

  • setSigningKey(Key) : 토큰 복호화에 어떤 비밀키를 사용할 것인지 설정합니다.
  • parseClaimsJws(token) : 토큰의 세 마디를 분리하여 정상적인지 검증하는 메서드입니다.
    만료가 되었거나, 서명이 다르거나, 구조가 다르거나 하는 등에 대해 예외를 발생시킵니다.

발생하는 예외 중 빈번하게 확인할 만한 건,

  • ExpiredJwtException : 만료
  • SignatureException : 위조 감지
  • MalformedJwtException : 구조 깨짐

이렇게 되겠네요, 어지간하면 JwtException에서 걸립니다.

차차 해당 예외들에 대한 글도 작성해 볼게요.


4. 토큰에서 정보 추출하기

위에서 검증했으니, 정상이다!라고 한다면 이제 데이터를 꺼내서 써야겠죠.
JJWT에서는 Claims 객체로 원하는 값을 불러올 수 있습니다.

import io.jsonwebtoken.Claims;

    public static String getSubject(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

 

 

getBody()는 JWT의 클레임(payload 부분)을 꺼내 쓸 수 있게 해 줍니다.

  • claims.getSubject()
  • claims.get("id")
  • claims.getIssuedAt()
  • claims.getExpiration()
  • ... payload에 정의된 모든 값

다른 건 알겠는데, claime.get("id")가 뭔가 생소하죠?

이건 토큰을 만들 때 아래와 같은 방법으로 만들 수 있어요.

...
.claim("id", "Value")
...

 

Jwts.builder 아래로 넣어주면 됩니다.

 

Map<String, Object> claims = new HashMap<>();
claims.put("testclam", "yourValue");
claims.put("role", "admin");
claims.put("foo", 123);

...
.setClaims(claims)
...

 

이런 식으로, 한 번에 많이 넣어 줄 수도 있습니다.

두 경우 모두 동일하게 claime.get("Key")로 읽어올 수 있어요.


 

생각보다 간단하지요,

Controller는 포함하지 않았습니다.

REST API 개발 블로그 포스팅에서 뵙겠습니다.

 

-- 컨트롤러가 포함된 풀 코드 링크 --

https://github.com/devbini/Blog/tree/main/jwt-none-security

 

Blog/jwt-none-security at main · devbini/Blog

Contribute to devbini/Blog development by creating an account on GitHub.

github.com

 

감사합니다.