인프런 커뮤니티 질문&답변

김귤님의 프로필 이미지
김귤

작성한 질문수

Spring Boot JWT Tutorial

jjwt 버전을 올렸더니 jwt가 유효하지 않다고 합니다

작성

·

3K

·

수정됨

0

강의 예제를 보면 springboot 3.2.0 버전이고 java도 17버전으로 기준하여 작성된 것 같습니다.

제 jdk 환경도 17버전이고요. cmd 창에서 java -version으로 확인도 했습니다.

내려받아 돌려보면 잘 작동합니다.

 

그런데 jjwt 0.11.5 버전이 아닌 jjwt 0.12.3 버전을 사용해보고 싶어서 코드를 일부 수정했습니다.

또한 Bcrypt알고리즘이 이제는 더 이상 안전하지 않다는 얘길 들어서 Argon2 알고리즘을 사용해보고자 했습니다.

dependency에 jjwt 0.12.3 버전으로 변경하였고

Jwts 이하 메서드가 jjwt에서는 바뀌어서 key에 타입을 명시해주어야 했습니다.

PostMan에서 JWT Bearer로 설정하고 encode체크를 하지 않은 채로(이미되어있으니) HS512알고리즘으로 Secret을 입력하고 localhost:8080/api/authenticate 로 요청을 보내봤습니다.

{

"username": "user",

"password", "user"

}

하지만 jjwt 0.11.5 버전일 때는 잘 오던 token 내용이 500error를 내면서 토큰이 유효하지 않다고 합니다;

제 코드에서 잘못 한 점이 있을까요?

 

P.S 스스로 gradle refresh, clean build 등을 한 후에 테스트해본 결과

jjwt 0.12.3은 0.11.5와 vscode의 REST Client로 api 테스트 시에 강의와 동일한 결과를 보여주었습니다.

그러나 Argon2 알고리즘을 사용하여 생성한 비밀번호를 사용하면 엉뚱하게도 jwt토큰이 유효하지 않다며 401 에러가 났습니다. 강의대로 Bcrypt 알고리즘을 쓰면 없는 일입니다...

다만 이런 오류를 내는 연관성을 모르겠습니다 ;;

 

package com.gyull.jwt.jwt;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;

@ExtendWith(MockitoExtension.class)
@SpringBootTest
class JwtApplicationTests {

	private static final Logger logger = LoggerFactory.getLogger(JwtApplicationTests.class);

	@Mock
	private BytesKeyGenerator keyGeneratorMock;

	private Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();

	@Test
	void contextLoads() {
	}
        //argon2알고리즘으로 비밀번호를 생성하기 위해 테스트를 했습니다
        //data.sql로 table에 insert하기 위함.
	@Test
	public void encodeWhenEqualPasswordWithCustomParamsThenMatches() {
		encoder = new Argon2PasswordEncoder(16, 32, 4, 65536, 3);
		String result = encoder.encode("admin");
		logger.info("인코딩완료: "+result);
		assertTrue(encoder.matches("admin", result));
	}

}
package com.gyull.jwt.jwt.security.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;

import com.gyull.jwt.jwt.security.filter.JwtFilter;
import com.gyull.jwt.jwt.security.filter.JwtSecurityConfig;
import com.gyull.jwt.jwt.security.handler.JwtAccessDeniedHandler;
import com.gyull.jwt.jwt.security.handler.JwtAuthenticationEntryPoint;
import com.gyull.jwt.jwt.security.provider.TokenProvider;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final TokenProvider tokenProvider;
  private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
  private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
  private final CorsFilter corsFilter;
  
  //Bcrypt 알고리즘은 예제 연습할 때는 쓰지만 연산속도가 발전한 지금은
  //취약점이 있기 때문에 Argon2를 비롯한 대체 알고리즘을 더 권장하고 있다.
  @Bean
  public PasswordEncoder passwordEncoder(){
    return new Argon2PasswordEncoder(16, 32, 4, 65536, 3);
  }

  //Spring Security 6.1.x 버전부터는 Lambda DSL의 사용으로 .and()와 같은 메서드가 불필요하다
  //Spring Security 7을 염두로 두고 deprecated, removal 되는 메서드가 무척 많았다
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf((csrf) -> csrf.disable())
        //
        .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
        .exceptionHandling((exceptionHandling) ->
            exceptionHandling
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .accessDeniedHandler(jwtAccessDeniedHandler)
        )
        .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(
          authorize -> 
          authorize
            .requestMatchers("/").permitAll()
            .requestMatchers("/api/hello", "/api/authenticate", "/api/signup").permitAll()
            .requestMatchers("/admin/**").permitAll()
            .requestMatchers("/favicon.ico").permitAll()
            .requestMatchers(PathRequest.toH2Console()).permitAll()
            .anyRequest().authenticated()
        )
        //Spring Security 6.2.0 javadoc에서는 .apply()메서드가 deprecated되었으니 .with()을 대신 쓰라고 되어있다.
        //다른 github 예제들은 jwtSecurityConfig를 따로 작성하지 않고
        //addFilterBefore을 사용하여 직접 추가해주고 있는데, jwtSecurityConfig 또한 내부적으로 addFilterBefore을 사용해서 httpSecurity에 추가해줄 수도 있다.
        .with(new JwtSecurityConfig(tokenProvider), customizer -> {});

    return http.build();
  }
}
  implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.3'
  runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.3'
  runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.12.3'

import javax.crypto.SecretKey;
//나머진 동일하여 생략

