BE/Spring

[Spring Security] JSON으로 로그인 및 응답하기

멍목 2023. 12. 10. 18:04
반응형

안녕하세요.

 

이번 포스팅에서는 Spring Security에서 JSON 형식으로 로그인하는 방법에 대해서 알아보겠습니다.

아래의 내용에서 자신의 개발 방향에 맞게 커스텀하시면 될 듯 합니다.


1. UsernamePasswordAuthenticationFilter 살펴보기

로그인 요청이 들어오면, 인증 필터를 거쳐 UsernamePasswordAuthenticationFilter에서 ID와 PW를 처리합니다.

 

UsernamePasswordAuthenticationFilter

- SPRING_SECURITY_FORM_UESRNAME_KEY, SPRING_SECURITY_FORM_PASSWORD_KEY : username과 password 인자 값 설정

- DEFAULT_ANT_PATH_REQUEST_MATCHER : 로그인 요청 URL 설정

 

 

 

UsernamePasswordAuthenticationFilter - attemptAuthentication

위의 소스는 UsernamePasswordAuthenticationFilter의 attemptAuthentication 메소드 부분인데,

기본적으로 POST를 이용한 요청만을 받을 수 있고, username과 password를 obtain이라는 함수를 이용해서 가져온다.

 

 

 

UsernamePasswordAuthenticationFilter - obtainPassword, obtainUsername

위의 obtatin 이라는 함수는 단순하게 request.getParameter를 이용해서 유저의 입력 값을 가져온다.

 

 

 

2. UsernamePasswordAuthenticationFilter에서 JSON도 받을 수 있도록 커스터마이징하기

 

CustomUsernamePasswordAuthenticationFilter.java

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.authentication.AuthenticationServiceException;
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 org.springframework.util.MimeTypeUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("CustomUsernamePasswordAuthenticationFilter - attemptAuthentication START");
        UsernamePasswordAuthenticationToken authenticationToken = null;

        String userId = null;
        String userPassword = null;

        // JSON 요청일 경우
        if (request.getContentType().equals(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
            try{
                // ObjectMapper를 이용해서 JSON 데이터를 dto에 저장 후 dto의 데이터를 이용

                LoginDto loginDto = objectMapper.readValue(
                        request.getReader().lines().collect(Collectors.joining()), LoginDto.class);

                userId = loginDto.getUserId();
                userPassword = loginDto.getUserPassword();

                logger.info("JSON 접속. USERID : " + userId + ", USERPW : " + userPassword);
            } catch(IOException e){
                e.printStackTrace();
            }

        // POST 요청일 경우 기존과 같은 방식 이용
        } else if(request.getMethod().equals("POST")){
            userId = obtainUsername(request);
            userPassword = obtainPassword(request);

            logger.info("POST 접속. USERID : " + userId + ", USERPW : " + userPassword);
        }
        else {
            logger.error("POST / JSON 요청만 가능합니다.");
            throw new AuthenticationServiceException("Authentication Method Not Supported : " + request.getMethod());
        }

        if(userId.equals("") || userPassword.equals("")){
            System.out.println("ID 혹은 PW를 입력하지 않았습니다.");
            throw new AuthenticationServiceException("ID 혹은 PW를 입력하지 않았습니다.");
        }

        authenticationToken = new UsernamePasswordAuthenticationToken(userId, userPassword);
        this.setDetails(request, authenticationToken);
        System.out.println("CustomUsernamePasswordAuthenticationFilter - attemptAuthentication END");
        return this.getAuthenticationManager().authenticate(authenticationToken);

    }

    @Getter
    @Setter
    @ToString
    private static class LoginDto {
        private String userId;
        private String userPassword;
    }
}

 

 

3. 로그인 성공/실패 시, JSON 형태로 성공/실패 메시지 보내는 Handler 생성하기

로그인 성공 or 실패 시, JSON 형태로 다시 데이터를 보내줘서 Front 단에 메시지를 띄워줘야 하는 경우 SuccessHandler, FailureHandler에서 JSON 형식으로 응답해준다.

 

CustomSuccessHandler.java

