Spring

스프링 시큐리티 (Spring Security) 정리

개발중인 감자 2023. 11. 4. 16:29

🌟 1. Spring Security?

인증, 권한 부여 및 보호 기능을 제공하는 스프링 기능. 

 

✔️ 보안 3요소 

접근 주체 (Principal) : 보호된 대상에 접근하는 사용자

인증(Authentication) : 해당 사용자가 본인이 맞는지 확인하는 과정. 일반적으로 아이디/암호로 인증.

인가 (Authorization) : 인증된 사용자가 자원(url, 기능 등)에 접근이 가능한지 결정하는 절차

 

보통 인증 -> 인가 로 절차를 진행한다. 

 

 

🌟 2. Spring Security Architecture (구조)

 

 

✔️ Authentication(인증) 의 용도

- 현재 접근 주체 정보(principal), 자격 (Credential) 을 담는 목적

- 인증 요청할 때, 요청 정보를 담는 목적

public interface Authentication extends Principal, Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();
    
	Object getCredentials();
    
	Object getDetails();
 
	Object getPrincipal();
 
	boolean isAuthenticated();
    
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
 
}

 

✔️ SecurityContext

- Authentication(인증)을 보관

- 현재 사용자에 대한 Authentication 객체를 구할 때 SpringContext로부터 구함.

public interface SecurityContext extends Serializable {
	Authentication getAuthentication();
	void setAuthentication(Authentication authentication);
}

 

✔️ SecurityContextHolder

- SpringContext 를 보관 

- 기본적으로 쓰레드 로컬에 SpringContext를 보관한다. 

- 스프링 시큐리티의 기본적인 시큐리티 콘텍스트의 전략은 ThreadLocal 사용하는 ThreadLocalSecurityContextHolderStrategy 이다. 

 

public class SecurityContextHolder {
	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
		}
		else {
			// Try to load a custom strategy
			try {
				Class<?> clazz = Class.forName(strategyName);
				Constructor<?> customStrategy = clazz.getConstructor();
				strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
			}
			catch (Exception ex) {
				ReflectionUtils.handleReflectionException(ex);
			}
		}
		initializeCount++;
	}
	public static SecurityContext getContext() {
		return strategy.getContext();
	}
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}
}

 

 

✔️ UsernamePasswordAuthenticationToken

-  Authentication(인증) 구현한 AbstractAuthenticationToken의 하위의 하위클래스로, 유저의 ID가 Principal의 역할을 하고 유저의 Password가 Credential의 역할을 한다.

- UserPasswordAuthenticationToken의 첫번째 생성자는 인증 전에 객체를 생성하고, 두번째는 인증이 완료된 객체를 생성한다.

 

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
	/* 1번째 생성자 : 인증 전 객체 생성 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/* 2번째 생성자 : 인증 완료 후 객체 생성 */
	public UsernamePasswordAuthenticationToken(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
	}

 

✔️ AuthenticationManager (interface)

- Authentication(인증)을 처리함. 

- 실질적으로는 AuthenticationProvider에 의해 처리된다. 

- 인증에 성공하면, 두번째 생성자를 이용해 생성한 객체를 SecurityContext에 저장.

 

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

 

 

✔️ ProviderManager

- AuthenticationManager의 일반적인 구현체 

- ProviderManager는 AuthenticationProvider 목록을 위임받음. 

 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		for (AuthenticationProvider provider : getProviders()) {
		}
		// ... 생략
	}
    public List<AuthenticationProvider> getProviders() {
		return this.providers;
	}
}

 

 

✔️ AuthenticationProvider

- 인증 성공, 실패, 결정할 수 없을 나타낼 수 있음. 

- 실제 인증에 대한 부분을 처리한다. 

- 인증 전에 Authentication 객체를 받아, 인증이 완료 된 객체를 반환하는 역할

- 아래와 같은 인터페이스를 구현해 커스텀한 AuthenticationProvider를 작성하고 AuthenticationManager에 등록하면 된다. 

