728x90

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

Security는 끝날 때까지 끝난 게 아닙니다..

지금은 그간 놓치고 있던 오류 처리를 해주고 있습니다.

 

회원 탈퇴 기능을 추가하면서 User 테이블에 IsUsed를 선언해서 flag 처럼 사용하고 있습니다.

이제 탈퇴한 회원은 조회가 되면 안되기에 검색 로직에서 IsUsed를 추가해주고 로그인을 테스트해본 것이 가까워진 Security와 멀어진 계기가 되었죠..

 

멀어진 Security와 다시 가까워지기 위한 노력을 정리해 보았습니다..

 

자.. 이제 문제가 뭐였느냐..

회원 정보가 없을 때, 제가 Throw한 Exception과 전달받은 Exception이 달랐습니다..

 

준 적이 없는데 누군가는 받은 셈..

 

LoginFilter.java
@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {

    try {
      LoginRequestDTO loginRequestDto = objectMapper.readValue(request.getInputStream(),
          LoginRequestDTO.class);

      String email = loginRequestDto.getEmail();
      String password = loginRequestDto.getPassword();

      UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
          email, password, null);

      return authenticationManager.authenticate(authToken);

    } catch (IOException e) {
      log.error("Failed to parse authentication request body {}", e);

      throw new BusinessException(INVALID_CREDENTIALS);
    }
  }

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException failed) {
    log.error("Login failed: {}", failed.getMessage());

    throw new BusinessException(CHECK_LOGIN_ID_OR_PASSWORD);
  }
}

로그인의 경우에는 Controller를 사용하지 않고, UsernamePasswordAuthenticationFilter를 상속받아 LoginFilter를 구현해서 사용하고 있습니다.

 

코드에 명시적으로 적혀있진 않지만, AuthenticationManager의 authenticate()를 호출하면 회원 및 비밀번호 검증이 이뤄지게 됩니다.

(관련 내용은 이 포스팅에 적어놨었죠.. 저도 다시 찾아봤습니다^,,^)

간단히 다시 정리해보자면, 다음과 같습니다.

  * login Filter는 AuthenticationManager를 사용하여 인증 작업을 수행

  * AuthenticationManager는 interface이기 때문에 실제로는 AuthenticationProvider가 인증 작업을 수행

  * AuthenticationProvider는 인증 작업을 수행할 때, UserDetailsService를 호출

    * 저의 경우에는 인터페이스인 UserDetailsService를 구현한 CustomUserDetailsService가 호출되었습니다.

  * UserDetailsService는 loadUserByUsername()를 통해 사용자 정보를 조회한 후, 사용자 인증 정보인 UserDetails를 반환

  * AuthenticationProvider는 반환된 UserDetails 객체와 클라이언트가 제공한 비밀번호를 비교하여 인증을 수행

 

따라서 실제 사용자가 존재하는지를 확인하는 로직은 UserDetailsService를 구현한 CustomUserDetailsService에 있기에 이곳을 검토해봐야 합니다.

 

CustomUserDetailsService.java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    User user = userRepository.findByEmail(email).orElseThrow(() ->
        new UsernameNotFoundException("Authentication failed"));

    return UserPrinciple.builder()
                        .user(user)
                        .build();
  }
}

지금은 회원이 없을 때, UsernameNotFoundException이지만, 기존에는 Custom한 Exception인 BusinessException을 Throw했습니다.

 

그렇다면 Filter에서 Exception 컨트롤을 했으므로, BusinessException이 뜨면서 종료가 되면 문제가 없는데..

unsuccessfulAuthentication를 타면서 Exception을 디버깅해보면, 저는 보낸 적 없는 InternalAuthenticationServiceException가 나옵니다. 콩 심은데 콩 나고 팥 심은데 팥 난다는 속담이 의미가 없어지는.. 그런 상황인 거죠..

 

자, 이제 동작 방식을 이해해 볼 차례입니다.

 

