인증 논리를 담당하는 것은 AuthenticationProvider 계층이며 여기에서 요청을 허용할지 결정하는 조건과 명령을 발견할 수 있다.
AuthenticationProvider의 이해
엔터프라이즈 애플리케이션에서는 사용자 이름과 암호 기반의 기본 인증 구현이 적합하지 않을 수 있다.
일반적으로 프레임워크는 가장 많이 이용되는 구현을 지원하지만 가능한 모든 시나리오를 해결할 수는 없다. 스프링 시큐리티에서는 AuthenticationProvider 계약으로 모든 맞춤형 인증 논리를 정의할 수 있다.
인증 프로세스 중 요청 나타내기
Authentication은 인증이라는 이름이 의미하듯이 인증 프로세스의 필수 인터페이스다.
Authentication 인터페이스는 인증 요청 이벤트를 나타내며 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담는다.
애플리케이션에 접근을 요청하는 사용자를 주체(Principal) 라고 한다.
스프링 시큐리티의 Authentication 계약은 주체만 나타내는 것이 아니라 인증 프로세스 완료 여부, 권한의 컬랙션 같은 정보를 추가로 가진다.
이 계약은 자바 시큐리티 API의 Principal 계약을 확장하여 설계하였기 때문에 호환성 측면에서 이점이 많다. 이 유연성 덕분에 다른 방식으로 인증을 구현한 애플리케이션을 스프링 시큐리티로 더 쉽게 마이그레이션 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 스프링 시큐리티의 Authorization 인터페이스 선언 */
public interface Authentication extends Principal, Serializable {
// 인증된 요청에 허가된 권한의 컬렉션을 반환
Collection<? extends GrantedAuthority> getAuthorities();
// 인증 프로세스에 이용된 암호나 비밀을 반환
Object getCredentials();
Object getDetails();
Object getPrincipal();
// 인증 프로세스가 끝났으면 true를 반환하고 아직 진행 중이면 false
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
맞춤형 인증 논리 구현
스프링 시큐리티의 AuthenticationProvider는 인증 논리를 처리한다. AuthenticationProvider 인터페이스의 기본 구현은 시스템의 사용자를 찾는 책임을 UserDetailsService에 위임하고 PasswordEncdoer로 인증 프로레스에서 암호를 관리한다.
1
2
3
4
5
6
7
8
/* AuthenticationProvider 인터페이스 */
public interface AuthenticationProvider {
// Authentication 계약과 강한 결합 : Authentication 를 매개 변수로 받아 반환
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
인증 논리를 정의하려면 authenticate(Authentication authentication)메서드를 구현한다.
- 인증이 실패하면 메서드는 AutehenticationException 을 던저야 한다.
- 메서드가 현재 AuthenticationProvider 구현에서 지원되지 않는 객체를 받으면 null을 반환
- 메서드는 완전히 인증된 객체를 나타내는 Authentication 인스턴스를 반환
supports(Class<?> authentication) 메서드는 현재 AuthenticationProvider가 Authentication 객체로 제공된 형식을 지원하면 true를 반환하도록 구현한다.
주의할 점은 supports() 메서드가 객체에 대해 true를 반환해도 authenticate() 메서드가 null을 반환해 요청을 거부할 수 있다. 2번이 이에 해당한다.
맞춤형 인증 논리 적용
1
2
3
4
5
6
7
8
9
10
11
/* AuthenticationProvider의 supports() 메서드 재정의 */
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
// 생략된 코드
// UsernamePasswordAuthenticationToken 허용
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* AuthenticationProvider의 인증 논리 구현 */
@Component
public class CustomAuthenticationProvider implements c {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void authenticate(Authentication authentication) {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails u = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, u.getPassword())) {
return new UserPasswordAuthenticationToken(username, password, u.getAuthrities());
} else {
throw new BadCredentialsException("Something went wrong");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* AuthenticationProvider 등록 */
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider);
}
// 생략된 코드
}
SecurityContext 이용
AuthenticationManaget는 인증 프로세스를 성공적으로 완료한 후 요청이 유지되는 동안 Authentication 인스턴스를 저장한다. Authentication 객체를 저장하는 인스턴스를 보안 컨텍스트라고 한다.
1
2
3
4
5
6
7
/* SecurityContext 인터페이스 */
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
SecurityContext의 주 책임은 Authentication 객체를 저장하는 것이다. 스프링 시큐리티는 관리자 역할을 하는 객체로 SecurityContext를 관리하는 세 가지 전략을 제공한다. 이 객체를 SecurityContextHolder라고 한다.
- MODE_THREADLOCAL : 각 스레드가 보안 컨텍스트에 각자의 세부 정보를 저장할 수 있게 해준다.
- MODE_INHERITABLETHREADLOCAL : MODE_THREADLOCAL과 비슷하지만 비동기 메서드의 경우 보안 컨텍스트를 다음 스레드로 복사하도록 스프링 시큐리티에 지시한다.
- 이 방식은 @Async 메서드를 새 스레드가 보안 컨텍스트를 상속하게 할 수 있다.
- MODE_GLOBAL : 애플리케이션의 모든 스레드가 같은 보안 컨텍스트 인스턴스를 보게 한다.
보안 컨텍스트를 위한 보유 전략 이용 - MODE_THREADLOCAL
MODE_THREADLOCAL 전략은 스프링 시큐리티가 보안 컨텍스트를 관리하는 기본 전략으로 ThreadLocal 을 이용해 컨텍스트를 관리한다.
ThreadLocal을 이용하기 때문에 애플리케이션의 각 스레드가 컬렉션에 저장된 데이터만 볼 수 있도록 보장하며 다른 스레드의 ThreadLocal 접근이 불가능하다.
MODE_THREADLOCAL는 기본 전략이기 때문에 명시적으로 구성할 필요가 없다.
1
2
3
4
5
6
7
8
9
10
11
/* SecurityContextHolder 에서 SecurityContext 열기 - 명시적 방법 */
public class TestController {
@GetMapping("/hello")
public String hello() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication a = context.getAuthentication();
return "Hello, " + a.getName() + "!";
}
}
스프링은 인증을 메서드 매개 변수에 곧바로 주입할 수 있으므로 엔드포인트 수준에서는 편하게 컨텍스트에서 인증을 얻을 수 있다.
1
2
3
4
5
6
7
8
/* SecurityContextHolder 에서 SecurityContext 열기 */
public class TestController {
@GetMapping("/hello")
public String hello(Authentication a) {
return "Hello, " + a.getName() + "!";
}
}
매번 명시적으로 사용할 필요없이 메서드 매개 변수로 선언하면 스프링 부트가 주입해준다.
비동기 호출을 위한 보유 전략 이용 - MODE_INHERITABLETHREADLOCAL
요청당 여러 스레드가 사용될 때는 상황이 복잡하다. 엔드포인트가 비동기가 되면 메서드를 실행하는 스레드와 요청을 수행하는 스레드가 다른 스레드가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
/* 다른 스레드로 실행되는 @Async 메서드 */
public class TestController {
@Async
@GetMapping("/bye")
public String bye(Authentication a) {
SecurityContext context = SecurityContextHolder.getContext();
Authentication a = context.getAuthentication();
// 로직 수행
}
}
1
2
3
4
5
6
/* @Async 활성화를 위한 어노테이션 추가 */
@Configuration
@EnableAsync
public class ProjectConfig {
}
설정이후 context.getAuthentication().getName()
을 호출하면 메서드가 보안 컨텍스트를 상속하지 않는 다른 스레드에서 실행되기 때문에 NullPointException 이 발생한다.
이 문제를 해결하는 방법이 MODE_INHERITABLETHREADLOCAL 이다. 이 전략을 설정하면 프레임워크는 요청의 원래 스레드에 있는 세부 정보를 비동기 메서드의 새로 생성된 스레드로 복사한다.
1
2
3
4
5
6
7
8
9
10
11
/* InitializeingBean을 이용해 SecurityContextHolder 모드 설정 */
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
}
독립형 애플리케이션을 위한 보유 전략 이용 - MODE_GLOBAL
보안 컨텍스트가 애플리케이션의 모든 스레드에 공유되는 전략을 원하면 MODE_GLOBAL 을 이용하면 된다.
이 전략은 일반적인 애플리케이션의 그림에는 맞지 않기 떄문에 웹 서버에는 이용되지 않는다. 백엔드 웹 애플리케이션은 수신하는 요청을 독립적으로 관리하므로 모든 요청에 대해 요청별로 보안 컨텍스트를 분리하는 것이 합리적이다.
1
2
3
4
5
6
7
8
9
10
11
/* InitializeingBean을 이용해 SecurityContextHolder 모드 설정 */
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_GLOBAL);
}
}
또한 SecurityContext는 ThreadSafe를 지원하지 않기 때문에 이 전략을 사용하면 Safe하게 구현해야 한다.
DelegatingSecurityContextRunnable로 보안 컨텍스트 전달
기본적으로 프레임워크는 요청의 스레드에 보안 컨텍스트를 제공하고 이 스레드만 이 보안 컨텍스트에 접근하도록 보장하지만 새로 생성된 스레드(비동기 메서드 이용)에 관해서는 별도의 작업을 하지 않는다.
즉, MODE_INHERITABLETHREADLOCAL 이더라도 프레임워크가 모르는 방법으로 코드가 새 스레드를 시작하면 처리할 수 없다.
이러한 스레드는 프레임워크가 관리해주지 않아 개발자가 관리해야 하므로 자체 관리 스레드라고 한다.
이는 DelegatingSecurityContextCallable(반환 값이 없는 경우), DelegatingSecurityContextRunnable(반환 값이 있는 경우) 을 통해 해결할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* DelegatingSecurityContextCallable 예시 */
public class TestController {
@GetMapping("/ciao")
public String ciao() throws Exception {
// 예시를 위한 Callable
Callable<String> task = () -> {
SecurityContext context = SecurityContextHolder.getContext();
return context.getAuthentication().getName();
};
ExcutorService e = Excutors.newCachedThreadPool();
try {
DelegatingSecurityContextCallable contextTask =
new DelegatingSecurityContextCallable<>(task);
return "Ciao, " + e.submit(contextTask).get() + "!";
} finally {
e.shutdown();
}
}
}
DelegatingSecurityContextExecutorService로 보안 컨텍스트 전달
작업을 장식하는 대신 특정 유형의 Excutor를 이용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* DelegatingSecurityContextExecutorService 예시 */
public class TestController {
@GetMapping("/hola")
public String hola() throws Exception {
// 예시를 위한 Callable
Callable<String> task = () -> {
SecurityContext context = SecurityContextHolder.getContext();
return context.getAuthentication().getName();
};
ExcutorService e = Excutors.newCachedThreadPool();
e = new DelegatingSecurityContextExecutorService(e);
try {
return "Ciao, " + e.submit(task).get() + "!";
} finally {
e.shutdown();
}
}
}
또한 스프링 시큐리티에는 유연성을 높이기 위한 DelegatingSecurityContextExecutor라는 더 추상적인 데코레이터 버전이 있다. 이 클래스는 스레드 풀 계층 구조의 가장 추상적인 계약인 Executor를 직접 장식한다.
보안 컨텍스트를 별도의 스레드로 전파하는 객체
클래스 | 설명 |
---|---|
DelegatingSecurity-ContextExecutor | Executor 인터페이스를 구현하며 Executor 객체를 장식하면 보안 컨텍스트를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공하도록 디자인했다. |
DelegatingSecurityContext-ExecutorService | ExecutorService 인터페이스를 구현하며 ExcutorService 객체를 장식하면 보안 컨텍스트를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공하도록 디자인했다. |