SOP(Same - Origin Policy)
브라우저에서 실행될 때 특정한 보안 제약 사항
다른 출처(도메인, 포트, 프로토콜)간의 상호작용을 제한함으로써 보안을 강화한다. 즉 웹 페이지가 특정 출처에서 로드된 리소스에만 접근할 수 있도록 제한한다.
웹 보안을 강화하는 중요한 기능이다. 이를 통해 웹 페이지가 웹 애플리케이션은 사용자 개인 정보와 민감한 데이터를 보호하고 악의적인 스크립트로부터의 공격을 방지할 수 있습니다. 그러나 이로 인해 웹 애플리케이션 간의 데이터 공유와 통신이 제한되는 경우도 있습니다
CORS는 SOP 일부로 SOP의 제약을 완화하여 특정 출처에서의 리소스 요청을 허용하는 매커니즘이다. CORS를 통해 서버는 특정 출처에서의 리소스 요청을 허용하는 매커니즘이다. CORS를 통해 서버는 특정 출처에서의 요청에 대한 액세스를 허용할 수 있다. 이를 통해 다른 출처의 리소스를 안전하게 요청하고 사용할 수 있다.
CORS(Cross-Origin Resource Sharing)
웹 애플리케이션에서 발생하는 보안 정책 중 하나이다. 웹 브라우저에서 실행되며 웹 페이지가 다른 출처의 리소스를 요청할 때 CORS 정책이 적용됩니다. 기본적으로 웹 브라우저는 보안상의 이유로 스크립트에서 다른 출처의 리소스를 요청하는 것을 제한한다.
이러한 제한을 완화하기 위한 매커니즘으로 웹 서버가 브라우저에서 특정 출처에서의 리소스를 허용하도록 허용 헤더를 제공한다. 웹 서버는 클라이언트의 요청 헤더에 'Origin'이라는 필드를 통해 출처 정보를 전달받는다. 서버는 이 정보를 기반으로 응답 헤더에 필드를 포함하여 허용할 출처를 지정합니다
보안성을 위해서 서로 다른 출처의 리소스를 요청할 때 요청하는 리소스에 대한 정보를 미리 작성하고, 지정하여 해당 리소스의 요청을 허가하도록 하는 원리인 것 같았다. 스프링에서는 크게 해당 정보를 설정하는 class를 따로 작성하거나, 스프링 시큐리티에서 수동 빈등록을 사용하는 방법이 있는데 이 중 후자를 택하여 CORS를 작성해보고자 했다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
config.setAllowedMethods(Arrays.asList("HEAD","POST","GET","DELETE","PUT","OPTIONS"));
config.setAllowedHeaders(Arrays.asList("*"));
config.addExposedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
Origins, Mehods
CORS를 허용할 url은 리액트의 포트번호인 3000으로 지정하고, 요청을 허용할 메서드들을 지정했다. 여기서 API에 명시된 GET,POST,PUT,DELETE 말고 OPTIONS와 HEAD도 허용하도록 설정했는데, 이 두 가지 메서드는 CORS 프리플라이트 요청과 관련하여 중요한 역할을 한다.
CORS 정책은 실제 요청을 보내기 전에 브라우저가 사전에 "프리플라이트(preflight)" 요청을 보낸다. 이 프리플라이트 요청은 실제 요청을 보내기 전에 서버가 클라이언트의 요청을 수락할 수 있는지를 확인하는 역할을 하는데 이 때 사용되는 HTTP 메서드가 OPTIONS이다. 따라서 OPTIONS 메서드를 허용하도록 설정하면 브라우저에서 프리플라이트 요청을 보낼 수 있고, 이를 통해 실제 요청이 허용되는지 여부를 확인할 수 있다.
HEAD 메서드는 GET과 유사하지만, 서버는 실제 데이터를 응답으로 보내지 않고, 헤더만을 반환하는 HTTP 메서드이다. 일반적으로 브라우저는 HEAD 메서드를 사용하여 리소스가 존재하는지, 최신 버전인지 등을 확인하는 용도로 사용하는데,
HEAD 메서드를 허용함으로써, 브라우저에서 이런 유형의 요청을 수행할 수 있다.
HEAD와 OPTIONS 메서드를 허용하지 않으면, 브라우저가 CORS 정책으로 인해 해당 메서드를 제한하게 되어 다양한 문제가 발생할 수 있게된다. 특히, 다른 도메인에서 Ajax 요청을 보낼 때 문제가 발생할 수 있으며, 예상치 못한 동작 또는 오류가 발생할 수 있기 때문에 두 메서드는 허용해야한다.
Headers
Headers는 허용되는 요청 헤더를 설정하는 부분이며 요청하는 모든 헤더를 허용하도록 설정한다. 즉 클라이언트가 어떤 종류의 헤더를 사용하여 요청하더라도 서버측은 이를 허용한다는 의미이다
Credentials
Credentials는 자격증명 허용 여부를 설정한다. 해당 부분을 true로 설정하여 클라이언의 요청에서 자격증명을 포함할 수 있는 쿠키 및 인증 정보를 포함하는 것들을 허용하게 된다
ExposedHeaders
응답 헤더에 노출할 수 있는 모든 헤더를 추가하는 역할을 한다. 노출할 수 있는 모든 헤더란 브라우저에서 클라이언트가 접근한 리소스로부터 받은 응답에 포함된 헤더들을 클라이언트가 액세스할 수 있게 허용하는 것을 의미한다. 현재는 *을 사용하여 모든 헤더를 노출하였지만 보안 측면에서는 권장되지 않는다. 다만 혹시 몰라서 해당 설정을 이용하여 클라이언트가 모든 헤더에 접근할 수 있게끔 설정했다.
모든 설정을 마치고, UrlBasedCorsConfigurationSource 객체를 생성하여 CORS 구성을 설정하는 객체를 생성한다.이 객체를 사용하여 특정 URL 패턴에 대한 CORS 설정을 등록할 수 있습니다.
source.registerCorsConfiguration("/**", config); 이 코드는 /** 패턴에 대한 CORS 구성을 등록하는 부분이다. 즉 config에 설정된 내용을 이용하여, 모든 URL에 대해 살장힌 CORS를 적용하도록 설정한다. 여기서 /** 패턴은 모든 URL을 대상으로 한다는 의미.
위의 설정들을 통하여 프론트엔드와 연결을 할 기본 준비를 마쳤고 연결을 시도하였고, CORS 오류없이 연결을 진행할 수 있었다. CORS에 대해 기반 지식이 없을 때에는 해당 문제를 해결해야한다는 압박감이 크게 다가왔는데 기본적인 개념부터 구체적인 해결방식들을 공부하면서 웹 브라우저가 보안성을 높이기 위해 이러한 정책들을 적용하는 것이 놀랍다는 느낌을 많이 받았다. 여태까지 공부하면서 인터넷 통신, 쿠키, JWT토큰과 같이 다양한 방식으로 보안성이 지원된다고 느꼈는데 이를 모두 뚫고 해킹하는 사람들도 대단하다고 느낄 수 밖에 없었다.
해당 CORS는 스프링 시큐리티 필터를 거치기 전에, 먼저 검증되기 때문에 시큐리티 설정에도 적용이 필요했다
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정, CORS 설정, 기존 세션 방식 -> JWT 방식
http
.csrf((csrf) -> csrf.disable())
.cors(withDefaults())
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/auth/**").permitAll()
.requestMatchers(GET, "/post/**").permitAll()
.anyRequest().authenticated()) // 그 외 모든 요청 인증처리
.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter(), JwtAuthenticationFilter.class);
return http.build();
}
import static org.springframework.security.config.Customizer.*;
해당 CORS를 Customizer.withDefaults를 사용하여 수동으로 등록한 config를 사용하여 Customizer는 static import하여 사용했다.
커스텀 예외와 예외 메세지 Enum 처리
@Getter
public enum ErrorCodeEnum {
TOKEN_INVALID(BAD_REQUEST, "유효한 토큰이 아닙니다."),
TOKEN_EXPIRED(BAD_REQUEST, "토큰이 만료되었습니다"),
LOGIN_FAIL(BAD_REQUEST, "로그인 실패"),
DUPLICATE_USERNAME_EXIST(BAD_REQUEST, "중복된 사용자가 존재합니다"),
USER_NOT_MATCH(BAD_REQUEST, "작성자만 수정,삭제가 가능합니다"),
POST_NOT_EXIST(BAD_REQUEST, "존재하지 않는 게시글입니다"),
COMMENT_NOT_EXIST(BAD_REQUEST, "존재하지 않는 댓글입니다"),
FILE_INVALID(BAD_REQUEST, "유효한 파일이 아닙니다"),
UPLOAD_FAIL(BAD_REQUEST, "유효하지 않은 요청입니다");
private final HttpStatus status;
private final String message;
ErrorCodeEnum(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
}
public class InvalidConditionException extends IllegalArgumentException{
ErrorCodeEnum errorCodeEnum;
public InvalidConditionException(ErrorCodeEnum errorCodeEnum) {
this.errorCodeEnum = errorCodeEnum;
}
}
public class UploadException extends RuntimeException{
ErrorCodeEnum errorCodeEnum;
public UploadException(ErrorCodeEnum errorCodeEnum) {
this.errorCodeEnum = errorCodeEnum;
}
public UploadException(ErrorCodeEnum errorCodeEnum, Throwable cause) {
super(cause);
this.errorCodeEnum = errorCodeEnum;
}
}
@Transactional(readOnly = true)
public Comment findComment(Long commentId) {
return commentRepository.findById(commentId).orElseThrow(() ->
new InvalidConditionException(COMMENT_NOT_EXIST));
}
@Transactional(readOnly = true)
public Post findPost(Long postId) {
return postRepository.findById(postId).orElseThrow(() ->
new InvalidConditionException(POST_NOT_EXIST));
}
private void checkUsername(Long commentId, User user) {
Comment comment = findComment(commentId);
if (!(comment.getUser().getId() == user.getId())) {
throw new InvalidConditionException(USER_NOT_MATCH);
}
}
기존에 코드에서는 자바에서 제공하는 기본 예외를 사용하고 예외메서지들을 직접 작성했는데 기술매니저님의 피드백을 받고 보다 코드의 일관성과 예외메세지를 일괄적으로 처리할 수 있도록 재구성하고자 했다. Enum을 통해 해당 예외 메세지들을 한 군데서 관리하고 IllegalargumentException을 상속받는 InvalidConditionException을 생성하여 해당 Enum을 매개변수로 받아 사용할 수 있게끔 수정했다. 원래는 각 도메인마다 예외를 새로 만들었지만 작성하는 과정에서 이는 매우 비효율적이라고 느끼게 되었다. 예외가 발생하는 원인이 충분히 유추가능하고 해당 원인이 이미 자바에서 제공하는 이유라면 이를 이용하여 일괄적으로 처리하는 것이 좋을 것 같아 크게 InvalidCondtionException과 UploadException로 나누고 예외처리를 진행하였다.
'미니 프로젝트 > 이노베이션 6주차' 카테고리의 다른 글
이노베이션 6주차 4 - S3 이미지 업로드, 삭제, 수정 (0) | 2023.07.20 |
---|---|
이노베이션 6주차 2 - 좋아요 표시,취소 기능 / S3 이미지 업로드 (0) | 2023.07.17 |
이노베이션 6주차 1 - API, ApiResponse<T>, Querydsl (0) | 2023.07.16 |