[Spring Security] 최신! 로그인 결과를 ResponseBody에 나타내는 법

2024. 3. 5. 15:55BE/Spring

728x90

본 포스팅은 Spring Security에서 로그인 결과를 ResponseBody에 나타내는 법을 다룹니다.

문제 상황

  • Spring Security를 이용하여 인증 및 인가를 진행하고, JWT를 인증방식으로 사용하여 UserDetails, UserDetailsService를 상속받아서 사용하고 있음. 
  • 아래와 같이 로그인을 구현할 때, Swagger ResponseBody에 아무것도 표현되지 않는 상태
        @Operation(summary = "유저 로그인", description = "유저 로그인 사용할 정보를 입력합니다.")
        @PostMapping("/user/login")
        public ResponseEntity<?> login(@RequestBody LoginRequestDto requestDto) {
            try {
                return ResponseEntity.ok().body(ResponseDto.success("로그인", "로그인이 성공적으로 이루어졌습니다."));
            } catch (Exception e) {
                throw new CustomException(ErrorCode.LOGIN_FAILED);
            }
        }​

기대 상황

  • 로그인에 성공했을 때, "로그인이 성공적으로 이루어졌습니다."가 ResponseBody에 출력되길 바람
  • 로그인에 실패했을 때, 사전에 정의한 ErrorCode가 출력되길 바람

시도 방법

  • chatGPT한테 물어보니 Handler를 Security Config 파일에 정의해서 붙여주라고함 -> 많은 블로그에서 이 방법을 이용하여 문제를 해결하고있었음.
    • 하지만, formlogin을 이용하지 않고 백엔드 서버만을 이용하여 테스트하여야했기 때문에 formlogin을 사용할 수 없었음. 
    • 또한, '아래의 방법은 실패했을 때, 에러를 핸들링하는 것인데, 그것과 별개로 로그인할 때는 정상적으로 메세지가 출력되어야하는 것이 아닌가?'라는 생각이 들었음. 하지만, 현재 상황에서는 로그인 메세지도 response가 되지 않는 상황.
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Operation(summary = "유저 로그인", description = "유저 로그인 사용할 정보를 입력합니다.")
@PostMapping("/user/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDto requestDto, HttpServletRequest request, HttpServletResponse response) {
    // 인증 처리 로직
    try {
        // 로그인 성공 시
        return ResponseEntity.ok().body(ResponseDto.success("로그인", "로그인이 성공적으로 이루어졌습니다."));
    } catch (Exception e) {
        // 로그인 실패 시
        authenticationFailureHandler.onAuthenticationFailure(request, response, new AuthenticationException("인증 실패") {});
        throw new CustomException(ErrorCode.LOGIN_FAILED);
    }
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new SimpleUrlAuthenticationFailureHandler();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/user/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginProcessingUrl("/user/login")
                .failureHandler(authenticationFailureHandler()) // 인증 실패 핸들러 설정
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .and()
            .csrf().disable(); // CSRF 보호 비활성화 (테스트 용이성을 위해)
    }
}

해결 방법

  • 선술했듯이, 로그인이 성공했을 때도 메세지가 반환이 안되는 것으로 보아서 다른 곳에서 로그인을 처리하고 있다는 사실을 인지 
  • Security Config File을 보았을 때, AuthenticationFilter에서 먼저 filtering하고 다루고 있다는 사실을 알게됨.
  • AuthenticationFilter에서 successfulAuthentication, unsuccessfulAuthentication에서 이를 다루고 있었음. 따라서 이 부분을 원하는 형식대로 수정해주면됨.
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 필터 관리 -> 새로만든 JWT 기반 spring security filter를 언제 어디서 호출할 지 정함.
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.csrf().disable().build();
    }
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.sixth_week_lv4.common.exception.CustomException;
import com.sparta.sixth_week_lv4.common.exception.ErrorCode;
import com.sparta.sixth_week_lv4.dto.login.LoginRequestDto;
import com.sparta.sixth_week_lv4.entity.UserRoleEnum;
import com.sparta.sixth_week_lv4.security.UserDetailsImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { // 이렇게 하는 방식은 session 방식임. 이것을 구현하는 이유는 우리는 JWT를 이용하여 인증을 진행하기 때문임.
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("로그인 시도");
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
            return getAuthenticationManager().authenticate( // 클라이언트로부터 입력받은 유저 이름과 패스워드를 통해서 토큰을 만들고, Authentication
                    new UsernamePasswordAuthenticationToken(requestDto.getEmail(), requestDto.getPassword(), null));
        } catch (IOException e) {
            throw new CustomException(ErrorCode.LOGIN_FAILED);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("로그인 성공 및 JWT 생성"); // attemptAuthentication 메소드에서 받은 Authentication 객체가 인증되었는지 아닌지를 판단하여, 성공한 경우 수행함.
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getAuthority();
        String token = jwtUtil.createToken(username, role);
        log.info("JWT " + token); // attemptAuthentication 메소드에서 받은 Authentication 객체가 인증되었는지 아닌지를 판단하여, 성공한 경우 수행함.
        jwtUtil.addJwtToCookie(token, response); // 로그인에 성공한 경우, JWT 토큰을 발급함.
        response.setCharacterEncoding("UTF-8");
        setSuccessResponse(response, ErrorCode.LOGIN_SUCCESS.getMessage());
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("로그인 실패");
        response.setCharacterEncoding("UTF-8");
        setFailResponse(response, ErrorCode.LOGIN_FAILED.getMessage());
    }

    private void setFailResponse(HttpServletResponse response, String msg) throws IOException {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setContentType("application/json");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("resultCode", ErrorCode.LOGIN_FAILED.getKey());
        jsonObject.put("message", "로그인");
        jsonObject.put("data", msg);
        response.getWriter().print(jsonObject);
    }
    private void setSuccessResponse(HttpServletResponse response, String msg) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("resultCode", "SUCCESS");
        jsonObject.put("message", "로그인");
        jsonObject.put("data", msg);
        response.getWriter().print(jsonObject);
    }
}

추가 작업

  • 하지만 이렇게 로그인 실패를 다룰 때, 어떤 이유 때문에 로그인이 실패했는지는 나눠서 다룰 수 없다는 한계가 존재.
  • 따라서 로그인 실패의 원인을 나눠서 다룰 수 있도록 코드를 수정할 예정.