나만의 custom filter 만들어 spring security filter chain에 연결하기

김가빈·2023년 8월 29일
0

springsecurity

목록 보기
12/23
post-thumbnail
  • filter chain은 연쇄적으로 일어나며, 앞선 filter의 결과물이 뒤의 filter에 injection되어 적용되는 방식이다.
  • 다음과 같이 filterchainproxy에서 가상의 filterchain을 만들어서 차례로 실행한다.

실습

  • 실습을 위해 다음과 같이 debug모드로설정한다.
  • 참고로 debug모드에서는 유저의 session id 등 중요 정보가 콘솔에 노출될 수 있으니, 실제로는 절대로 사용하면 안된다.

  • application.properties

custom filter만들기

  • 다음과 같이 filter 인터페이스를 구현해 filter class를 생성한다.
package com.eazybytes.filter;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.util.StringUtils;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class RequestValidationBeforeFilter implements Filter {

	public static final String AUTHENTICATION_SCHEME_BASIC= "Basic";
	private Charset credentialsCharset = StandardCharsets.UTF_8;
	
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse res = (HttpServletResponse) response;
		String header = req.getHeader(AUTHORIZATION);
		
		if(header != null) {
			header = header.trim();
			if(StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
				byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
				byte[] decoded;
				// basic 무시하기 위해서 6자리부터 시작				
				try {
					decoded = Base64.getDecoder().decode(base64Token);
					String token = new String(decoded, credentialsCharset);
					int delim = token.indexOf(":");
					if(delim == -1) {
						throw new BadCredentialsException("Invalid basic authentication token");
					}
					String email = token.substring(0, delim);
					if(email.toLowerCase().contains("test")) {
						res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
						return;
					} 
				} catch(IllegalArgumentException e) {
					throw new BadCredentialsException("Failed to decode basic authentication token");
				}
			}
		}
        chain.doFilter(request, response);
	}
}
  • 보안에 활용되는 AUTHORIZATION 헤더를 가져온 뒤, basic으로 시작하는지 즉, basic authentication token인지를 검사한다.
  • 맞을 경우 해당 token을 추출해서 decode하는데, base authentication은 기본적으로 user:password 형태로 저장되므로 해당 형식에 맞게 token을 잘라서 해당 값을 확인한다.
  • 이 형식이 적용되기 위해서는 유저가 header에 다음과 같은 형태로 전달할 필요가 있다.
httpHeaders.append('Authorization', 'Basic' + window.btoa(this.user.email + ':' + this.user.password));
  • chain.doFilter(request, response)를 통해서 다음 filterchain과 연결한다.

  • 다음과 같이 filter들을 추가로 생성한다.
package com.eazybytes.filter;

import java.io.IOException;
import java.util.logging.Logger;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

public class AuthoritiesLoggingAfterFilter implements Filter {

	private final Logger LOG = Logger.getLogger(AuthoritiesLoggingAfterFilter.class.getName());
	
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
	
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if(null != authentication) {
			LOG.info("User " + authentication.getName() + " is successfully authenticated and "
					+ "has the authorities " + authentication.getAuthorities().toString());
		}
		
		chain.doFilter(request, response);
	}

}
package com.eazybytes.filter;

import java.io.IOException;
import java.util.logging.Logger;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

public class AuthoritiesLoggingAtFilter implements Filter {

	private final Logger LOG = Logger.getLogger(AuthoritiesLoggingAtFilter.class.getName());
	
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		LOG.info("Authentication Validation is in progress");
		chain.doFilter(request, response);
	}
	

}

custom filter 주입하기

  • security config class에 다음과 같이 설정한다.
  • requestValidationBeforeFilter -> basicAuthenticationFilter -> AuthroitiesLoggingAfterFilter순으로 적용된다.
  • basicAuthenticationFilter 앞이나 뒤에 AuthroitiesLoggingAtFilter가 적용되는데 앞에 적용될지 뒤에 적용될지 모른다.(그래서 이 메소드는 잘 사용하지 않는다)


(참고) spring security의 filter chain

  • spring security의 filter chain은 기본적으로 다른 포트에서 접근했을 때 적용된다.
  • 따라서 localhost8080등으로 접근했을 때는 spring security filter가 안 돌 수 있다.
  • spring security filter chain은 security에서 만들어오는 default filter가 모두 돌고 난 후에 유저가 만든 custom filter가 도는 구조이다.
  • 필터들 간 적용 순서는 다음과 같다.
    • SecurityContextPersistenceFilter : 보안컨텍스트 설정, 검색
    • UsernamePasswordAuthenticationFilter : 사용자 자격증명 기반으로 인증 시도
    • AuthenticationProvider : 실제 인증 수행 후 Authentication 객체 생성
    • SecurityContextHolderAwareRequestFilter : Spring Security에 의해 보호되는 리소스에 대한 요청을 판별하고, 요청을 알맞은 보안 컨텍스트와 연결
    • FilterSecurityInterceptor : 권한 검사 및 권한 부여 수행(Authentication객체 사용)
    • 그 외 유저의 custom filter

  • 따라서 다음과 같은 사진에서 UsernamePasswordAuthenticationToken을 사용할 수 있다.(UsernamePasswordAuthenticationFilter에 의해 이미 생성되었기 때문)
  • 그리고 default filter를 사용할 수도 있지만 AuthenticationProvider를 구현한 것 처럼 위의 ault필터라도 유저가 custom할 수 있다.

  • 내가 생성한 custom filter가 default filter다음에 돌기 때문에 다음과 같이 SecurityContextHolder에 있는 Authentication객체를 가져와서 사용할 수 있다.

profile
신입 웹개발자입니다.

0개의 댓글