hyeonga_code
Project_06_SpringSecurity + JWT 인증 후 다른 기능 구현 테스트 코드 작성 본문
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");
}
}
> 실행
'Project_HYEONGARL' 카테고리의 다른 글
Project_08_Url 수정/삭제 기능 구현 Controller 테스트 코드 작성 (1) | 2024.06.03 |
---|---|
Project_07_Spring Security + JWT 토큰에서 추출한 정보 사용하기 (1) | 2024.06.03 |
Project_05_Url 조회 기능 구현 /TestCode (0) | 2024.06.01 |
Project_04_Url 등록 기능 구현 (0) | 2024.05.31 |
Project_03_Spring Security, JWT 구현 (0) | 2024.05.30 |