본문 바로가기

Development/Spring

[Spring] Spring Security + JWT: JwtTokenProvider

JwtTokenProvider 구현

  • JWT 기반 보안 시스템을 구현하기 위해 JWT 토큰을 생성하는 메서드가 필요하다.
  • 이를 위해 JwtTokenProvider라는 JWT 토큰을 생성하는 메서드를 단계 별로 구현하겠다.

1. secretKey 생성

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);

    private String secretKey = "secretKey";

    private final long tokenValidMillisecond = 1000L * 60 * 60; // Valid 1 hours

    @PostConstruct
    protected void init() {
        LOGGER.info("[init] JwtTokenProvider: Start init secretKey");
        System.out.println(secretKey);
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
        System.out.println(secretKey);
        LOGGER.info("[init] JwtTokenProvider: Finish init secretKey");
    }
  • 위 코드에서 init() 메서드는 토큰을 생성하기 위한 secretKey를 정의한다. 이 때 Base64 형식으로 인코딩한다.
  • 참고로 Base64 인코딩이란 이진 데이터를 텍스트 기반 시스템에서 사용할 수 있도록 변환 (예: 이메일, HTTP, HTML)하며, 데이터 손실 없이 안전하게 전송 및 저장하도록 해준다.
  • @PostConstruct 어노테이션은 해당 객체가 Bean 객체로 주입된 이후 수행되는 메서드를 가리킨다. JwtTokenProvider 클래스는@Component 어노테이션이 지정돼 있어 애플리케이션이 시작되면 Bean으로 자동 주입되는데, 그때 @PostConstruct 어노테이션이 지정된 init() 함수가 실행된다.

2. Token 생성 메서드 구현

public String createToken(String userUid, List<String> roles) {
        LOGGER.info("[createToken] Start to create Token");
        Claims claims = Jwts.claims().setSubject(userUid);
        claims.put("roles", roles);

        Date now = new Date();
        String token = Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + tokenValidMillisecond))
            .signWith(SignatureAlgorithm.HS256, secretKey) 
            .compact();

        LOGGER.info("[createToken] Complete to create Token");
        return token;
    }
  • Claims 객체는 토큰에 값을 삽입하기 위해 필요한 객체이다.
  • setSubject 메서드를 통해 sub 속성에 값은 User의 uid 값을 사용한다.
  • 위 코드에서는 "roles"라는 사용자의 권한을 확인할 수 있는 값을 따로 추가했다.
  • Jwts.builder() 메서드를 통해 token을 생성했다.
  • signWith는 암호화 알고리즘을 정의하며, secret key 값을 세팅한다.

Claims

  • Spring Security에서 클레임 객체는 인증된 사용자 또는 요청에 대한 추가 정보를 담는 데이터 구조이다. 
  • 리소스에 대한 접근 권한을 승인하고, 요청 처리 방법에 대한 결정을 내리고, 사용자 또는 요청에 대한 추가적 컨텍스트를 제공하는 데 사용될 수 있다.

주요 Claim 타입:

  • Subject: Claim의 사용자 또는 ID
  • Issuer: Claim을 발급한 엔티티
  • Audience: Claim의 수신자
  • Expiration Time: Claim이 . 더 이상 유효하지 않은 시간
  • Roles: 유저 또는 엔티티의 역할
  • Permissions: 유저 또는 엔티티에게 부여된 권한

3. 인증을 생성하는 메서드 구현

  public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "",
            userDetails.getAuthorities());
    }
  • 필터에서 인증이 성공했을 때 SecurityContextHolder에 저장하는 Authentication을 생성하는 메서드이다.
  • Authentication을 구현하는 편한 방법은 UsernamePasswordAuthenticationToken을 사용하는 것이다.
  • 위 코드에서 UserDetails는 SpringSecurity에서 제공하는 인터페이스이며, 이 링크에서 구현 예제를 확인할 수 있다.
  • UserDetails의 username은 각 사용자를 구분할 수 있는 ID를 의미한다.

UsernamePasswordAuthenticationToken의 상속 구조는 다음과 같다.

이미지 출처:&nbsp;https://matthung0807.blogspot.com/2019/10/spring-security-usernamepasswordauthent.html

4. Token에서 회원 정보 추출하는 메서드 구현

public String getUsername(String token) {
        String info = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody()
            .getSubject();
        return info;
    }
  • Jwts.parser()를 통해 secretKey를 설정하고, Claim을 추출해서 토큰을 생성할 때 넣었던 sub 값을 추출한다.

5. HTTP 헤더에서 Token 값 추출 메서드 구현

public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }
  • 이 메서드는 HttpServletRequest를 파라미터로 받아 헤더 값으로 전달된 'X-AUTH-TOKEN' 값을 가져와 리턴한다.
  • 클라이언트가 헤더를 통해 애플리케이션 서버로 JWT 토큰 값을 전달해야 정상적인 추철이 가능하다.
  • 헤더의 이름은 임의로 변경할 수 있다.

6. Token의 유효성 검사 메서드 구현

 public boolean validateToken(String token) {
        LOGGER.info("[validateToken] 토큰 유효 체크 시작");
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
  • 위 메서드에서는 Token을 전달받아 Claim의 유효기간을 체크하고 boolean 타입 값을 리턴하는 역할을 한다.

전체 코드 링크:
GITHUB

참고