hyeonga_code

Project_06_SpringSecurity + JWT 인증 후 다른 기능 구현 테스트 코드 작성 본문

Project_HYEONGARL

Project_06_SpringSecurity + JWT 인증 후 다른 기능 구현 테스트 코드 작성

hyeonga 2024. 6. 1. 06:59
반응형

 

 

Project_03_Spring Security, JWT 구현

Project_04_Spring Web LayerSpring Web Layer1. Web Layer뷰 템플릿 영역입니다.( @Controller, JSP, Freemarker ) 외부 요청과 응답에 대한 전반적인 영역입니다.( @Filter, 인터셉터, @ControllerAdvice ) 2. Service Layer@Service에

hyeonga493.tistory.com

 

문제.

Spring Security와 JWT를 사용하여 로그인 시 AccessToken을 발급하는 과정을 작성했다.

postman에서의 실행과 테스트 코드 모두 성공했는데 이를 적용하는데 문제가 생겼다.

SecurityFilterChain.java의 filterChain(웹 기반 보안 구성)에서 인증/인가 없이 접근할 수 있는 로그인의 "/login" 과 회원 가입의 "/user"만 설정해두었고, 다른 api에 접근하려면 인증/인가가 필요했다.

 

시도 1.

사용자 등록/로그인만 되는 현재, Controller, Service, TokenProvider, SecurityFilterChain, TokenAuthenticationFilter 클래스를 사용하는 것은 알고 있으나 JWT와 SpringSecurity의 흐름을 파악하기 위해 로직의 과정을 출력하는 코드를 추가했다.

1. 서버 실행
    > SecurityFilterChain	>> bCryptPasswordEncoder() 	: 비밀번호 인코더로 사용할 빈등록
    > SecurityFilterChain	>> filterChain()		: 웹 기반 보안 구성
    	// HttpSecurity : org.springframework.security.config.annotation.web.builders.HttpSecurity@59dae020
    > SecurityFilterChain	>> configure() 			: 스프링 시큐리티 기능 비활성화
    > SecurityFilterChain	>> authenticationManager() 	: 인증 관리자 설정
    	// HttpSecurity : org.springframework.security.config.annotation.web.builders.HttpSecurity@1a02063a
	// BCryptPasswordEncoder : org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder@199549a5

2. 사용자 등록 시 "http://localhost:8080/user"
    > TokenAuthenticationFilte	>> doFilterInternal()
    	// HttpServletRequest : org.springframework.security.web.firewall.StrictHttpFirewall$StrictFirewalledRequest$2@6b100f9a
	// HttpServletResponse : [Vary, Vary, Vary]
    > TokenAuthenticationFilter	>> getAccessToken()
    	// authorizationHeader : null
    > TokenProvider		>> validatedToken()
    	// token : null
    > UserController		>> addUser

3. 로그인 "http://localhost:8080/login"
    > TokenAuthenticationFilte	>> doFilterInternal()
    	// HttpServletRequest : org.springframework.security.web.firewall.StrictHttpFirewall$StrictFirewalledRequest$2@7c708e93
	// HttpServletResponse : [Vary, Vary, Vary]
    > TokenAuthenticationFilter	>> getAccessToken()
    	// authorizationHeader : null
    > TokenProvider		>> validatedToken()
    	// token : null
    > AuthController		>> login()
        // getUserEmail : test@email.com
        // getPassword : testPost
    > AuthController 		>> login() 	: start try
    > TokenProvider		>> generatedToken()
        // getUserEmail : test@email.com
        // getUserId : 1
        // getPassword : $2a$10$n3WxuuGX4DMKdzGF/f6WI.5E.ySiphqloOgeiFnIDburox1k3TD3i
        // expiredAt : PT1H
		// token : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJoeWVvbmdhcmxfaXNzdWVyIiwiaWF0IjoxNzE3MTUzMjU0LCJleHAiOjE3MTcxNTY4NTQsInN1YiI6InRlc3RAZW1haWwuY29tIiwiaWQiOjF9.EvNdwReDKK-1bDSjFoWj-V5WP_dKf2k6EdJNaRl0K6Q

 

api 호출시 먼저 실행되는 코드

TokenAuthenticationFilter >> doFilterInternal
TokenAuthenticationFilter >> getAccessToken
TokenProvider >> validatedToken

 

 

해결 1.

 

Project_04_Url 등록 기능 구현