UserDetailsService 클래스의 loadUserByUsername 메서드에서 회원이 존재하지 않을 때 발생하는 BusinessException은 Spring Security에서 InternalAuthenticationServiceException로 래핑이 됩니다. 래핑을 원치 않을 경우, DaoAuthenticationProvider를 커스터마이징하거나 Spring Security에서 인증 실패로 처리하기 위한 표준 예외인  UsernameNotFoundException를 던져서 BadCredentialsException로 받아 처리하는 방법이 있습니다.

 

필터를 추가해 복잡하게 처리하기보다는 표준 방식을 따르기로 하였습니다.

이 때, 걱정을 했던 부분은 회원이 가입은 했지만 비밀번호가 틀린 경우와 회원이 가입을 하지 않았을 때 구분하는 것이었습니다.

하지만, 요새 로그인을 시도해보시면 아실 수도 있겠지만 비밀번호를 틀렸다는 말을 잘 안해줍니다. 아이디나 비밀번호를 다시 확인하라고 합니다. Spring Security에서는 보안상 의도한 것이라하여, 저도 구분하지 않기로 했습니다.

 

정리하자면, LoginFilter에서 인증 과정을 거치기 위해 UserDetailsService을 구현한 CustomUserDetailsService에서 Custom Exception을 발생시켰지만, 내부적으로 InternalAuthenticationServiceException로 래핑되는 문제가 있었습니다. Filter를 추가하여 복잡성을 올리기보다는 표준 방식을 따르기로 했고, 이에 따라 Custom한 Exception보다는 UsernameNotFoundException을 발생시켜 unsuccessfulAuthentication에서 BadCredentialsException을 처리하는 방식으로 수정하였습니다.

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

Chat GPT

https://hj0216.tistory.com/963

 

[1년 후 마실가실] Spring Security + JWT - LoginFilter

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.  요즘 제 표정입니다. Spring Security Exception 처리에 막혀서, 이건.. 이건.. 학습이 필요하다는 생각이 간절히 들어

hj0216.tistory.com

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html#page-title

 

UserDetailsService :: Spring Security

This is only used if the AuthenticationManagerBuilder has not been populated and no AuthenticationProviderBean is defined.

docs.spring.io

 

728x90
728x90

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

https://kr.pinterest.com/pin/578431145923739290/

(이미지와는 크게 상관없는 내용인데, 귀여워서 넣어봤습니다.)

 

여러분은 모르셨겠지만, 최근에 얼추 갖춰진 회원 CRUD가 끝났습니다.. 야호..

아직도 수정해야할 건 산더미이긴 하지만, 부담갖지 않고 해보고싶은 것들, 궁금한 것들을 위주로 리팩토링을 하자,, 라고 마음을 다지고 있습니다🚀.

 

지금은 큰 산이라고 생각한 이미지를 시작해보기 전에 코드를 좀 다듬는 중입니다.

 

그래서 이번에는 조금 간단한 내용으로 정적분석도구 SonarQube를 어떻게 쓰고 있는지를 짧게 남겨보고자 합니다.

 

SonarQube 사용하기

 

GlobalExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
	  private ErrorResponse makeErrorResponse(BindException e, ErrorCode errorCode) {
    List<ErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
                                                               .getFieldErrors()
                                                               .stream()
                                                               .map(
                                                                   ErrorResponse.ValidationError::of)
                                                               .collect(Collectors.toList());

    return ErrorResponse.builder()
                        .code(errorCode.name())
                        .message(errorCode.getMessage())
                        .errors(validationErrorList)
                        .build();
  }
}

 

* e.getBindingResult()

  * 회원 가입 시, 응집도를 높이기 위해 어노테이션을 사용해 각 필드값의 유효성 검사를 수행

    이 때, 유효성 검사에 실패할 경우, 해당 결과가 BindningResult에 저장

 

* getFieldErrors()

  * 유효성 검증에서 실패한 필드 단위의 오류 목록을 반환

  * FieldError 객체들로 구성된 List<FieldError>

[
  FieldError(field="email", rejectedValue="invalid@", message="Invalid email format"),
  FieldError(field="password", rejectedValue="123", message="Password is too short")
]

 

* stream()

  * 컬렉션 또는 배열과 같은 데이터를 함수형 프로그래밍 스타일로 처리할 수 있도록 지원하는 데이터 처리 도구

  * 가독성, 확장성 등을 위해서 해당 리스트를 stream으로 변환