public String createToken(Authentication authentication){
    String authorities = authentication.getAuthorities().stream()
                                      .map(GrantedAuthority::getAuthority)
                                      .collect(Collectors.joining(","));

    long now = (new Date()).getTime();
    Date validity = new Date(now + this.tokenValidityInMilliseconds);
    return Jwts.builder()
            .subject(authentication.getName())
            .claim(AUTHORITIES_KEY, authorities)
            .signWith((SecretKey) key, Jwts.SIG.HS512)
            .expiration(validity)
            .compact();
  }

  public Authentication getAuthentication(String token){
    Claims claims = Jwts
                    .parser()
                    .verifyWith((SecretKey) key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();

    Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                  .map(SimpleGrantedAuthority::new)
                  .collect(Collectors.toList());

    User principal = new User(claims.getSubject(), "", authorities);

    return new UsernamePasswordAuthenticationToken(principal, token, authorities);
  }

  public boolean validateToken(String token){
    try {
      Jwts.parser().verifyWith((SecretKey) key).build().parseSignedClaims(token);
      return true;
    } catch(SecurityException | MalformedJwtException e){

      logger.info("잘못된 JWT 서명입니다.");
    } catch(ExpiredJwtException e){

      logger.info("만료된 JWT 토큰입니다.");
    } catch(UnsupportedJwtException e){

      logger.info("지원되지 않는 JWT 토큰입니다.");
    } catch(IllegalArgumentException e){

      logger.info("JWT 토큰이 잘못되었습니다.");
    }

    return false;
  }

답변 1

0

안녕하세요, 인프런 AI 인턴입니다.

김귤님의 질문에 대해 몇 가지 가능성을 말씀드리겠습니다.

첫째, jjwt 라이브러리 버전을 변경했을 때 발생할 수 있는 문제점은 버전 업데이트로 인한 API 변경이나 호환성 문제일 수 있습니다. 이 경우, 변경된 버전에 맞게 코드를 수정하셨다고 하니, jjwt의 사용법이나 구현 방식에 대한 공식 문서를 살펴보시고, 메소드의 사용 방법이나 시그니처가 올바른지 확인해보시기 바랍니다.

둘째, Argon2 알고리즘을 사용했을 때 발생하는 문제는 비밀번호 인코딩 방식이 바뀜에 따라 기존 JWT 생성 로직과 호환되지 않아 생길 수 있는 이슈일 수 있습니다. Bcrypt 대신 Argon2를 사용함으로써 인증 프로세스 내에서 비밀번호 검증 부분이 영향을 받을 수 있습니다. 비밀번호 인코더가 달라지면서 실제 인증 로직에서 비밀번호를 매칭시키는 과정에 문제가 생겼을 가능성이 있습니다. 이 경우, Argon2 알고리즘으로 인코딩된 비밀번호를 정상적으로 처리하도록 관련 코드를 확인하고 수정해야 할 수 있습니다.

셋째, jjwt 0.11.5 버전과 0.12.3 버전 사이의 변경 사항은 jjwt 라이브러리의 릴리즈 노트를 확인하여 파악하시는 것이 좋습니다. 해당 버전에서 달라진 부분이나 수정 사항에 대해 잘 확인하셔서 문제를 해결하는 데 참조하시기 바랍니다.

마지막으로, signWithverifyWith 메소드에서 사용하는 key 타입이 올바른지 다시 한 번 검토해보세요. jjwt 0.12.3 버전에서는 타입을 명확하게 지정해줘야 하는 변화가 있을 수도 있으니, 해당 부분이 올바르게 적용되었는지 확인해볼 필요가 있습니다.

토큰을 검증하는 validateToken 메서드에서 사용하시는 SecretKey 캐스팅은 암호화 키가 정확히 일치하는지, 원하는 알고리즘으로 되어 있는지 확인해보시기 바랍니다.

개발 환경에서는 잘 되는데 테스트 시 JWT 토큰이 유효하지 않다는 에러가 발생하는 경우, 환경에 따른 설정 값의 차이나 빌드 프로세스에서 발생할 수 있는 이슈도 고려해보시기 바랍니다.

보다 정확한 진단과 해결을 위해서는 스프링 부트와 스프링 시큐리티, jjwt에 대한 공식 문서를 참고하시고, 에러 로그를 통해 보다 자세한 에러 메시지를 분석하실 필요가 있습니다. 관련 문서 링크를 참조해보세요.

Spring Boot Official Documentation: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
Spring Security Official Documentation: https://spring.io/projects/spring-security

에러를 해결하시길 바라며, 추가적인 질문이 있으시면 언제든 문의해 주세요.

김귤님의 프로필 이미지
김귤
질문자

.signWith과 .verifyWith 메서드가 내부적으로 타입을 javax.crypto.SecretKey로 지정하고 있습니다.

강의 예제를 Argon2 알고리즘으로 변경하였을 때 Argon2 builder.class 파일을 찾을 수 없다는 오류가 나고 있었습니다. 제가 알기로는 Spring Security에 Argon2 알고리즘은 내장된 알고리즘이고, 테스트 시에 알고리즘으로 비밀번호 생성(변환)도 잘 되었습니다. 그런데, 오류 설명을 보니 org.bouncycastle 라는 특정 라이브러리가 없다는 오류가 난 것입니다.

implementation 'org.bouncycastle:bcprov-jdk18on:1.76' 이런 식으로 org.bouncycastle 라이브러리를 추가해주었더니 JWT토큰이 일치하지 않는다는 오류도 사라졌고 토큰도 발행되었습니다.

구글링하면서 해당 라이브러리를 시큐리티 사용 시에 따로 import 해주어야 한다는 말은 못본 듯하여 의문입니다.

 

김귤님의 프로필 이미지
김귤

작성한 질문수

질문하기