개발자의 오르막

[SpringSecurity #10] HeaderWriterFilter, CsrfFilter 본문

SpringFrameWork/SpringSecurity

[SpringSecurity #10] HeaderWriterFilter, CsrfFilter

계단 2020. 10. 3. 00:54

# 응답 헤더에 시큐리티 관련 헤더를 추가해주는 필터

- XContentTypeOptionsHeaderWriter : 마임 타입 스니핑 방어

  ( 실행할 수 없는 마임 타입을 실행하려는 요청을 방어해줌 )

- XXssProtectionHeaderWriter : 브라우저에 내장된 XSS필터 적용

- CacheControlHeaderWriter : 캐시 히스토리 취약점 방어

- HstsHeaderWriter : HTTPS로만 소통하도록 강제

- XFrameOptionsHeaderWriter : clickjacking 방어

 

- Cache-Control : no-cache, no-store, max-age=0, must-revalidate

  ( 캐쉬를 쓰지 않도록 설정 )

- Content-Language : en-US

- Content-Type : text/html;charset=UTF-8

  ( 컨텐츠 타입에 명시된 마임 타입으로만 랜더링 하게 되어 있음 )

- Date : Sun, 04 Aug 2019 16:25:10 GMT

- Expires : 0

- Pragma : no-cache

  ( 캐쉬를 쓰지 않도록 설정해주는 것 )

- Transfer-Encoding : chunked

- X-Content-Type-Options : nosniff

  ( 실행이 되지 않음 )

- X-Frame-Options : DENY

  ( 보이지 않는 영역을 클릭 할 때 정보를 전송하는 것을 방지하는 것 )

- X-XSS-Protection : 1; mode=block

  ( 1이 기능을 활성화 하는 것을 의미 )

 

 

# CsrfFilter

- CSRF 어택 방지 필터

  인증된 유저의 계정을 사용해 악의적인 변경 요청을 만들어 보내는 기법

  CORS를 사용할 때 특히 주의해야 함

  ( 타 도메인에서 보내오는 요청을 허용하기 때문 )

 

- 의도한 사용자만 리소스를 변경할 수 있도록 허용하는 필터

  ( CSRF 토큰을 사용하여 다른 사용자 접근 방지 )

  위의 계좌 이체 부분에서 해당 CSRF 가 없기 때문에 요청을 막아준다.

 

 

- CsrfFilter

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		final boolean missingToken = csrfToken == null;
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);

		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}

		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!csrfToken.getToken().equals(actualToken)) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}

		filterChain.doFilter(request, response);
	}

  위의 필터는 요청이 들어올 때마다 CSRF 토큰을 검사해주는 필터이다.

  위의 requset.setAttribute(CsrfTokec.class.getName(), csrfToken); 에서 프로젝트가 생성한 csrf 토큰을

  set 해준다.

 

- 로그인 Form

  그리고 로그인 폼에서 보낸 csrf 토큰 값을 위의 소스에서 actualToken 값으로 받아온다.

  그러면 우리의 csrfToken 값과 actualToken 값이 일치하는지 여부를 확인할 수 있다.

 

  즉, 내가 작성한 Form 에서 요청한 동작이 맞구나 란 것을 알 수 있다.

  ?? ( input hidden 에 있는 csrf 밸류를 다른 웹에서 보내면 어떻게 되는거지? )

 

 

- 위의 내용을 securityConfig 메소드에 넣으면 csrf 를 사용하지 않을 수 있다.

http.csrf().disable();

 

 

# LogoutFilter

- 여러 LogoutHandler 를 사용하여 로그아웃 시 필요한 처리를 하며, 이후에는 LogoutSuccessHandler 를 사용하여

  로그아웃 처리를 한다.

  - CsrfLogoutHandler

  - SecurityContextLogoutHandler

  - SimplUrlLogoutSuccessHandler

- LogoutFilter 는 크게 LogoutHandler 와 LogoutSuccessHandler 로 나뉘어진다.

  LogoutHandler 는 Composit Handler 로 여러 Handler 가 결합된 것이고,

  LogoutSuccessHandler 는 로그아웃이 성공했을 때의 처리를 컨트롤 한다.

 

  /logout 으로 요청이 들어오면 DefaultLogoutPageGeneratingFilter 를 통해 기본 시큐리티에

  내장되어 있는 로그아웃 페이지로 이동하게 되고,

  로그아웃 Request 가 들어오면 밑의 로직의 requiresLogout 안으로 로직이 실행된다.

 

- LogoutFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (requiresLogout(request, response)) {
			Authentication auth = SecurityContextHolder.getContext().getAuthentication();

			if (logger.isDebugEnabled()) {
				logger.debug("Logging out user '" + auth
						+ "' and transferring to logout destination");
			}

			this.handler.logout(request, response, auth);

			logoutSuccessHandler.onLogoutSuccess(request, response, auth);

			return;
		}

		chain.doFilter(request, response);
	}

  즉, 이 필터는 모든 Request 마다 실행되지만, requiresLogout 조건을 충족하지 않으면 chain.doFilter 로 

  넘어가게 된다.

  이 때 인증 받은 auth 를 securityContextHolder 를 통해 불러오고, 그 객체를 handler.logout 메소드를 실행한다.

 

- CompositeLogoutHandler

public final class CompositeLogoutHandler implements LogoutHandler {

	private final List<LogoutHandler> logoutHandlers;

	public CompositeLogoutHandler(LogoutHandler... logoutHandlers) {
		Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
		this.logoutHandlers = Arrays.asList(logoutHandlers);
	}

	public CompositeLogoutHandler(List<LogoutHandler> logoutHandlers) {
		Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
		this.logoutHandlers = logoutHandlers;
	}

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		for (LogoutHandler handler : this.logoutHandlers) {
			handler.logout(request, response, authentication);
		}
	}
}

 

- SecurityConfig 

http.logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .logoutRequestMatcher()
                .invalidateHttpSession(true)
                .deleteCookies()
                .addLogoutHandler()
                .logoutSuccessHandler();

  시큐리티 설정 메소드에 로그아웃과 관련된 설정을 할 수 있다.

  http.logout().invalidateHttpSession 은 로그아웃 시 세션을 없애는 여부에 대한 설정

  http.logout().deleteCookies() 는 쿠키를 사용한 로그인을 했을 시 그에 대한 로그아웃 설정

  .addLogoutHandler(), logoutSuccessHandler 는 로그아웃 핸들러를 커스텀해서 주입함으로 써 사용하는 설정이다.

 

Comments