본문 바로가기
관리자

Programming-[Backend]/Spring Security

[스프링 시큐리티] 15. Ajax 인증 구현 - AbstractAuthenticationProcessingFilter, Token, AuthenticationProvider

728x90
반응형

1.Ajax 인증 개요

 

Ajax 인증은 Form 인증과 유사하다. 다만, Form 인증은 동기적인 방식으로 인증처리를 했던 것에 비해서 Ajax 인증은 비동기적인 방식으로 인증을 처리한다.

 

AjaxAuthenticationFilter  ->  AjaxAuthenticationToken  ->  AuthenticationManager  -> AjaxAuthenticationProvider

 

Ajax 인증의 가장 큰 기본적인 흐름은 위와 같다. Form 인증과 별반 다를게 없다.

 

 

※Ajax

Asynchronous Javascript and XML의 약자이다. 비동기적으로 클라이언트에서 서버로 요청을 하여, 웹페이지의 일부만을 비동기적으로 갱신할 수 있게 해서 웹페이지의 효용성을 높인다.

 

2. Ajax 인증 필터 만들기

 

Ajax를 통해 인증을 처리하는 필터를 만든다. security 패키지 안에 filter 패키지를 만들고, AjaxLoginProcessingFilter를 추가한다.

 

AjaxLoginProcessingFilter

 

AbstractAuthenticationProcessingFilter를 상속받는다. 이는 Form 로그인에서 UsernamePassowordAuthenticationFilter가 상속받던 부모 객체로, 맨 처음 인증 처리를 해주는 Filter 추상 클래스이다.

public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private ObjectMapper objectMapper = new ObjectMapper();

    //필터 작동 조건1 : /api/login으로 요청이 왔을 때
    public AjaxLoginProcessingFilter() {
        super(new AntPathRequestMatcher("/api/login"));
    }

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

        //필터 작동 조건2 : Ajax 요청일때
        if (!isAjax(request)) {
            throw new IllegalStateException("Authentication is not supported");
        }

        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());

        //authenticationManager에 직접만든 ajaxAuthenticationToken을 전달하여 인증 처리가 되도록 한다.
        return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
    }

    private boolean isAjax(HttpServletRequest request) {

        //클라이언트에서 특정 요청이 왔을때를 Ajax 요청으로 정의한다.(클라이언트와 약속)
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
            return true;
        }
        return false;
    }
}

 

attemptAuthentication

메서드를 Override한다. Ajax 요청인지 검사를 위해서 isAjax라는 메서드를 만들었는데, 메서드 내용에서 알 수 있듯이 Ajax 요청은 클라이언트와 약속을 통해 요청에 특정 Header 값을 넣어주기로 하면 된다. 여기서는 X-Requested-With 라는 key에 XMLHttpRequest value값을 넣어주었다. 

 

그리고 ObjectMapper를 이용해서 request.getReader()로 요청 정보를 읽어서 AccountDto로 매핑해준다. 이렇게 매핑된 정보를 뒤에서 구현할 AjaxAuthenticationToken에 담아주고, AuthenticationManager의 authenticate 인증 메서드로 전달하여 인증 처리가 되도록 한다.

 

 

 

 

AjaxAuthenticationToken

인증 정보를 담는 Token 객체를 생성한다.

 

 

이 부분도 Token 객체를 공통으로 구현하는 AbstractAuthenticationToken 추상 클래스를 상속받는다. Token의 구성정보를 직접 손댈 필요는 없기 때문에, UsernamePasswordAuthenticationToken.class의 내용을 그대로 복사해넣는다. 다만 생성자의 이름은 바꿔주어야한다.

 

public class AjaxAuthenticationToken extends AbstractAuthenticationToken {

    //UsernamePasswordAuthenticationToken의 코드를 복사해와서 생성자의 이름만 바꿔준다.

    private static final long serialVersionUID = 520L;
    private final Object principal;
    private Object credentials;

    public AjaxAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public AjaxAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    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");
        } else {
            super.setAuthenticated(false);
        }
    }

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

}

 

 

 

SecurityConfig 설정

 

이제 이전 시간에 했던 accessDeniedHandler 뒤쪽에 addFilterBefore 메서드를 이용해서 AjaxLoginProcessingFilter를 추가하면 된다. 

.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) //기존 필터 앞에 위치할때 사용하는 method
.csrf().disable() //기본적으로 csrf 토큰값을 들고 요청을 해야되는데, 임시적으로 off 한다.

 

csrf를 disable 처리하는 것을 볼 수 있다. 앞서 배운 바와 같이 기본적으로 요청이 왔을 때 csrf 필터를 통해 검사를 하는데 직접 만든 filter에서는 이 처리를 하지 않으므로 disable 처리를 하지 않는다면 아래와 같이 ClientProtocolException이 발생한다.

 

 

AuthenticationManagerBean 설정

 

그리고 ajaxLoginProcessingFilter 를 Bean으로 불러온다.

 

@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
    AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
    ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
    return ajaxLoginProcessingFilter;
}

 

이 때 주의해야할 점은 반드시 AuthenticationManager를 set 해주어야 한다는 것이다. 이를 위해서 AuthenticationManagerBean 메서드를 Override 해주어야 한다.

 

