이런 어처구니없는;; feat. Spring Security

Daeyoung Nam·2021년 12월 20일
1
post-thumbnail

문제의 코드

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val token = request.getHeader(jwtProvider.headerTokenKey)

        if(token != null && token.isNotEmpty() && jwtProvider.validation(token)){
            val authentication = jwtProvider.getAuthentication(token)
            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }

아니 리액트에서 인증된 사용자가 요청을 한번 보내고 token값을 null로 바꿔서 보내보니까 계속 인증이 된 상태로 무언가 저장이 되는 이슈가 있었는데... 하...
처음엔 캐시 문제인가 싶어서 들어오는 토큰값 찍어봤는데도 token이 안들어오는것이었다.

그러면 백단 문제인건 확실해서 SecurityContext 코드를 타고 들어가봤다.

SpringSecurity의 SecurityContext는 기본적으로 ThreadLocal로 관리된다.
ThreadLocal은 쓰레드마다 독립적인 변수를 가지고 관리할 수 있게 해주는데. 그림으로 표현하자면 다음과 같다.

실제 Context 구현체에 쓰레드 로컬이 다음과 같이 정의되어있었다.

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	//이놈 이놈 이놈!!
	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

	@Override
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder.set(context);
	}

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

우리가 얻어온 getContext() 함수를 잘 봐보자..

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

쓰레드 로컬에서 SecurityContext를 가져오고 null이면 하나 만들어 준다음 쓰레드 로컬에 다시 등록 후 반환하는 걸 볼수 있다.
그럼 이쯤에서 대체 SecurityContext는 무엇이고 인증 절차는 어떻게 이루어질까?

사진에 나와있듯이 SecurityContext는 Authentication(인증정보)를 가지고 있다.

SecurityContext의 코드를 한번 봐보자.

public interface SecurityContext extends Serializable {

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);

}

저기 setAuthentication의 주석을 해석해보면 현재 인증 principal을 변경하거나 인증정보를 삭제합니다 라고 나와있다.

parameter에도 새로운 토큰이나 인증정보를 저장하지 않아야 하는 경우는 null을 넣어주라고 되어있다.
이런 씹...

내가 겪었던 이슈는 인증이 한번 진행된 이후에 헤더에 token을 이상한 값으로 바꿔치기 해도 계속 인증이 통과하는 경우였는데 null을 넣어주니 403 에러를 제대로 뱉어냈다.

근데.. SecurityContext에 인증정보가 없는 경우 403은 어디서 날려주는걸까?

ExceptionTranslationFilter

	private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
		if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
						authentication), exception);
			}
			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(
							this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
									"Full authentication is required to access this resource")));
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace(
						LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
						exception);
			}
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}

이거 알아내려고 filter 다까봄.

간략하게 설명하자면 이 필터의 문서를 보면 다음과 같이 나와있다

필터 체인 내에서 발생한 모든 AccessDeniedException 및 AuthenticationException을 처리합니다.

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			if (securityException == null) {
				rethrow(ex);
			}
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

얘의 doFilter 메소드에는 이렇게 정의가 되어있는데 나의 경우 ExceptionTranslationFilter의 다음 filter는 FilterSecurityInterceptor 인데 FilterSecurityInterceptor의 경우
어떤 리소스에 접근하기 전 마지막에 AccessDecisionManager를 사용하여 인가처리를 하는 필터이다.

즉 저놈이 AccessDeniedException을 뱉어주는것이다. 그리고 저놈이 최종 인가 처리를 담당하는 놈이다!

후... 오늘도 보람찬 하루~!

profile
내가 짠 코드가 제일 깔끔해야하고 내가 만든 서버는 제일 탄탄해야한다 .. 😎

0개의 댓글