url 다이어리 서비스를 제공할 예정이므로 url 관련 기능을 구현한다.1.  Url Entitypackage com.hyeongarl.entity;import jakarta.persistence.*;import jakarta.validation.constraints.Size;import lombok.*;import org.hibernate.validator.con

hyeonga493.tistory.com

 

하여 Url 관련 기능을 구현하고 테스트코드를 실행하는 과정에서 Unauthorized 가 발생해 모든 api에 대해 접근이 가능하도록 설정해두고 작업했다. 

 

해결 2.

모든 api에 대해 접근이 가능하도록 설정한 후 모든 작업을 마치고 추후 작업할 예정이었으나, 계속해서 생각나고 돌고돌아 다시 인증/인가를 처리하고 있기에 해결하고 넘어가야겠다고 생각했다.

 

서버를 실제로 실행하고 postman에서 작업하는 경우

header에 직접 Authorization을 설정해야한다. 코드에서 헤더에 넣어주는 작업을 하는 줄 알고 삽질중이었다..

서버측에서 로그인이 성공하면 JWT 토큰을 생성하여 클라이언트에게 반환하고, 이를 클라이언트가 받아 안전한 저장소에 저장하는데 대부분이 HTTP 요청의 헤더에 포함시켜 서버에 전송하게 되는 것이었다.

대부분의 웹 애플리케이션에서는 클라이언트 측에서 이를 자동으로 처리하는 라이브러리를 사용한다.

ex) JavaScript에서 Axios, Fetch API와 같은 HTTP 클라이언트 사용시 인증된 요청에 JWT 토큰을 자동으로 헤더에 추가한다.

 

 

테스트코드를 작성하는 경우

UrlControllerMockTest

모든 테스트 시작 전에 JWT 토큰을 발급하고, 설정을 해야하므로 @BeforeEach 어노테이션으로 작성

현재는 모든 것을 확인하고 싶어 전부 출력하며 확인했다.

 

> 작성  코드

    @BeforeEach
    void setUp() {
        jwtProperties = new JwtProperties();
        jwtProperties.setIssuer(ISSUER);
        jwtProperties.setSecretKey(SECRETKEY);

        tokenProvider = new TokenProvider(jwtProperties);

        User testUser = User.builder()
                .userId(1L)
                .userEmail("testUser@email.com")
                .password("testUserPassword")
                .userRegdate(LocalDateTime.now())
                .build();
        User user = userService.save(testUser);

        String token = tokenProvider.generatedToken(testUser, Duration.ofDays(10));
        System.err.println("token : " + token);

        // id 추출
        Long userId = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody()
                .get("id", Long.class);
        System.err.println("userId : " + userId);

        boolean validatedToken = tokenProvider.validatedToken(token);
        System.err.println("validatedToken : " + validatedToken);

        Authentication authentication = tokenProvider.getAuthentication(token);
        System.err.println("authentication : " + authentication);
        System.err.println("getName : " + authentication.getName());
        System.err.println("getAuthorities : " + authentication.getAuthorities());
        System.err.println("getCredentials : " + authentication.getCredentials());
        System.err.println("getDetails : " + authentication.getDetails());
        System.err.println("getPrincipal : " + authentication.getPrincipal());
        System.err.println("getUsername : " + ((UserDetails)authentication.getPrincipal()).getUsername());
        System.err.println("getPassword : " + ((UserDetails)authentication.getPrincipal()).getPassword());

        Long userIdByToken = tokenProvider.getUserId(token);
        System.err.println("userIdByToken : " + userIdByToken);
    }



saveUrl 테스트 코드 작성

    @Test
    @DisplayName("testSaveUrl")
    void testSaveUrl() {
        UrlRequestDto urlRequest = UrlRequestDto.builder()
                .url("https://www.naver.com")
                .urlTitle("saveUrlTitle")
                .urlDescription("saveUrlDescription")
                .categoryId(1L)
                .build();

        UrlResponseDto urlResponse = UrlResponseDto.builder()
                .url(urlRequest.getUrl())
                .urlDescription(urlRequest.getUrlDescription())
                .urlTitle(urlRequest.getUrlTitle())
                .categoryId(urlRequest.getCategoryId())
                .build();

        when(urlService.save(any(Url.class))).thenReturn(urlRequest.toEntity());

        UrlResponseDto responseEntity = urlController.saveUrl(urlRequest);
        assertThat(responseEntity.getUrl()).isEqualTo(urlResponse.getUrl());
    }

 

 