// 일반
List<ValidationError> validationErrorList = new ArrayList<>();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
    validationErrorList.add(ValidationError.of(fieldError));
}

// Stream
List<ValidationError> validationErrorList = e.getBindingResult()
                                             .getFieldErrors()
                                             .stream()
                                             .map(ValidationError::of)
                                             .toList();

 

* map(ErrorResponse.ValidationError::of)

  * 기존 데이터의 각 요소를 가공하거나 다른 타입으로 변경할 때 사용

  * 스트림의 각 FieldError 객체를 ErrorResponse.ValidationError.of()를 사용하여 ErrorResponse.ValidationError 객체로 변환

 

* (기존) collect(Collectors.toList())

  * Collectors.toList(): mutable List(수정 가능한 리스트)를 반환

  * Validation Error는 한 번 발생한 이후, 수정될 가능성이 없으므로 Immutable List로 반환하는 것이 더 안정적

  * Java16 이상에서는 Stream.toList() / Java10 이상에서는 Collectors.toUnmodifiableList()로 Immutable List로 사용 가능

 

불변 리스트를 사용하여,

  1. 해당 코드의 의도를 명확히하고 

  2. 수정 작업이 불가능하므로 멀티 스레드 환경에서 동기화 작업에 들어가는 비용을 낮춰, 멀티스레드 환경에서 더 높은 성능과 낮은 복잡도를 제공

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

728x90
728x90

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

 

Filter에서 Exception 처리가 되지 않아 길을 잃은지 어느덧 한 달..

드디어 필터를 활용하여 Exception 처리까지 왔습니다..

 

의도치않은 리리팩토링으로 인해 기능 구현은 제자리지만, 포기하지 않으면 과정입니다..

파이팅..⭐

 

 

 

그간의 이야기를 잠깐 해보자면, 저에겐 @RestControllerAdvice를 선언한 GlobalExceptionHanlder가 있었습니다(지금도 있습니다^^). GlobalExceptionHanlder는 Controller로 들어온 Exception을 처리하는데, Filter에서 오류가 발생하면 Controller로 들어오지 않아 Exception 처리가 안되던 문제가 있었습니다..

 

그래서, 지금은 ⭐ Filter 전용 Exception Filter⭐를 만들고자 합니다.

 

ExceptionHandlerFilter.java
@Component
@Slf4j
public class ExceptionHandlerFilter extends OncePerRequestFilter {

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    try {
      filterChain.doFilter(request, response);
    } catch (BusinessException e) {
      setErrorResponse(response, e.getErrorCode());
    }
  }

  private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) {
    response.setStatus(errorCode.getHttpStatus().value());

    ErrorResponse errorResponse = ErrorResponse.builder()
                                               .code(errorCode.name())
                                               .message(errorCode.getMessage())
                                               .build();

    try {
      ObjectMapper mapper = new ObjectMapper();
      mapper.enable(SerializationFeature.INDENT_OUTPUT);

      String jsonResponse = mapper.writeValueAsString(errorResponse);
      response.getWriter().write(jsonResponse);

    } catch (IOException e) {
      log.error("Error writing response: {}", e.getMessage(), e);
    }
  }
}

 

기존에 사용하던 Error Response와 동일한 형식으로 작성하였습니다.

JSON 형식으로 Error를 반환해주고 싶기에 ObjectMapper를 사용했습니다.

