네이버 로그인 API를 활용한 소셜로그인 구현 과정(with. Spring Security, JWT) - 2. Jwt class 생성

Sia Hwang·2022년 11월 28일
2

이제 본격적으로 비즈니스 로직을 작성할 것이다.
모든 코드는 프로그래머스 웹 백엔드 스터디 강의 중 받았던 샘플 코드를 바탕으로 작성되었다.

Package structure in my application

package com.august.soil.api.xxx;
  • 내 애플리케이션의 패키지 구조는 위와 같은 형태이다.
  • august는 팀 이름, soil은 프로젝트 이름이다. 그 아래에 api 패키지를 생성했다. API 관련 비즈니스 코드들은 모두 api 아래에 생성되어 있다.

Create package 'security'

package com.august.soil.api.security;
  • 이런 경로로 import될 수 있게 security 패키지를 생성한다.
  • 이 패키지 아래에 JWTSpring Security 관련 로직들을 작성할 것이다.

Create class 'Jwt'

  • 방금 생성한 security 패키지 아래에 Jwt라는 이름으로 클래스를 생성한다.
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;

public class Jwt {

  private final String issuer;
  
  private final String clientSecret;
  
  private final int expirySeconds;

  private final Algorithm algorithm;

  private final JWTVerifier jwtVerifier;
}
  • 먼저 클래스 내에서 사용할 멤버변수들을 선언해 준다.

    클래스 내의 멤버변수를 선언할 때엔 기본적으로 final 키워드를 붙여 초기화 이후 변경이 불가능하게 만든다.
    이후 변경이 필요한 멤버변수가 있으면 그것만 제한적으로 final 키워드를 제거하고 setter를 만드는 형태로 코드를 작성하는 등 불필요한 상태 변화를 방지하도록 한다.

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;

public class Jwt {

  ...
  
  public Jwt(String issuer, String clientSecret, int expirySeconds) {
    this.issuer = issuer;
    this.clientSecret = clientSecret;
    this.expirySeconds = expirySeconds;
    this.algorithm = Algorithm.HMAC512(clientSecret);
    this.jwtVerifier = JWT.require(algorithm)
      .withIssuer(issuer)
      .build();
  }
}
  • 멤버변수에 모두 final 키워드를 붙여주었기 때문에 생성자를 통해서 초기화 해야한다. 의존성으로 롬복을 사용하기 때문에 @RequiredArgsContructor를 사용하면 생성자 코드를 일일이 작성할 필요는 없지만 이 클래스에서 일부 멤버변수는 암호화 처리와 함께 초기화 할 것이라서 따로 만들었다.
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.Getter;
import java.util.Date;

@Getter
public class Jwt {

  ...
  
  public String newToken(Claims claims) {
    Date now = new Date();
    JWTCreator.Builder builder = JWT.create();
    builder.withIssuer(issuer);
    builder.withIssuedAt(now);
    if (expirySeconds > 0) {
      builder.withExpiresAt(new Date(now.getTime() + expirySeconds * 1_000L));
    }
    builder.withClaim("userKey", claims.userKey);
    builder.withClaim("name", claims.name);
    builder.withClaim("email", claims.email.getEmail());
    builder.withArrayClaim("roles", claims.roles);
    return "Bearer " + builder.sign(algorithm);
  }
}
  • 새로운 토큰을 발급하는 로직도 작성한다.
  • getter는 롬복 어노테이션으로 해결했다.
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.Getter;
import java.util.Date;

@Getter
public class Jwt {

  ...
  
  public String refreshToken(String token) throws JWTVerificationException {
    Claims claims = verify(token);
    claims.eraseIat();
    claims.eraseExp();
    return newToken(claims);
  }
  
  public Claims verify(String token) throws JWTVerificationException {
    return new Claims(jwtVerifier.verify(token));
  }
}
  • 토큰 유효기간을 갱신(refreshToken)하고 유효한 토큰인지 확인하는 로직을 추가(verify)한다. 여기서 사용될 Claims는 이 클래스 내부에 스태틱 클래스로 만들 것이다.
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Date;

@Getter
public class Jwt {

  ...
  
  @NoArgsConstructor
  static public class Claims {
    Long userKey;
    String name;
    Email email;
    String[] roles;
    Date iat;
    Date exp;
  
    Claims(DecodedJWT decodedJWT) {
      Claim userKey = decodedJWT.getClaim("userKey");
      if (!userKey.isNull())
        this.userKey = userKey.asLong();
      Claim name = decodedJWT.getClaim("name");
      if (!name.isNull())
        this.name = name.asString();
      Claim email = decodedJWT.getClaim("email");
      if (!email.isNull())
        this.email = new Email(email.asString());
      Claim roles = decodedJWT.getClaim("roles");
      if (!roles.isNull())
        this.roles = roles.asArray(String.class);
      this.iat = decodedJWT.getIssuedAt();
      this.exp = decodedJWT.getExpiresAt();
    }
  
    public static Claims of(long userKey, String name, Email email, String[] roles) {
      Claims claims = new Claims();
      claims.userKey = userKey;
      claims.name = name;
      claims.email = email;
      claims.roles = roles;
      return claims;
    }
    
    long iat() {
      return iat != null ? iat.getTime() : -1;
    }
    
    long exp() {
      return exp != null ? exp.getTime() : -1;
    }
    
    void eraseIat() {
      iat = null;
    }
    
    void eraseExp() {
      exp = null;
    }
  }
}
  • JWT 토큰을 만드는 데에 필요한 정보들을 담은 클래스 Claim을 만든다. 클래스 내부에서만 사용될 것이기 때문에 새로운 클래스 파일로 만들지는 않았다. claimJWTCreator로 넘기면 JWT 토큰을 발급해 준다.
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.Getter;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringStyle;
import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString;

@Getter
public class Jwt {

  ...
  
  @Override
  public String toString() {
    return reflectionToString(this, ToStringStyle.JSON_STYLE);
  }
}
  • 그리고 마지막으로 toString()을 오버라이딩 해 주었는데 손 아프게 쓰는 것 좀 줄여보려고 org.apache.commons.lang3 패키지의 ToStringBuilder를 사용했다. 이것만 해도 단순노동이 많이 줄어든다.

ToStringBuilder란?
https://velog.io/@miro7923/JAVA-ToStringBuilder-사용이유와-사용법

  • ToStringBuilder가 생소하다면 위 게시글을 참고하자.

Entry as a Bean

@Configuration
public class ServiceConfigure {
  
  @Bean
  public Jwt jwt(JwtTokenConfigure jwtTokenConfigure) {
    return new Jwt(jwtTokenConfigure.getIssuer(), jwtTokenConfigure.getClientSecret(), jwtTokenConfigure.getExpirySeconds());
  }
}
  • Service layer와 관련된 설정 정보를 처리하는 클래스에 빈으로 등록한다.

Done 'Jwt'

Recommendation

[Spring Boot] 스프링 부트에서 JWT 사용하기

  • JWT 토큰 발급 클래스와 관련해 상세하게 설명이 되어 있다. 토큰 발급 과정을 보다 이해하기 위해 참고하면 좋은 글이라 링크를 추가한다.

  • 다음부터 오늘 만든 Jwt 객체를 이용해 토큰을 발급하는 클래스를 만들 것이다.

profile
당면한 문제는 끝까지 해결하기 위해 노력하는 주니어 개발자입니다.

0개의 댓글