public interface AuthenticationProvider {
 
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	boolean supports(Class<?> authentication);
 
}

 

 

✔️ UserDetailsService

- 이 클래스는 UserDetails 객체를 반환하는 하나의 메서드만을 가지고 있는데, 일반적으로 이를 구현한 클래스에서 UserRepository를 주입받아 DB와 연결하여 처리한다. 

 

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

 

 

✔️ UserDetails (interface)

- 인증에 성공하여 생성된 UserDetails클래스는 Authentication 객체를 구현한 UserPasswordAuthenticationToken을 생성하기 위해 사용된다. 

 

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

 

 

🌟 2. Spring Security의 특징

1) Filter 기반으로 동작한다. 

2) Bean으로 설정할 수 있다. 단, spring security 3.2부터는 xml 설정이 필요 없다. 

 

 

🌟 3. Filter란?

https://docs.spring.io/spring-security/reference/servlet/architecture.html

 

출처 :&nbsp;https://www.slideshare.net/madvirus/ss-36809454

 

▶️ Security Filter

Spring security 동작은 사실상 filter로 동작한다고 해도 무방하다. 

다양한 필터들이 동작하는데, 이 필터들은 각자 다른 기능을 하고 있다.

이런 filter들은 제외할 수 있고, 추가할 수도 있다. 

 

간단히 생각하면 요청 전, 응답 후 어떤 작업을 하도록 하는게 필터이다. 

필터가 여개인 상황이면 마지막 순서 필터가 안쪽부터 첫번째 필터의 가장 밖까지 감싸고 있는 형태이다.

 

 

1) SecurityContextPersistenceFilter 

보통 두번째로 실행되는 필터 (첫번째는 Async 요청에 SecurityContext를 처리할 수 있도록 해주는WebAsyncManagerIntegrationFilter이다)

SpringContext를 찾아와서 SecurityContextHolder에 넣어주는 역할을 하는 Filter이다. 

만약에 SpringContext를 찾았는데 없다면 새로 만들어준다. 

📌 HttpSesstion
SecurityContextPersistenceFilter는 SecurityContext가 있으면 그걸 사용하고 없으면 새로 만들음. 
어디서 가져오는건데? ▶️ 기본적으로 HttpSession에서 가져온다

 

public class SecurityContextPersistenceFilter extends GenericFilterBean {
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
		if (this.forceEagerSessionCreation) {
			HttpSession session = request.getSession();

		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
			SecurityContextHolder.setContext(contextBeforeChainExecution);

			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			SecurityContextHolder.clearContext();
		}
}

 

 

2) BasicAuthenticationFilter

모든 요청에 아이디, 패스워드를 같이 보냄. 

직접 필터를 적용해보면 따로 로그인이라는 과정을 하지 않았는데도 일회성으로 페이지를 불러올 수 있다. 

▶️ 로그인이라는 별도의 과정이 없어도, BasicAuthenticationFilter가 인증해줌. 

▶️ 그렇기 때문에 session이 필요가 없음. 하지만 요청시마다 아이디와 비번이 반복해서 노출되기 때문에 보안에 취약함.

그래서 https를 사용해야함!

 

 

 

3) UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter는 FForm based Authentication 방식으로 인증을 진행할 때 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터

▶️ 로그인하면 실행되는 필터

▶️  쉽게 설명하자면 유저가 로그인 창에서 Login을 시도할 때 보내지는 요청에서 아이디(username)와 패스워드(password) 데이터를 가져온 후 인증을 위한 토큰을 생성 후 인증을 다른 쪽에 위임하는 역할을 하는 필터이다.

 

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	
    // request안에서 username, password 파라미터를 가져와서 
    // UsernamePasswordAuthenticationToken 을 생성 후 
    // AuthenticationManager을 구현한 객체에 인증을 위임한다.
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException
            	("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest 
        	= new UsernamePasswordAuthenticationToken(username, password);
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

 

UsernamePasswordAuthenticationFilter : username, password 인증 필터 

ProviderManager (AuthenticationManager) : 인증 정보 제공 관리자

AbstractUserDetailAuthenticationProvider : 인증 정보 제공 (계정의 상태나 패스워드 일치 여부 파악)

DaoAuthenticationProvider : 유저 정보 제공 

UserDetailsService : 유저 정보 제공하는 Serivce

 

 

4) CsrfFilter