기본값으로 ObjectMapper는 데이터가 한 줄로 출력이되기에, 들여쓰기 옵션도 추가해주었습니다.

 

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  // 생략

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    http.httpBasic(AbstractHttpConfigurer::disable);

    http.csrf(AbstractHttpConfigurer::disable);

    http.cors(httpSecurityCorsConfigurer ->
        corsConfigurationSource()
    );

    http.sessionManagement(sessionManagement ->
        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );

    http.authorizeHttpRequests(auth ->
        auth
            .requestMatchers(PERMIT_ALL_URL).permitAll()
            .requestMatchers(NEED_ROLE_URL).hasRole("USER")
            .anyRequest().authenticated()
    );
    http.addFilterBefore(characterEncodingFilter(), CsrfFilter.class);
    http.addFilterBefore(new JWTFilter(jwtUtils), LoginFilter.class);

    LoginFilter loginFilter = new LoginFilter(
        authenticationManager(authenticationConfiguration)
        , jwtUtils
        , redisUtils);
    loginFilter.setFilterProcessesUrl("/api/v2/users/login");
    http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);

    http.addFilterBefore(new CustomLogoutFilter(jwtUtils, redisUtils), LogoutFilter.class);

    http.addFilterAfter(new ExceptionHandlerFilter(), SecurityContextPersistenceFilter.class);

    return http.build();
  }
}

Exception을 담당하는 필터를 새로 만들어줬기에 SecurityConfig에 추가를 해줍니다.

다른 모든 필터에서 발생한 예외를 한 곳에서 처리하고자 Spring Security 필터 체인의 마지막에 위치하는 SecurityContextPersistenceFilter의 뒤에 설정하였습니다.

 

LoginFilter.java
@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

  // 생략

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException failed) {
    log.error("Login failed: ", failed.getMessage());

    throw new BusinessException(INVALID_CREDENTIALS);
  }
}

기존에는 response에 status만 setting을 했었는데, 지금은 BusinessException을 throw했습니다.

제 ErrorResponse는 CustomErrorCode라는 별도의 Enum을 활용해서 code와 메시지를 리턴해주는데, 동일한 형식을 가져가려고 CustomException을 발생시켰습니다.

그래서 Filter에서 BusinessException이 발생하면, 최종 단계의 필터인 ExceptionHandlerFilter에서 catch로 잡혀 Error 메시지를 리턴하게 됩니다.

 

Exception에 따른 response 처리와 비즈니스 로직이 분리되어 단일 책임을 향해가는 기분이 듭니다🥸.

뿌듯하네요 🥸 .

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://thalals.tistory.com/451

 

Spring Security Exception Handling - Filter 단 예외 처리하기

오늘은 Spring Security 를 적용했지만 JWT 가 만료되거나, 잘못된 토큰일 경우 401 코드 뿐만아니라 에러 메세지까지 핸들링 해줄 수 있도록 설정해 주고자 합니다. 1. Spring Security 와 @ControllerAdvice 먼

thalals.tistory.com

 

728x90
728x90

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

애플리케이션 껐다키면 userId가 50 증가한 썰..

 

Spring Security를 전체적으로 리리팩토링을 하고 Postman으로 전체적인 동작을 확인하고 있습니다.

 

그러다 문득, DB에서 UserId값을 보는데, 2000번을 넘었더라고요.

내가 Create 메서드를 2000번이나 했으려나..? 라는 생각과 함께 Postman으로 Id값이 제대로 증가하는지 확인해보다 애플리케이션을 껐다 키면 50이 증가해버리는 문제를 발견했습니다.

 

간단하게 고쳐보는 과정을 남겨봅니다..

 

Hibernate5부터 MySQL DB로 @GeneratedValue를 사용해서 AUTO를 strategy로 선택하게 되면, GenerationType.TABLE이 선택됩니다.

이 때, 애플리케이션을 껐다가 다시 실행하면 값이 50씩 증가하는 이유는 Hibernate가 테이블 기반 시퀀스를 사용하는데, 기본적으로 증가 단위를 50으로 설정하기 때문입니다. 이는 성능을 위해서 미리 ID를 확보해 놓는 방식이며, 애플리케이션이 종료되면 캐시된 ID들이 사라져서 다음 시작 시 새로운 ID 할당이 시작됩니다.

이를 해결하기위해서는 allocationSize를 명시적으로 지정하면 됩니다.

User.java
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "my_table_generator")
  @TableGenerator(name = "my_table_generator", allocationSize = 1)
  @Column(name = "user_id")
  private Integer id;
}

 

이렇게 하면 해결은 되는데, 문제는 TABLE 전략을 사용하는 것에 대해 고민을 해봐야 한다는 것입니다..

 