@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

 

 

http 요청 만들고 보내기

 

postman으로 Header 값 등을 설정하고 보내도 되지만, 강의에서는 intelliJ에서 ajax 객체를 만들어서 바로 요청을 보내는 법을 소개한다. java 디렉터리 아래에 ajax.http 파일을 만들고 아래와 같이 작성한다. method, url을 작성하고 header 값 및 body값을 http 스펙대로 전달한다.

 


POST http://localhost:8080/api/login
Content-Type: application/json
X-Requested-With: XMLHttpRequest

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

 

우상단에서 Examples 버튼을 통해 어떻게 요청을 보낼 수 있는지 예제 코드를 얻을 수도 있다.

그리고 좌측 실행버튼을 통해서 요청을 보낼 수 있다.

 

그럼 Services 탭에서 아래와 같이 401 응답값이 나온다. 이것은 AuthenticationManager로 Token 값은 전달했는데, AuthenticationManager가 인증 처리를 위임할 Provider가 없기 때문이다. 다시 말해 AjaxAuthenticationToken을 처리할 수 있는 Provider가 없기 때문에 인증 에러가 발생하는 것이다.

 

ProviderManager.class에서 authenticate 메서드에 break point를 걸고 확인해보자. AnonymousAuthenticationProvider와 이전 시간에 작성한 CustomAuthenticationProvider(FormAuthenticationProvider로 이름 변경 예정)만 있다.

 

 


 

3. AjaxAuthenticationProvider

 

 

AuthenticationProvider

 

Ajax용 AuthenticationProvider는 이전에 작성했던 FormAuthenticationProvider와 같게 처리해도 된다. 코드를 복사하되, secret key를 이용하던 부가 기능 부분은 삭제한다.

 

public class AjaxAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    //기본 생성자를 넣어주기 위해 @RequiredArgsConstructor 제외, @Autowired 적용

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //설계나 정책에 따라서 인증 메서드를 다양하게 구현할 수 있다. 아래는 가장 기본적인 예시이다.

        String username = authentication.getName();
        String password = (String) authentication.getCredentials(); //Object 타입으로 반환되어 캐스팅

        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username); //직접 만든 클래스로 캐스팅

        if(!passwordEncoder.matches(password, accountContext.getAccount().getPassword())) {
            throw new BadCredentialsException("invalid password");
        }

        //인증 성공 시 성공한 인증 객체를 생성 후 반환
        return new AjaxAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //전달된 파라미터의 타입이 AjaxAuthenticationToken 타입과 일치하는지 검사한다.
        return authentication.isAssignableFrom(AjaxAuthenticationToken.class);
    }
}

 

 

 

AjaxSecurityConfig

 

그리고 기존 SecurityConfig 파일에서 Form 인증 방식을 처리하던 것과 구분하여, Ajax 인증 방식의 설정 파일을 따로 만든다. 이렇게 설정을 분리하여 필터 및 Provider를 따로 적용할 수 있다는 것을 기억하자

 

@Configuration
@Order(0)
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(ajaxAuthenticationProvider());
    }

    @Bean
    public AuthenticationProvider ajaxAuthenticationProvider() {
        return new AjaxAuthenticationProvider();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) //기존 필터 앞에 위치할때 사용하는 method
                .csrf().disable() //기본적으로 csrf 토큰값을 들고 요청을 해야되는데, 임시적으로 off 한다.
        ;
    }

    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
        return ajaxLoginProcessingFilter;
    }
}

 

@Order

기존 SecurityConfig와 똑같이 WebSecurityConfigurerAdapter를 상속받는다. Config 파일이 2개이기 때문에, @Order 어노테이션을 통해서 우선순위를 지정해주어야 한다. Ajax Config 부분을 0번째 우선순위로 설정하여 먼저 적용되도록 한다.

 

AuthenticationProvider

그리고 AuthenticationProvider 타입의 ajaxAuthenticationProvider를 @Bean으로 불러와서 AuthenticationManager에 추가, Manager를 다시 filter에 추가하여 동작이 가능하도록 해주면 된다.

 

ProviderManager.class의 첫 줄에서 debug를 해보면 설정해준 AjaxAuthenticationProvider가 providres에서 호출되는 것을 확인할 수 있다.

 

 

 

 

 

 ※ @RequiredArgsConstructor - 기본(default) 생성자가 필요하다면 @Autowired로 변경 적용하기

 

 

AjaxAuthenticationProvider에서 의존성 주입을 할 때, 편의를 위해 @RequiredArgsConstructor를 적용했었다. 그런데 해당 Provider가 호출될 때 필드값이 필요하지 않은 기본 생성자 new AjaxAuthenticationProvider(); 를 사용해야되는 필요가 있었다. 그래서 각 필드값들은 final을 제외한 채 @Autowired로 변경해주고, 기본 생성자가 자동으로 생성되도록 만들어주었다. 향후 Bean을 주입할 때 기본 생성자가 필요한 경우에 참고하자.

 

public class AjaxAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    //기본 생성자를 넣어주기 위해 @RequiredArgsConstructor 제외, @Autowired 적용

 

참조

 

1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

728x90
반응형