화요일 스터디 때 멘토님이 Member클래스를 만드실 때 Role이란 Enum값을 필드로 주는 게 기억났다. 그래서 나도 일단 Member에 Role필드를 추가해줬다.

 

// Entity : 실제 DB의 테이블과 매칭되는 클래스
@Getter
@Setter
@Entity
public class Member {
    // Id : 해당 테이블의 primary key
    @Id
    @Column(name = "email")
    private String email;

    @Column(name = "password")

    private String password;

    @Column(name = "nickname")
    private String nickname;

    @Column(name = "role")
    @Enumerated(EnumType.STRING)
    private Role role;
}

 

@Enumerated 어노테이션은 엔티티 클래스가 갖는 필드 중 enum필드한테 멕이는 어노테이션이라고 한다. 괄호에 들어가는 EnumType은 두 가지 종류가 있다고 한다. 찾아보니,

 

  • EnumType.STRING : 각 Enum의 이름을 컬럼에 저장함. ex) USER, ADMIN, GUEST...
  • EnumType.ORDINAL : 각 Enum에 대응되는 순서를 컬럼에 저장(Enum클래스에 선언된 순서대로) ex) 0, 1, 2, 3..

 

 

그리곤 mysql에서 만들어뒀던 Member테이블에도 role이란 enum값을 담는 컬럼을 추가해줬다.

 

테이블에 컬럼 추가하는 것도 좀 헤멨다..ㅠ (근데 컬럼 not null로 추가하게되면 기존에 있던 필드들은 어떻게 되는건지 궁금했는데 알아서 채워짐.) 


본격적으로 jwt를 활용한 회원가입/ 로그인을 드디어 만들 차례..ㅠ

우선 jwt를 발급하는 기능부터 만들어야한다. 다음 dependency를 추가해줬다.

 

implementation 'io.jsonwebtoken:jjwt:0.9.1'

 

그리고는 다음과 같이 JwtTokenProvider라는 클래스를 작성해줬다.

 

@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String jwtSecretKey;
    private final Long TOKEN_VALID_MS = 30 * 60 * 1000L; // token 유효시간. 30분

    public String createToken(String userEmail) {
        // claims : jwt에서 내가 원하는 걸 담는 공간. payload라고 보면 됨. 일종의 map
        Claims claims = Jwts.claims();
        claims.put("email", userEmail);

        return Jwts.builder()
                .setClaims(claims) // 위에서 만들어둔 claims 넣기
                .setIssuedAt(new Date(System.currentTimeMillis())) // 현재 시간 넣기
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALID_MS)) // 종료시간 넣기
                .signWith(SignatureAlgorithm.HS256, jwtSecretKey) // 서명하기
                .compact();
    }
}

 

JWT를 발급하는 경우 secret key를 이용해 생성해줘야 하는데 이 key값을 .java파일에 그대로 쓰면 깃허브에 올릴 시 노출되는게 당연했다. 뭔가 환경변수처럼 어딘가에 두고 활용해야 할 것 같은데,, 문제는 스프링이 첨이라 여기서는 그런 걸 어떻게 하는지 몰랐다. 리액트를 할 때는 .env란 파일을 만들고 거기에 환경변수를 적으면서 활용했었는데, 찾아보니까 자바에서도 그런 기능을 할 수 있는 파일이 있었고 그게 바로 apllication.properties파일이었다. 이 파일에서 jwt.token=블라블라..이런 식으로 작성한 다음, 위 코드에서 보이는 것처럼 @Value어노테이션을 통해 그 값을 불러올 수 있었다. 이렇게 또 하나 배웠다.

 

 

그러고는 토큰 검증 클래스와 메서드를 만들어줬다.

 

@Component
public class JwtTokenValidator {
    @Value("${jwt.secret}")
    private String jwtSecretKey;
    public boolean validateToken(String token) {
        try {
            // 토큰 복호화
            Claims claims = Jwts.parser() // parser 생성
                    .setSigningKey(jwtSecretKey) //  JWS 디지털 서명을 확인하는 데 쓰일 키를 세팅
                    .parseClaimsJws(token)
                    .getBody();
            return true;
        } catch (SignatureException e) {
            return false;
        } catch (ExpiredJwtException e) {
            return false;
        }

    }
}

 

그리고 토큰검증용 테스트를 만들어줬다.

 

class JwtTokenValidatorTest {
    private final JwtTokenProvider tokenProvider = new JwtTokenProvider();
    private final JwtTokenValidator tokenValidator = new JwtTokenValidator();

    @DisplayName("토큰 검증 성공 테스트")
    @Test
    void validateToken() {
        String testToken = tokenProvider.createToken("test@naver.com");

        assertThat(tokenValidator.validateToken(testToken)).isTrue();
    }
}

 

근데 오류가 났다.. TokenProvider의 jwtSecretKey필드가 null이란다. 뭐지? 테스트 코드로 안 하고 프로덕션 코드에서 jwt토큰은 잘만 발급되던데..(실험해봤음)

 