왜냐면, Table 전략은 모든 Table들이 하나의 Sequence 테이블을 두고 사용하는 전략인데

1. 여러 트랜잭션이 동시에 ID를 할당받으려 하면 ID 테이블에 대해 잠금(lock)이 발생할 수 있고,

2. TABLE 전략을 사용할 때는 allocationSize와 같은 설정을 최적화해야 하는데, 그렇지 않으면 ID 증가 단위가 크거나, 중간에 간격이 생기는 등 예상치 못한 문제들이 (저처럼;;) 발생할 수 있기 때문입니다.

 

이에 반해 Identity 전략은 DB에서 직접 ID를 할당하는 방식인데, MySQL의 경우 AUTO_INCREMENT 기능을 사용합니다.

ID 생성 시 별도의 테이블 접근이 필요하지 않으므로, 성능 상의 이점이 있습니다.

또한, 추가 설정 없이 MySQL의 기본 기능을 사용하기 때문에, 설정이 복잡하지 않고 유지보수도 상대적으로 간단합니다.

 

따라서 MySQL에서는 성능과 관리 효율성 측면에서 @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하는 것이 일반적으로 권장됩니다. 

 

User.java
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "user_id")
  private Integer id;
}

 

🚨 Auto Increment를 사용하기 때문에 MySQL에서 userId값에 해당 설정이 추가되어 있어야 합니다.

또한, 기존에 사용했던 Id값보다 +1 값부터 증가시켜줘야 하므로, 추가적인 설정이 필요합니다.

-- AUTO_INCREMENT 추가
ALTER TABLE user MODIFY user_id INT NOT NULL AUTO_INCREMENT;

-- 외래키 조건이 있을 경우, 임시 삭제
ALTER TABLE trip DROP FOREIGN KEY 외래키_이름;

-- 외래키 재설정
ALTER TABLE trip
ADD CONSTRAINT 외래키_이름
FOREIGN KEY (user_id) REFERENCES user(user_id);

-- AUTO INCREMENT 시작 번호 지정
ALTER TABLE user AUTO_INCREMENT = 기존_USERID보다_1_더_큰_값;

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://dev-box.tistory.com/101#google_vignette

 

[Jpa] 실전을 위한 JPA - #4 @GeneratedValue 컬럼 시퀀스 전략 및 성능 개선(튜닝)

이번 포스팅에서는 저번 포스팅(#3 엔티티(Entity) 기본 어노테이션)에서 공부했던 것 중에서 @GeneratedValue 에 대해 조금 더 깊게 파헤쳐보려합니다. 그럼 @GeneratedValue 의 자세한 내용을 공부하기 전

dev-box.tistory.com

 

728x90
728x90

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

최근에 Security Filter를 리리팩토링하면서 기본 HttpStatus 값을 반환하여, Custom한 Error Code가 쓸 일이 없어졌습니다.

 

하지만, 쓰라고 만들어둔 것이기에 코드를 조금씩 바꿔서 response에 조심스럽게 담아 고객님께 전달해드렸는데 한글이 출력이 안됩니다.

 

하지만, 이 곳은 한국.. 한국어가 나와야 합니다..

 

검색해서 나온 방법들은 통하지 않아 네이버 블로그와 GPT와 함께 열심히 찾은 결과를 작성해둡니다..

 

(실패) application.yml

전역 설정의 중앙 허브.. application.yml 설정으로 먼저 시작해 봤습니다.

spring:
  servlet:
    encoding:
      charset: UTF-8
      force: true

 

charset을 UTF-8로 강제로 바꾸겠다는 의지는 통하지 않았습니다.

 

 

(실패) Filter.java
Response의 Header와 ContentType 설정

가장 많이 나온 해결 방법.. 하지만 제가 Postman에게 전달받은 글자는 물음표 뿐..

이 말은 즉, Postman도 저도 모르겠다는 의미입니다..

private void setResponse(HttpServletResponse response, ErrorCode errorCode) {
  response.setCharacterEncoding("UTF-8");
  response.setContentType("text/plain; charset=UTF-8");
  response.setStatus(errorCode.getHttpStatus().value());

  try {
    PrintWriter writer = response.getWriter();
    response.getWriter().write(errorCode.getMessage());
  } catch (IOException e) {
    log.warn(e.getMessage());
  }
}

 

 

(성공🎉)SecurityConfig.java
CharacterEncodingFilter 사용
public class SecurityConfig {
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.addFilterBefore(characterEncodingFilter(), CsrfFilter.class);
  }


  @Bean
  public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding("UTF-8");
    filter.setForceEncoding(true);
    return filter;
  }  
}

 

