[Spring Security] 최신! 로그인 결과를 ResponseBody에 나타내는 법
2024. 3. 5. 15:55ㆍBE/Spring
본 포스팅은 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);
}
}
추가 작업
- 하지만 이렇게 로그인 실패를 다룰 때, 어떤 이유 때문에 로그인이 실패했는지는 나눠서 다룰 수 없다는 한계가 존재.
- 따라서 로그인 실패의 원인을 나눠서 다룰 수 있도록 코드를 수정할 예정.
'BE > Spring' 카테고리의 다른 글
[Spring JPA] 영속성 컨텍스트의 기능 (2) | 2024.03.07 |
---|---|
[Spring Security] URI에 따라 접근 권한 부여하기 최신 버전 (0) | 2024.03.05 |
[Spring] Failed to load remote configuration. (0) | 2024.03.01 |
[Spring] 의존성 주입(DI)과 제어의 역전(IoC) (0) | 2024.02.25 |
[Spring] 초간단! 인자의 종류 (0) | 2024.02.24 |