> 전체 코드

더보기
package com.hyeongarl.controller;

import com.hyeongarl.config.jwt.JwtProperties;
import com.hyeongarl.config.jwt.TokenProvider;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import com.hyeongarl.entity.Url;
import com.hyeongarl.entity.User;
import com.hyeongarl.repository.UserRepository;
import com.hyeongarl.service.UrlService;
import com.hyeongarl.service.UserService;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.time.Duration;
import java.time.LocalDateTime;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;


@ExtendWith(MockitoExtension.class)
public class UrlControllerMockTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private UrlService urlService;

    @Mock
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @InjectMocks
    private UserService userService;

    @InjectMocks
    private UrlController urlController;

    private JwtProperties jwtProperties;
    private TokenProvider tokenProvider;
    private final String SECRETKEY = "testJwtSecretkey";
    private final String ISSUER = "testJwtIssuer";

    @BeforeEach
    void setUp() {
        jwtProperties = new JwtProperties();
        jwtProperties.setIssuer(ISSUER);
        jwtProperties.setSecretKey(SECRETKEY);

        tokenProvider = new TokenProvider(jwtProperties);

        User testUser = User.builder()
                .userId(1L)
                .userEmail("testUser@email.com")
                .password("testUserPassword")
                .userRegdate(LocalDateTime.now())
                .build();
        User user = userService.save(testUser);

        String token = tokenProvider.generatedToken(testUser, Duration.ofDays(10));
        System.err.println("token : " + token);

        // id 추출
        Long userId = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody()
                .get("id", Long.class);
        System.err.println("userId : " + userId);

        boolean validatedToken = tokenProvider.validatedToken(token);
        System.err.println("validatedToken : " + validatedToken);

        Authentication authentication = tokenProvider.getAuthentication(token);
        System.err.println("authentication : " + authentication);
        System.err.println("getName : " + authentication.getName());
        System.err.println("getAuthorities : " + authentication.getAuthorities());
        System.err.println("getCredentials : " + authentication.getCredentials());
        System.err.println("getDetails : " + authentication.getDetails());
        System.err.println("getPrincipal : " + authentication.getPrincipal());
        System.err.println("getUsername : " + ((UserDetails)authentication.getPrincipal()).getUsername());
        System.err.println("getPassword : " + ((UserDetails)authentication.getPrincipal()).getPassword());

        Long userIdByToken = tokenProvider.getUserId(token);
        System.err.println("userIdByToken : " + userIdByToken);
    }
    
    @Test
    @DisplayName("testSaveUrl")
    void testSaveUrl() {
        UrlRequestDto urlRequest = UrlRequestDto.builder()
                .url("https://www.naver.com")
                .urlTitle("saveUrlTitle")
                .urlDescription("saveUrlDescription")
                .categoryId(1L)
                .build();

        UrlResponseDto urlResponse = UrlResponseDto.builder()
                .url(urlRequest.getUrl())
                .urlDescription(urlRequest.getUrlDescription())
                .urlTitle(urlRequest.getUrlTitle())
                .categoryId(urlRequest.getCategoryId())
                .build();

        when(urlService.save(any(Url.class))).thenReturn(urlRequest.toEntity());

        UrlResponseDto responseEntity = urlController.saveUrl(urlRequest);
        assertThat(responseEntity.getUrl()).isEqualTo(urlResponse.getUrl());
    }
}

 

 

> 실행

더보기
TokenProvider >> generatedToken

        // System.err.println("token : " + token);
        token : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0Snd0SXNzdWVyIiwiaWF0IjoxNzE3MTUzOTczLCJleHAiOjE3MTgwMTc5NzMsInN1YiI6InRlc3RVc2VyQGVtYWlsLmNvbSIsImlkIjoxfQ.-gjs_YCIF3t6kwD4n0Fe7NKFede-jEN0aGYEEGHP5hc

        // System.err.println("userId : " + userId);
        userId : 1

TokenProvider >> validatedToken

        // System.err.println("validatedToken : " + validatedToken);
        validatedToken : true

TokenProvider >> getAuthentication

