springframework/시작하자SpringSecurity

35.AjaxAuthenticationFilter

Jungsoomin :) 2020. 9. 29. 19:39

Ajax 요청방식에 대한 스프링 시큐티티의 전반적인 순차과정 중 처음인 AjaxAuthenticationFilter를 구현한다.

 

  1. AbstractAuthenticationProcessingFilter 상속.
  2. 작동조건AntPathRequestMatcher 로 매칭, 요청방식이 Ajax 라면 작동
  3. AjaxAuthenticationToken  구현객체를 만들어 AuthenticationManager에게 전달


구현

  • AbstaractAuthenticationProcessingFilter 의 생성자를 가져와 AntPathRequestMatcher 에 경로를 설정한 것을 주목
  • isAjax 메서드를 만들어 X-Request-With 헤더 정보에 XMLHttpRequest 값이 있는지 검사(null-Safety 를 위해 String 문자열을 앞으로 배치)
  • jackson 의 ObjectMapper 로 Json 데이터를 DTO 객체로 객체화
  • SpringFramework 의 StringUtil 로 검사
  • CustomAjaxAuthenticationToken 생성 후 AuthenticationManager 에게 넘겨줌 (getAuthenticationManager().authenticate())
import com.example.securityapp.domain.AccountDTO;
import com.example.securityapp.security.token.AjaxAuthenticationToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;

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

public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private ObjectMapper objectMapper = new ObjectMapper();

    public AjaxLoginProcessingFilter() {//생성자에 AntPathRequestMatcher 를 넘겨 경로검사
        super(new AntPathRequestMatcher("/api/login"));
    }

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

        if (!isAjax(request)) {
            throw new IllegalStateException("Authentication is not supported");
        }

        //jackson 의 ObjectMapper 로 Json 데이터를 객체화
        AccountDTO accountDTO = objectMapper.readValue(request.getReader(), AccountDTO.class);

        if(StringUtils.isEmpty(accountDTO.getUsername()) || StringUtils.isEmpty(accountDTO.getPassword())){
            throw new IllegalArgumentException("Username or Password is empty");
        }

        AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(accountDTO.getUsername(), accountDTO.getPassword());


        return getAuthenticationManager().authenticate(ajaxAuthenticationToken);//AuthenticationManager 에게 넘겨줌
    }

    private boolean isAjax(HttpServletRequest request) {

        if ("XMLHttpRequest".equals(request.getHeader("X-Request-With"))) {
            return true;
        }
        return false;
    }
}

CustomAjaxAuthentiationToken 구현

  • 기본 골격은 UsernameAuthenticationFilter 를 사용
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

public class AjaxAuthenticationToken extends AbstractAuthenticationToken { // 정보 값은 UsernamePasswordAuthenticationToken 을 참조한다.

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;
    private Object credentials;


    public AjaxAuthenticationToken(Object principal, Object credentials) { // 인증전 생성
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }


    public AjaxAuthenticationToken(Object principal, Object credentials,
                                   Collection<? extends GrantedAuthority> authorities) { // 인증 성공시 생성
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }


    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        credentials = null;
    }

}

등록

  1. http.addFilterBefore(AjaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
  2. AjaxAuthenticationFilter 를 Bean 으로 등록
  3. 등록과정에서 AuthenticationManager 가 필요하므로 authenticationManagerBean 메서드를 재정의하여 super 클래스로 부터 Bean 등록
@Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
        return ajaxLoginProcessingFilter;
    }
    
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {//
        return super.authenticationManagerBean();
    }
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/","/users","user/login/**","/login*").permitAll()
                .antMatchers("/mypage").hasRole("USER")
                .antMatchers("/messages").hasRole("MANAGER")
                .antMatchers("/config").hasRole("ADMIN")
                .anyRequest().authenticated()

            .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .successHandler(customAuthenticationHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .loginProcessingUrl("/login_proc")
                .permitAll()
        .and()
                .exceptionHandling()
                .accessDeniedHandler(customAccessDeniedHandler())

        .and()
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)// 해당필터 앞에 위치시킴
                .csrf().disable();
    }

설정클래스의 필터 추가 API들

  1. addFilterBefore(filter,ClassType) : 추가하고자하는 필터가 기존 필터 앞에 위치할 떄
  2.  addFilter(filter,ClassType) : 추가하려고 하는 필터가 가장 마지막에 위치
  3. addFilterAfter(filter,ClassType) : 추가하고자 한 필터가 기존필터 뒤에 위치할때
  4. addFilterAt(filter,ClassType) : 기존 필터위치를 대체하고자 할때


추가사항

  • 인텔리제이 에서는 HttpRequest 파일api 요청을 할 수 있게 지원한다.
  • HttpRequest 파일 생성 후 원하는 요청방식과 URL, 데이터 정보를 설정
  • 보낼 데이터를 정의하고 보내면 된다.
# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or
# paste cURL into the file and request will be converted to HTTP Request format.
#
# Following HTTP Request Live Templates are available:
# * 'gtrp' and 'gtr' create a GET request with or without query parameters;
# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body;
# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data);
POST http://localhost:8080/api/login
Content-Type: application/json
X-Requested-With: XMLHtttpRequest

{
  "username":"user",
  "password":"1111"
  }

###

이후 작동과정에서 AuthenticationProvider가 AjaxAuthenticationToken을 인식하지 못하므로 AjaxAuthenticationProvider를 구현해서 AuthenticationManager 에 등록해야한다.