자, 이제 2가지 의문점을 해결해 봅시다.

1. 왜 Filter 설정을 해야만 한글을 볼 수 있는가.

응답에서 직접 한글 인코딩을 진행할 경우, 이미 초기화된 기본 인코딩을 덮어쓰지 못하는 경우가 발생할 수 있음

 

response.getWriter()를 호출하면 서블릿 컨테이너가 해당 응답을 "커밋"(commit)하게 되는데, 이 과정에서 콘텐츠 타입과 인코딩이 이미 지정된 상태로 고정됨

→ 그 후, setHeader("Content-Type", "text/plain; charset=UTF-8")나 setCharacterEncoding("UTF-8") 같은 설정이 더 이상 반영되지 않음

→ 한글이 제대로 출력되지 않음

  * 기존 코드의 경우, Header 설정을 getWriter() 호출 전 사용하긴 했으나, Spring Security가 적용된 환경에서는 인코딩 설정이 제대로 반영되지 않을 가능성이 있음

    Spring Security나 다른 필터들이 응답 처리를 할 때, 인코딩 설정을 덮어쓰거나 변경하는 경우가 발생할 수 있기 때문

 

2. csrfFilter는 누구시고, 왜 그전에 설정해야 하는가.

csrfFilter: 사이트 간 요청 위조를 방지하는 필터, 요청마다 고유한 CSRF 토큰을 생성하고 이를 검증하는 역할

  * CSRF 토큰 : 사용자가 서버에 로그인하거나 새 세션을 시작하면, 서버는 CSRF 토큰을 생성하고 이를 세션에 저장

  * CSRF: 악의적인 사이트가 사용자를 대신해 신뢰된 사이트에 요청을 보내게 하여 피해를 입히는 공격 방식

    예: 사용자가 은행에 로그인한 상태에서 악의적인 사이트를 방문하면 악의적인 사이트가 사용자 몰래 계좌 이체 등의 요청을 보낼 수 있음

 

CharacterEncodingFilter는 요청과 응답의 문자 인코딩을 설정하는 필터로, 요청 본문이나 응답에 대한 인코딩을 처리함
인코딩 여부에 영향을 미치는 필터들 중 가장 앞에 위치해야 하고, 보통 인코딩 영향을 받는 필터 중 첫 번째가 CSRF 필터
CSRF 토큰 검증 필터(CsrfFilter) 이후 설정 시, CsrfFilter나 그 외 다른 보안 필터가 요청 본문에 접근할 때 이미 잘못된 인코딩이 적용될 수 있음
CsrfFilter가 요청의 인코딩을 고려하지 않고 바로 검증을 수행하는 구조이기 때문에, 인코딩이 올바르게 적용되지 않은 상태에서 필터가 실행되면 CSRF 필터를 포함한 이후 필터들이 요청을 잘못 해석할 수도 있게 됨
CSRF 보호가 비활성화되어도 다른 필터들이 요청 본문이나 응답을 처리할 때 올바른 인코딩이 적용되어야 하기 때문에, CharacterEncodingFilter는 CSRF 설정과 무관하게 항상 앞단에 두는 것이 권장

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://velog.io/@choonbok22/230314-TIL-32

 

230314 TIL #32 getWriter() 한글 깨짐

Spring 3주차.1 getWriter() 한글 깨짐

velog.io

https://m.blog.naver.com/haengro/220549463106

 

Spring에서의 한글깨짐 문제 (Spring Security 사용시)

개인 프로젝트를 진행하기위해 가장 최신버전의 Spring을 적용해서 웹페이지를 개발하기로 결정했다 그외...

blog.naver.com

 

 

728x90