![[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] Auth + Test + CORS + Header + Swagger Error](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboNlPz%2FbtsIYvb7uhj%2FgfkqXc7WwytGpdm9akYSi0%2Fimg.png)
// Axios 인스턴스 생성
const api = axios.create({
baseURL: API_BASE_URL,
withCredentials: true, // 모든 요청에 대해 withCredentials: true 설정
});
1. 서론
엄청난 에러들과 싸운 그리고 프론트엔드에서 고생을 많이한 분기점 프론트엔드에서 고생을 해서 깊게는 아니지만 얻어간것이 많다. 개발 시간과 공부 시간의 간극을 줄이는 것이 이번 프로젝트에서 가장 신경써야하는 점. 어느정도 이해하고 이 기술을 사용할 것인가가 나의 전두엽을 계속 자극한다. 그럼 시작해보겠다.
2. Facade
[SW/DP] Facade Pattern (퍼사드 패턴)
정의복잡한 시스템에 대해서 간단한 인터페이스를 제공해서 시스템을 이용하는 사용 객체가 단일 진입점을 가지고도 모든 시스템을 시용할 수 있는 디자인 패턴을 말한다.위에 있는사진과 같
naturecancoding.tistory.com
너무나도 사랑스러운 디자인 패턴 service로직의 진입점이 한개로 변하니까 컨트롤러에서 사용하기 너무 편하다.
하지만 생각해야하는 것은 한곳에서 많은 method를 찾을 확률이 높음으로 규칙이 있는 메서드를 사용해야한다는 것이다.
2-1. Reade
crud 리파지토리에서 사용하는 방식을 이용한다.
예를 들어 user를 email로 찾을라면 UserFindByEmail 이런식으로 메서드명을 설정한다.
@Override
public Optional<User> UserfindByEmail(String email) {
return emailService.UserfindByEmail(email);
}
@Override
public Optional<User> UserfindById(Long id) {
return emailService.UserfindById(id);
}
이로써 다른 사람들이 나의 로직에 접근할때 어떤 객체를 어떤 값으로 받아오는지 명확해 진다.
2-2. Create & Update
수행작업이유 + 객체명을 사용한다.
예를 들어 register(사용자 생성) + Uesr 이런식으로 메서드 명을 생성한다.
@Override
public void registerUser(UserJoinRequest userJoinRequest) {
registerService.registerUser(userJoinRequest);
}
객체를 수행 작업이 뭔지 뚜렸하게 알려준다.
3. Test (JWT & Auth)
로직이 잘 돌아가는지 알기 위해서 test코드를 작성했다. 단위 테스트로 진행했다.
given/when/then 패턴
- given : 테스트 단위가 필요로하는 객체 및 데이터
- when : 어떤 코드를 실행했을 때
- then : 어떤 결과가 나와야하는지 - assertEquals, verify 등을 사용
중요한 것은 의존 관계를 끊어야 한다는 것
진짜 내가 작성한 코드가 잘 되는지만 검사할건데 db를 필요로 한다거나, jpa를 사용해야한다거나 하면 이들이 내 검증 로직을 방해하게 된다. 따라서 사용하는 것이 다음과 같다.
- @Mock : 모의 객체
- @MockBean : 통합 테스트에서 사용하는 모의 객체 나는 단위만 해서 사용 X
- @Spy : 실제 객체를 가져오지만 부분적으로만 모킹해서 가져옴 Mock과의 가장 큰 차이는 Mock은 실제 객체를 호출하지 않지만 Spy는 부분 적이긴 하지만 실제 객체를 호출해서 의존성이 생길수 있음 하지만 복잡한 객체에서 일부분의 메서드만 꺼내 사용할 수 있다는 장점이 있음
- @SpyBean : : 통합 테스트에서 사용하는 Spy 객체 나는 단위만 해서 사용 X
- @InjectMocks : 의존성을 주입 받야하하는 객체 Mock 객체를 주입받을 수 있게 해준다.
예를 들자면
@Mock
private UserService userService;
@InjectMocks
private UserController userController;
@Mock
private BindingResult bindingResult;
이런식으로 userService랑 bindingResult에 의존이 있는 userController가 있다면 @InjectMocks로 실제 코드의 의존성 없이 온전한 UesrController의 test를 수행할 수 있다.
잠시 찬양시간 가지고 가겠습니다.
멘토님들 정말 Respect!
나는 이미 멘토님들에게 의존성을 주입받은 몸이 돼버렸다.
이렇게 service랑 controller부분의 로직이 내가 원하는 데로 돌아가는지 확인 프론트엔드랑 연결을 할때 로직이 틀렸음에 고생하지 않기 위해서 작성했다.
4. CORS + Header 지옥
다른 도메인에서 리소스를 요청할 때 발생하게 되는 보안 문제. 가장 큰 문제점은 내 프로젝트가 frontend랑 backend를 라우팅해주는 nginx같은게 없어서 생기는 문제
어디서 발생했냐
frontend에서 백엔드에서 보낸 cookie와 header를 확인하지 못함
내가 만든 시큐리티 CORS 아직 팀이 프론트엔드에 익숙하지 않기 떄문에 모든 헤더, HTTP메서드를 허용 꽤 관대한 보안관리 (나중에 프로젝트엔드폰인트가 거의다 정립된다면 정리해서 보안성을 높어야함)
해결
// CORS 허용
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
/**
* CORS 설정을 위한 Bean
*
* @return CORS 필터
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:3000"); // 허용할 도메인 설정
config.addAllowedHeader("*"); // 모든 헤더 허용
config.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
config.addExposedHeader("Refresh-Token"); // 노출할 헤더 추가
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
/**
* CORS 설정을 위한 메서드
*
* @return CORS 설정 소스
*/
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOrigin("http://localhost:3000"); // 허용할 도메인 설정
configuration.addAllowedHeader("*"); // 모든 헤더 허용
configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
configuration.addExposedHeader("Refresh-Token"); // 노출할 헤더 추가
source.registerCorsConfiguration("/**", configuration);
return source;
}
노출할 헤더가 보이는가 ? 이게 없어서 하루 죙일 내 컨트롤러 reponse를 까보면서 내가 안보냈나를 확인했다. 하필이면 이 코드를 작성하기 전 공통 응답 처리가 완료되어서 내 컨트롤러의 공통응답 및 jwt컨트롤러의 공통응답을 변경했는데, 여기서 문제가 생긴줄알고 삽질 아무리 찾아봐도 프론트엔드에서 받아오는 응답에서 header에 쿠키랑 refresh가 보이지 않았다.
이를 cors를 한번더 허용해줘야 한다는 것을 알았다. 노출할 헤더를 설정해줬다. 이렇게 보면 간단한 문제지만 역시 공부할껀 많아~
원인 : Access-Control CORS 헤더
- Preflight Request :
클라이언트가 브라우저를 통해 'OPTION' 메서드를 사용해 사전 요청을 보냄 여기에는 Origin, Access-Control-Request-Method, Access-Control-Request-Headers 등의 헤더가 포함 - Preflight Response :
서버는 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 등의 헤더를 포함하여 응답 이 응답을 통해 브라우저는 실제 요청을 보내도 안전한지 확인 - Actual Request :
사전 요청이 성공하면, 클라이언트는 실제 요청(GET, POST 등)을 보냄
요청에는 Origin과 필요한 경우 자격 증명(Cookie 등)이 포함 나 같은 경우에는 access가 담킨 쿠키만 간다. - Actual Response :
서버는 실제 응답과 함께 CORS 관련 헤더(Access-Control-Allow-Origin, Access-Control-Expose-Headers, Set-Cookie 등)를 포함하여 응답 이 응답을 통해 클라이언트는 응답 데이터를 처리하고 필요한 헤더에 접근할 수 있음
이를 보면 Actual Response에 헤더가 안오는 것이 아니라 숨켜져 온다는 것 그리고 이를 브라우저가 사용자에게 보여주지 않는 것이라 보안이 취약한 브라우저 혹은 설정이 이상한 브라우저를 사용하면 헤더가 노출될 수 있다는 것이다.
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
// Axios 인스턴스 생성
const api = axios.create({
baseURL: API_BASE_URL,
withCredentials: true, // 모든 요청에 대해 withCredentials: true 설정
});
// 응답 인터셉터 설정
api.interceptors.response.use(
response => {
// 응답에서 Refresh-Token 헤더를 읽어 로컬 스토리지에 저장
const refreshToken = response.headers['refresh-token'];
if (refreshToken) {
localStorage.setItem('refreshToken', refreshToken);
}
return response;
},
error => {
return Promise.reject(error);
}
);
export default api;
프론트 엔드에서는 이 코드를 통해서 모든 응답에 refresh를 담을 수 있도록 했고 access를 안쓰는 응답에서 refresh가 재발급이 안되서 null값이 와도 에러는 무시하도록했다. 요청성공했는지는 다른 컴포넌트에서 확인할꺼니까~
5. Swagger
어제는 됐던 서버를 끄고 잠자고 일어나서 서버를 키고 swagger에 들어가니 403 에러코드가 뜨는 것이 아닌가? 읭 뭐지 하고봤더니 나를 반겨주는 EXPIRED_TOKEN이 있었다
원인 : 전날에 swagger try-it으로 해본 로그인에 남아있던 쿠키
쿠키가 시큐리티 인증 필터에 들어가서 swagger index자체에 들어갈 수 없었다. 나는 swagger에 들어갈때 인증을 따로 빼놨었는데 왜 걸리지 하고 security를 확인했다.
기존 코드에서는 어떤 요청이 들어오던 JWT 토큰 필터에 무조건 걸리게 되어있었고 내가 남겨둔 토큰이 인증을 시도했고 이로인한 오류로 403에러를 뵙게 됐다.
부분적인 해결
//swagger
String[] swaggerAllowPage = new String[]{
"/swagger-ui/**", // Swagger UI
"/v3/api-docs/**", // Swagger API docs
"/swagger-resources/**", // Swagger resources
"/swagger-ui.html", // Swagger HTML
"/webjars/**", // Webjars for Swagger
"/swagger/**" // Swagger try it out
};
/**
* 스웨거 필터 체인
*
* @param http 수정할 HttpSecurity 객체
* @return 체인 빌드
* @throws Exception HttpSecurity 구성 시 발생한 예외
*/
@Bean
@Order(1)
public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(swaggerAllowPage)
.authorizeHttpRequests(auth -> auth
.requestMatchers(swaggerAllowPage).permitAll()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
/**
* 보안 필터 체인
*
* @param http 수정할 HttpSecurity 객체
* @return 구성된 SecurityFilterChain
* @throws Exception HttpSecurity 구성 시 발생한 예외
*/
@Bean
@Order(2)
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//로직
}
이를 Order어노테이션으로 부분 해결했다. 명령의 순서인데 1 Order를 먼저 수행하겠다는 듯이다.
swagger에 관련된 url은 swaggerAllowPage에 다 넣아 놨기 때문에 전날 내가 쿠키를 지우지 않아도 들어갈수는 있게 했다.
남은 문제 해결
내가 작성한 코드에서는 jwt필터를 swagger문서에서 거치지 않기 떄문에 jwt관련 인증이 필요한 엔드포인트들을 try-it으로 swagger에서 실행해볼 수가 없다. 이를 해결해주는 방법이 있었다. 이로인해서 완전 해결은 아니고 부분적인 해결로 남겨뒀다. 다시한번 찬양해~~~
'Project : 그때 살껄;;.. > 개발일지' 카테고리의 다른 글
[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 시스템 아키텍처 (0) | 2024.09.09 |
---|---|
[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 디스코드 Github 알림 (0) | 2024.08.26 |
[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 거래 시스템 설계 (2) | 2024.08.17 |
[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] Security + JWT (0) | 2024.08.05 |
[멋쟁이사자처럼 백엔드 TIL/ 그때 살껄;;..] 프로젝트 초기 설계 (0) | 2024.08.05 |
Coding, Software, Computer Science 내가 공부한 것들 잘 이해했는지, 설명할 수 있는지 적는 공간