(spring security) 인증 구현

jint·2024년 10월 21일

보안

목록 보기
5/15

Authentication Manager는 필터 계층에서 요청을 수신해 요청을 허용할지 거부할지를 Authentication Provider에 위임 하는 구성 요소이다.

AuthenticationProvider에 대해 이해하기 위해서는 Authentication 인터페이스를 이해해야한다.

Authentication

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

상속 받는 Principal인터페이스는 접근을 요청하는 사용자를 나타내는 인터페이스다.

getName()추상 메서드를 가지고 있다.

Authentication 계약은 Principal을 확장하여 인증이 되었는지, 인증에 사용된 암호가 뭔지, 허가된 권한 컬랙션을 반환하는 추상메서드를 정의한다.

AuthenticationProvider의 책임

인증이 실패하면 메서드는 AuthenticationException을 투척해야한다.

지원되지 않는 객체를 받으면 null을 반환해야한다.

인증에 성공하면 Authentication인스턴스를 반환해야한다. 이때 isAuthenticated()메서드는 true를 반환해야한다.

이떄의 Authentication 인스턴스는 더이상 credentials정보가 필요없기 때문에 원치 않는 유출을 방지하기 위해 제거한다.

AuthenticationProvider인터페이스

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

supports메서드는 AuthenticationProvider가 Authentication의 구현체를 지원하면 true를 반환한다.

주의할 점은 supports메서드가 true를 반환해도 실제로 authenticate메서드에서 인증의 세부 정보가 다르다면

null을 반환할 수 있다는 점이다.

Authentication Manager의 역활

Authentication Manager는 자신이 가지고있는 Authentication구현체를 처리할 수 있는 등록된 AuthenticationProvider을 찾는다.

직접 AuthenticationProvider를 구현해 등록하기

@Component
public class CustonAuthenticationProvider implements AuthenticationProvider {
	@Autowired
	private UserDetailsService userDetailsService;
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Override
	public Authentication authenticate(Authentication authentication) {
		//Principal의 메서드
		String username = authentication.getName();
		String password = authentication.getCredentials().toString();
		//userDetailsService를 통해 알맞은 유저 가져오기
		UserDetails u = userDetailsService.loadUserByUsername(username);
		
		if(passwordEncoder.matches(password, u.getPassword()) {
			return new UsernamePasswordAuthenticationToken(
				username,
				password,
				u.getAuthorities());
		} else {
			//알맞은 유저는 찾았지만 credential이 일치하지 않을떄
			throw new BadCredentialsException("Something went Wrong!");
		}
	}
	
	@Override
	public boolean supports(Class<?> authenticationType) {
		return authenticationType.equals(UsernamePasswordAuthenticationToken.class)
	}
}

첫 예외 발생지점인 loadUserByUsername은 알맞은 이름의 유저네임이 없으면 AuthenticationException을 투척한다.

credential이 일치하지 않아도 AuthenticationException을 상속 받은 BadCredentialsException을 투척한다.

필터는 이를 받아 401 응답을 보낸다.

UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken은 사용자이름과 암호를 사용하는 Authentication 계약의 구현체이다.

UsernamePasswordAuthenticationToken의 authorities까지 포함된 생성자는 setAuthenticated를 true로 설정해준다.

AuthenticationProvider등록하기

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	private AuthenticationProvider authenticationProvider;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) {
		auth.authenticationProvider(authenticationProvider);
	}
}
		

@Autowired를 통해 현제 component로 등록됨 authenticationProvider가 CustomAuthenticationProvider밖에 없기 때문에 CustomAuthenticationProvider를 주입받는다.

AuthenticationManagerBuilder의 등록메서드를 통해 CustomAuthenticationProvider를 등록한다.


securityContext

인증이 되었다면 요청을 처리할때 이름이나 권한을 참조할 가능성이 높기 때문에 AuthenticationManager는

인증에 대한 정보를 securityContect에 넣어둔다.

SecurityContext 인터페이스

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

SecurityContext의 주책임은 Authentication을 저장하고 필요할때 반환하는 것이기 때문에 2개의 추상 메서드가 필요하다.

서블릿 어플리케이션에서의 SecurityContext의 3가지 전략

아래 전략은 webflux등 리엑티브 앱에서는 적용되지 않는다

MODE_THREADLOCAL

Spring MVC에서는 각각의 요청이 고유의 스레드를 가지기 때문에 가장 일반적인 접근법이다.

JDK에 있는 구현인 ThreadLocal을 이용하여 Context를 스레드안에서만 접근가능하도록 만든 전략이다.

다른 스레드(요청)에서 접근이 불가능하므로 경합같은 문제가 발생하지 않는다.

securityContext는 SecurityContextHolder가 관리하기 떄문에 spring mvc controller에서

다음과 같이 접근가능하다.

@Getmapping("/hello")
public String hello() {
	SecurityContext context = SecurityContextHolder.getContext();
	Authentication a = context.getAuthentication();
	
	return a.getName();
}

스프링 부트를 이용하면 더 간단하게 접근가능하다.

@GetMapping("/hello")
public String hello(Authentication a) {
	return a.getName();
}

스프링 부트가 Authentication 인스턴스를 주입해준다.

MODE_INHERITABLETHREADLOCAL

이름 그대로 ThreadLocal를 이용하되 새 스레드가 기존 스레드의 SecurityContext를 상속받게 해 새로운 스레드에서도 기존의 SecurityContext에 접근가능하게 하는 전략이다.

@Configuration
@EnableAsync
public class ProjectConfig {
	@Bean
	public InitializingBean initializingBean() {
		return () -> SecurityContextHolder.setStrategyName(
			SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
	}
}

다음과 같이 setStrategyName을 설정하면 @Async를 통해 새로운 스레드로 요청을 처리할때도 SecurityContext에 접근가능하다.

MODE_GLOBAL

모든 스레드가 SecurityContext객체에 접근할 수 있게 하는 전략

다른 스레드(요청)에서 접근할 수 있기 때문에 보안, 경합등 다양한 문제가 발생할 수 있다.

0개의 댓글