import com.withus.withusApp.config.security.auth.PrincipalDetails;
import com.withus.withusApp.config.security.dto.JsonDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest != null) {
            requestCache.removeRequest(request, response);
            clearAuthenticationAttributes(request);
        }

        String accept = request.getHeader("accept");

        PrincipalDetails principalDetails = null;
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principal != null && principal instanceof UserDetails) {
                principalDetails = (PrincipalDetails) principal;
            }
        }

        // 로그인 시, JSON 으로 반환
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        MediaType jsonMimeType = MediaType.APPLICATION_JSON;

        String message = "로그인 성공";
        JsonDto jsonDto = JsonDto.success(principalDetails, message);
        if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
            jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
        }
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

 

 

CustomFailureHandler.java

import com.withus.withusApp.config.security.dto.JsonDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("Exception Type : " + exception.getClass().getName());
        logger.info("Exception Message : " + exception.getMessage());

        // 로그인 시, JSON 으로 반환
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        MediaType jsonMimeType = MediaType.APPLICATION_JSON;

        String message = "로그인에 실패하였습니다.";
        JsonDto jsonDto = JsonDto.fail(message);
        if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
            jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
        }
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

 

 

추가) 로그아웃 시 성공했다는 메세지를 담은 Handler는 아래와 같다.

CustomLogoutSuccessHandler.java

import com.withus.withusApp.config.security.dto.JsonDto;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //super.onLogoutSuccess(request, response, authentication);

        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        MediaType jsonMimeType = MediaType.APPLICATION_JSON;

        String message = "로그아웃 완료";
        JsonDto jsonDto = JsonDto.success(null, message);
        if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
            jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
        }
    }
}

 

 

4. Spring Security Config 설정하기

import com.withus.withusApp.config.security.filter.*;
import com.withus.withusApp.config.security.oauth2.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity      // Spring Security Filter 가 Spring Filter Chain 에 등록되도록
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * Spring Security Setting
     * @param http
     * @Author Seong-Mok Kim
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception{
    
        // CORS 설정 및 CSRF 비활성화
        http.cors().configurationSource(corsConfigurationSource()).and().csrf().disable();



        http
            // URL에 따른 접근 제한 처리
            .authorizeRequests()
                .antMatchers("/api/user/**").authenticated()        // URL user : 인증이 되어야 함
                .antMatchers("/api/admin/**").access("hasRole('ROLE_ADMIN')")         // URL admin : 권한 'ROLE_ADMIN'이 있어야함
                .anyRequest().permitAll()      // 위에 명시되지 않은 URL 은 로그인 및 권한 검사 X
            .and()
            // 로그인 설정
                .formLogin().disable()      // 기존의 로그인 비활성화 후 
                .addFilterAt(getAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)     // 새로 만들어준 CUSTOM 필터를 넣어줌
            .logout()
                .logoutUrl("/api/logout")
                .logoutSuccessHandler(logoutSuccessHandler())		// LogoutSuccessHandler 설정
        ;
    }

    // JSON 방식으로도 로그인 가능하도록 설정한 Filter
    protected CustomUsernamePasswordAuthenticationFilter getAuthenticationFilter() {
        CustomUsernamePasswordAuthenticationFilter authFilter = new CustomUsernamePasswordAuthenticationFilter();
        try {
            authFilter.setFilterProcessesUrl("/api/login");		// login 주소 설정
            authFilter.setAuthenticationManager(this.authenticationManagerBean());
            authFilter.setUsernameParameter("userId");			// username 설정
            authFilter.setPasswordParameter("userPassword");	// password 설정
            authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());		// SuccessHandler 설정
            authFilter.setAuthenticationFailureHandler(authenticationFailureHandler());		// FailuerHandler 설정
        } catch (Exception e) {
            e.printStackTrace();
        }
        return authFilter;
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler(){
        return new CustomSuccessHandler();
    }

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

    @Bean
    public LogoutSuccessHandler logoutSuccessHandler(){
        return new CustomLogoutSuccessHandler();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOrigin("http://localhost:3000");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

 

 

 

참고 블로그

https://johnmarc.tistory.com/74

https://jungeunlee95.github.io/java/2019/07/18/8-Spring-Security-ajax-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9B%84-json%EC%9D%91%EB%8B%B5%EB%B0%9B%EA%B8%B0/

 

반응형