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

2023. 12. 10. 18:04· BE/Spring
반응형

안녕하세요.

 

이번 포스팅에서는 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/

 

반응형

'BE > Spring' 카테고리의 다른 글

JdbcTemplate를 이용한 데이터 통신  (1) 2024.09.19
[Spring] Inteceptor prehandle 에서 response 넘기는 방법  (0) 2023.04.06
[Spring Security] React(Front)와 Spring Security 설정 방법  (2) 2022.07.19
[Spring Security] JWT - 4. JWT 로그인 및 권한 처리  (0) 2022.06.19
[Spring Security] JWT - 3. JWT 임시 토큰 테스트 및 로그인 시 토큰 생성  (0) 2022.06.18
'BE/Spring' 카테고리의 다른 글
  • JdbcTemplate를 이용한 데이터 통신
  • [Spring] Inteceptor prehandle 에서 response 넘기는 방법
  • [Spring Security] React(Front)와 Spring Security 설정 방법
  • [Spring Security] JWT - 4. JWT 로그인 및 권한 처리
멍목
멍목
개발 관련 새롭게 알게 된 지식이나 좋은 정보들을 메모하는 공간입니다.
반응형
멍목
김멍목의 개발블로그
멍목
전체
오늘
어제
  • 분류 전체보기 (514)
    • BE (190)
      • Spring (21)
      • Java (141)
      • Kotlin (6)
      • JPA (22)
    • FE (33)
      • Javascript (16)
      • Typescript (0)
      • React (5)
      • Vue.js (9)
      • JSP & JSTL (3)
    • DB (32)
      • Oracle (22)
      • MongoDB (10)
    • Algorithm (195)
    • Linux (8)
    • Git (6)
    • etc (42)
    • ---------------------------.. (0)
    • 회계 (4)
      • 전산회계 2급 (4)
    • 잡동사니 (2)

블로그 메뉴

  • 홈
  • 관리

공지사항

인기 글

태그

  • 코테 공부
  • 코테공부
  • junit5
  • Java to Kotlin
  • JPA
  • 자기 공부
  • 자바 테스팅 프레임워크
  • 자바 공부
  • 이펙티브자바
  • 자바 개발자를 위한 코틀린 입문
  • 프로젝트로 배우는 Vue.js 3
  • 알고리즘공부
  • java 8
  • 코틀린
  • 더 자바 애플리케이션을 테스트하는 다양한 방법
  • MongoDB with Node.js
  • 자기개발
  • 자기공부
  • 전산회계 2급 준비
  • JPA 공부
  • 더 자바 Java 8
  • 알고리즘 공부
  • Effective Java
  • Oracle
  • 이펙티브 자바
  • MongoDB 공부
  • MongoDB 기초부터 실무까지
  • 자기 개발
  • 자바공부
  • vue3 공부

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
멍목
[Spring Security] JSON으로 로그인 및 응답하기
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.