뭔가 하고 찾아보니, SpringBootTest 어노테이션이 없는 단위 테스트시 @value값이 주입되지 않는다고 한다. 단위 테스트에선 Spring의 Application Context가 로딩되지 않기 때문이라고 한다. 

 

그냥 토큰발급 클래스에서 시크릿키를 필드로 가지는게 아니라 메서드의 파라미터로 받도록 했다. 또한 만료기간이 지나는 것도 테스트하기 위해 만료기간도 토큰발급 클래스가 필드로 가지는게 아니라 메서드의 파라미터로 받도록 했다.

 

결국 최종적으로 바뀐 토큰발급 클래스(JwtTokenProvider클래스)는 다음과 같았다.

 

@Component
public class JwtTokenProvider {
    public String createToken(String userEmail, String jwtSecretKey, long expiredMs) {
        // claims : jwt에서 내가 원하는 걸 담는 공간. payload라고 보면 됨. 일종의 map
        Claims claims = Jwts.claims();
        claims.put("email", userEmail);

        return Jwts.builder()
                .setClaims(claims) // 위에서 만들어둔 claims 넣기
                .setIssuedAt(new Date(System.currentTimeMillis())) // 현재 시간 넣기
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs)) // 종료시간 넣기
                .signWith(SignatureAlgorithm.HS256, jwtSecretKey) // 서명하기
                .compact();
    }
}

 

초기와는 달리 secretKey와 만료시간을 파라미터로 받는 모습. Validator클래스에서도 마찬가지로 secretKey를 파라미터로 받도록 바꿔줬다.

 

그리고 드디어 얘네를 테스트하는 코드를 마저 작성한 다음, 테스트를 돌려봤다..!

 

 

편-안

드디어 편안..하다.

 

이제 jwt와 관련해 또 만들 것은

 

  1. 클라이언트가 토큰을 담아 보내는 request로부터 토큰을 빼내는 기능
  2. 토큰에서 내가 담았던 정보(즉 email)을 빼내는 기능
  3. 빼낸 정보를 바탕으로 인증정보를 가져오는 기능. 이건 시큐리티랑 같이 짝짝꿍하는 듯함(아닐 수도)

 

요로코롬 3개다. 구글링해서 나온 예제들에선 내가 위에서 만들었던 토큰발급기능과 토큰검증기능, 그리고 바로 위의 3개 기능을 하나의 클래스에 작성했었다. 하지만 나는 발급 / 검증을 다른 책임으로 보고 클래스를 분리해서 만들어뒀다. (근데 이것도 토큰에 관련된 책임이란 넓은 관점에서 보면 같은 클래스에 쓰는 것도 합당하긴 할 것 같다) . 암튼 그래서 바로 위 3개의 기능은 TokenAnalyzer라는 클래스(토큰 분석기..ㅋㅋㅋㅋ 마땅히 붙여줄 이름이 생각나지 않는다..)에 작성해두기로 했다. 3번 기능은 시큐리티 쓸 때 마저 만들고, 일단 1, 2번만 만들기로!

 

@Component
public class JwtTokenAnalyzer {
    public String getTokenFromHeader(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    public String getUserEmailFromToken(String token, String jwtSecretKey) {
        // 토큰 복호화
        Claims claims = Jwts.parser() // parser 생성
                .setSigningKey(jwtSecretKey) //  JWS 디지털 서명을 확인하는 데 쓰일 키를 세팅
                .parseClaimsJws(token)
                .getBody();
        String userEmail = (String)claims.get("email"); // object형태로 저장돼있어서..문자열로 변환해야 함
        return userEmail;
    }
}

 

토큰 생성시 claim들을 만들 땐 문자열을 put하면서 만들었었는데, parser가 토큰으로부터 다시 정보들을 역으로 만드는 과정에서는 문자열 형태가 아니라 객체 형태로 생성되는 듯 하다. 그래서 String으로 형변환을 해서 이메일을 가져온다.

 

이메일을 정말 잘 가져오나..토큰에 들어가는 이메일이 내가 써둔 이메일이 맞나! 를 테스트하는 코드를 짰다.

 

class JwtTokenAnalyzerTest {
    private final JwtTokenProvider tokenProvider = new JwtTokenProvider();
    private final JwtTokenAnalyzer tokenAnalyzer = new JwtTokenAnalyzer();

    @DisplayName("토큰에 저장된 이메일이 내가 적어준 이메일이 맞는지 테스트")
    @Test
    void getUserEmailFromToken() {
        String secretKey = "1234560ACB6F1AD6B6A6184A31E6B7E37DB3818CC36871E26235DD67DCFE4041492";
        String testEmail = "test@naver.com";
        long expiredMs = 30 * 60 * 1000L;
        String testToken = tokenProvider.createToken(testEmail, secretKey, expiredMs);

        assertThat(tokenAnalyzer.getUserEmailFromToken(testToken, secretKey)).isEqualTo(testEmail);
    }
}

 

결과는..

 

 

무사히 통과. 이제 시큐리티랑 짝짝꿍해 회원가입/로그인 로직을 만들면 될 것 같다.

+ Recent posts