📌 CsrfAttack을 방어하는 필터 

CSRF란 언어 그대로 풀이하면 Cross-Site-Request-Forgery 의 약어이다. 출처 : https://kk-7790.tistory.com/73
▶️ 사이트 사이 요청을 위조한다는 의미를 가지고 있다.
▶️ 사용자와 특정 웹간의 통신을 할때 Request와 Response를 분석해서 Request(요청)을 위조하여 공격자가 원하는 행위를 하게 만드는 공격 

 

Csrf Token을 사용하여 위조된 페이지의 악의적인 공격 방어할 수 있다. 서버는 Csrf 토큰의 여부로 사용자를 인증함. 

정상적인 페이지 : Csrf Token 가짐. 

위조된 페이지 : Csrf Token 없음. 

 

굳이 사용자에게 보여줄 필요없는 값이라서, hidden으로 처리됨. 

▶️ 타임리프 사용하면 자동으로 포함시켜줌.

 

public final class CsrfFilter extends OncePerRequestFilter {
	@Override
	protected void doFilterInternal(HttpServletRequest request, 
    		HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			AccessDeniedException exception = (!missingToken) ? 
            		new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
		filterChain.doFilter(request, response);

 

 

 

5) RememberMeAuthenticationFilter

일반적인 세션보다 훨씬 오랫동안 로그인 사실을 기억할 수 있도록 함. 

session의 기본 만료 시간은 30분이지만, 얘는 기본 설정이 2주다. 

 

 

6) AnonymousAuthenticationFilter

인증이 안된 유저가 요청을 하면, Anonymnous(익명) 유저로 만들어서 Authentication에 넣어주는 필터. 

인증되지 않았다고, null을 넣는게 아니라 기본 authentication을 만들어 주는 개념.

다른 filter에서 익명 유저인지 정상적으로 인증된 유저인지 분기 처리 할 수 있다. 

 

public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
	public AnonymousAuthenticationFilter(String key) {
		this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	}

 

 

7) FilterSecurityInterceptor 

앞서 본 SecurityContextPersistentFilter, UsernamePasswordAuthenticationFilter, 

AnonymousAuthenticationFilter에서 SecurityContext를 찾거나 넘겨줌. 

그러나 얘는 이렇게 넘어온 authentication의 내용을 기반으로 최종 인가 판단을 내림. 

 

대부분의 경우에는 필터 중 뒤쪽에 위치함. 

먼저 인증을 가져오고, 인증에 문제가 있다면, AuthenticaitonException을 발생시킨다. 

인증에 문제가 없다면, 해당 인증으로 인가를 판단.

이때 인가가 거절된다면, AccessDeniedException을 발생시키고, 승인된다면 정상적으로 필터가 종료된다. 

 

 

8) ExceptionTranslationFilter

앞서 본 FilterSecurityInterceptor 에서 발생할 수 있는 두가지 Exception을 처리해주는 필터임. 

  1. AuthenticaitonException : 인증 실패할 때 발생 
  2. AccessDeniedException : 인가에 실패할 때 발생

즉 인증이나 인가에 실패했을 때 어떤 행동을 취해야하는지를 결정해주는 필터이다. 

필터의 handleSpringSecurityException Exception 종류에 따라 로직을 분산시켜준다. 

 

public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
	private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, RuntimeException exception) throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
		}
	}

 

 

출처 

hope0206님 블로그 : 🔒 Spring Security 구조, 흐름 그리고 역할 알아보기 🌱