TokenProvider >> getClaims

        // System.err.println("authentication : " + authentication);
        authentication : UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=testUser@email.com, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]

        // System.err.println("getName : " + authentication.getName());
        getName : testUser@email.com

        // System.err.println("getAuthorities : " + authentication.getAuthorities());
        getAuthorities : [ROLE_USER]

        // System.err.println("getCredentials : " + authentication.getCredentials());
        getCredentials : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0Snd0SXNzdWVyIiwiaWF0IjoxNzE3MTUzOTczLCJleHAiOjE3MTgwMTc5NzMsInN1YiI6InRlc3RVc2VyQGVtYWlsLmNvbSIsImlkIjoxfQ.-gjs_YCIF3t6kwD4n0Fe7NKFede-jEN0aGYEEGHP5hc

        // System.err.println("getDetails : " + authentication.getDetails());
        getDetails : null

        // System.err.println("getPrincipal : " + authentication.getPrincipal());
        getPrincipal : org.springframework.security.core.userdetails.User [Username=testUser@email.com, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]

        // System.err.println("getUsername : " + ((UserDetails)authentication.getPrincipal()).getUsername());
        getUsername : testUser@email.com

        // System.err.println("getPassword : " + ((UserDetails)authentication.getPrincipal()).getPassword());
        getPassword : 

TokenProvider >> getUserId

TokenProvider >> getClaims

        // System.err.println("userIdByToken : " + userIdByToken);
        userIdByToken : 1

UrlController >> saveUrl

 

이게 해결책인줄 알았는데 setUp()코드 모두 주석처리하고 실행해도 성공하는 로직이었다.

다시 작업해봐야겠다.

 

 

해결.

현재 테스트했던 코드는 통합 테스트가 아닌 단위 테스트로 인증/인가를 거치지 않고 진행하고 있다.
하여 인증 관련 코드를 작성하지 않아도 실행이 되고 있었다.
하여 원래 목적이었던 통합 테스트 코드에서 실행을 했을 때에도 오류가 발생해 다른 곳만 계속 찍어보고 수정했는데 알고보니 TokenProvider의 validatedToken() 메소드에서 try-catch문에서 return false를 반환하면서 발생하는 문제였다.

    // JWT 토큰 유효성 검증 메소드
    public boolean validatedToken(String token) {
        System.err.println("TokenProvider >> validatedToken");
        System.err.println("_____ token : " + token);
        try {
            System.err.println("TokenProvider >> validatedToken : start try");
            System.err.println(">>>>>>>>> token : " + token);
            System.err.println(">>>>>>>>> jwtProperties : " + jwtProperties.getSecretKey());
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey())    // 비밀값으로 복호화
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {     // 복호화 과정 중 에러 발싱 시 유효하지 않은 토큰
            System.err.println("return false");
            System.err.println("Exception during token validation: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

 

에러를 출력하기 위해 작성후 테스트 코드를 실행하니 오류가 발생했다.

Exception during token validation: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

 

JWT 서명이 로컬의 서명과 일치하지 않아 발생하는 오류였다. 기존에는 Test코드에서 JwtProperties 값을 따로 작성했었는데 @Autowired로 jwtProperties를 불러와 사용하니 해결되는 문제였다.

 

> 최종 코드

package com.hyeongarl.controller;

import com.hyeongarl.config.jwt.TokenProvider;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import com.hyeongarl.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;

import java.time.Duration;
import java.time.LocalDateTime;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UrlControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TokenProvider tokenProvider;

    HttpHeaders headers;
    UrlRequestDto urlRequest;
    HttpEntity<UrlRequestDto> requestEntity;

    @BeforeEach
    void setUp() {
        User testUser = User.builder()
                .userId(1L)
                .userEmail("testUser@email.com")
                .password("testUserPassword")
                .userRegdate(LocalDateTime.now())
                .build();

        String token = tokenProvider.generatedToken(testUser, Duration.ofDays(10));
        headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + token);

        urlRequest = UrlRequestDto.builder()
                .url("https://www.naver.com")
                .urlTitle("saveUrlTitle")
                .urlDescription("saveUrlDescription")
                .categoryId(1L)
                .build();

        requestEntity = new HttpEntity<>(urlRequest, headers);
    }

    @Test
    @DisplayName("testSaveUrl")
    void testSaveUrl() {
        ResponseEntity<UrlResponseDto> responseEntity
                = restTemplate.exchange("/url", HttpMethod.POST, requestEntity, UrlResponseDto.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        UrlResponseDto responseBody = responseEntity.getBody();
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getUrl()).isEqualTo("https://www.naver.com");
    }
}

 

> 실행

반응형