인증 논리를 담당하는 부분은 AuthenticationProvider
이다. AuthenticationManager
는 HTTP 요청을 수신하고 AuthenticationProvider
에게 인증 책임을 위임한다. 이 단원에서는 인증 결과가 두 가지인 인증 프로세스를 살펴본다.
요청하는 엔티티가 인증되지 않는다
애플리케이션이 사용자를 인식하지 못해 권한 부여 프로세스에 위임하지 않고 요청을 거부한다. 일반적으로 이 경우 클라이언트에 HTTP 401 Unauthorized 응답이 반환된다.
요청하는 엔티티가 인증된다
요청자의 UserDetails
가 SecurityContext
에 저장되어 애플리케이션이 이를 권한 부여에 이용할 수 있다.
AuthenticationProvider의 이해
어떠한 시나리오가 주어지더라도 구현할 수 있게 해주는 것이 프레임워크의 목적이다. 엔터프라이즈 애플리케이션에서는 사용자의 이름과 암호 기반의 기본 인증 구현이 적합하지 않을 수 있다. 또한 인증과 관련해서 여러 시나리오를 구현해야 할 수 있다.
스프링 시큐리티에서는 AuthenticationProvider
계약으로 모든 맞춤형 인증 논리를 정의할 수 있다.
인증 프로세스 중 요청 나타내기
AuthenticationProvider
를 구현하기 위해서는 인증 이벤트 자체를 나타내는 방법을 이해해야 한다. Authentication
은 인증 프로세스의 필수 인터페이스이다. 이 인터페이스는 인증 요청의 이벤트를 나타내며 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담는다. 인증 요청 이벤트와 관한 정보는 인증 프로세스 도중 그리고 이후에 사용할 수 있다. 애플리케이션에 접근을 요청하는 사용자를 주체(Principal)이라고 한다. 스프링 시큐리티의 Authentication
인터페이스는 자바 시큐리티 API의 Principal
인터페이스를 확장한다.
스프링 시큐리티의 Authentication
계약은 주체만 나타내는 것이 아니라 인증 프로세스 완료 여부, 권한의 컬렉션 같은 정보를 추가로 가진다. 이 계약은 자바 시큐리티의 Principal
계약을 extends
하여 설계되었다. 따라서, 다른 프레임워크나 애플리케이션 구현에서 호환성이 높다.
현재 이 계약에서 알아야 할 메서드는 다음과 같다.
isAuthenticated()
- 인증 프로세스가 끝났으면 true를 아직 진행 중이면 false를 반환한다.getCredentials()
- 인증 프로세스에 이용된 암호나 비밀번호를 반환한다.getAuthorities()
- 인증된 요청에 허가된 권환의 컬렉션을 반환한다.
Custom AuthenticationProvider 구현
AuthenticationProvider
의 기본 구현은 UserDetailsService
에서 사용자를 찾고 PasswordEncoder
에서 사용자의 암호를 검증한다.
AuthenticationProvider
책임은 Authentication
과 강하게 결합되어 있다. 인증 로직을 정의하려면 authenticate
메서드를 구현해야 하는데 구현 방법을 아래 세 항목으로 간단하게 요약할 수 있다.
- 인증에 실패하면
AuthenticationException
을 발생시킨다. - 현재
AuthenticationProvider
구현에서 지원하지 않는 증명 방식이면null
을 반환한다. HTTP 필터 수준에서 분리된 여러Authentication
형식을 사용할 가능성이 생긴다. - 인증이 완료되었다면
authenticate
메서드의 결과물로Authentication
인스턴스를 반환해야 한다. 이 인스턴스에 대해isAuthenticated
메서드는 true를 반환해야 한다. 또한 인증이 완료되었으므로 비밀빈호와 같은 민감 정보는 제거하는 것이 좋다.
AuthenticationProvider
의 두 번째 메서드는 supports(Class<?> authentication)
다. 이 메서드는 현재 요청이 지원하는 인증 형식이면 true를 반환한다. 하지만 이 메서드에서 true를 반환했다고 하더라도 authenticate
메서드에서 null을 반환할 수 있는데 이는 인증 세부 정보를 기준으로 요청을 거부했기 때문이다.
AuthenticationManager
는 사용가능한 인증 공급자 중 하나에 인증을 위임한다. AuthenticationProvider
는 주어진 인증 유형을 지원하지 않거나 객체 유형은 지원하지만 해당 특정 객체를 인증하는 방법을 모를 수 있다. 인증을 평가한 후 요청이 올바른지 판단할 수 있는 AuthenticationProvider
가 AuthenticationManager
에 응답한다.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
// 생략된 코드
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
어떤 종류의 Authentication
을 지원할지 정의해야 한다. 이는 authentication
메서드의 매개 변수에 어떤 형식이 전달되는지에 따라 달라진다. AuthenticationFilter
에서 별다른 구성을 하지 않았다면 UsernamePasswordAuthenticationToken
클래스가 형식을 정의한다.
어떤 형식을 지원할지 결정했으므로 authenticate
메서드를 구현할 수 있다.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails u = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, u.getPassword())) {
return new UsernamePasswordAuthenticationToken(
username,
password,
u.getAuthorities()
);
}
throw new BadCredentialsException("Something went wrong!");
}
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
}
UserDetails
를 가져오기 위해 UserDetailsService
구현을 사용한다. 이 때 사용자를 찾지 못하거나 PasswordEncoder
로 검증했을 때 비밀번호가 일치하지 않는다면 AuthenticationException
을 발생시킨다. 그러면 인증 프로세스는 중단되고 인증 필터가 응답 상태를 401 Unauthorized를 설정한다. 인증이 성공된 경우 요청의 세부정보를 포함하는 Authentication
을 인증됨으로 표시하고 반환한다.
이를 그림으로 나타내면 위와 같다.
이제 구현한 AuthenticationProvider
를 연결하려면 프로젝트의 구성 클래스에서 추가해줘야 한다. 책에서는 deprecated된 WebSecurityConfigAdapter
를 사용해 예시를 조금 수정했다.
@Configuration
public class ProjectConfig{
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
String usersByUsernameQuery =
"select username, password, enabled from users where username = ?";
String authByUserQuery =
"select username, authority from authorities where username = ?";
var userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authByUserQuery);
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider) {
return new ProviderManager(authenticationProvider);
}
}
ProviderManager
는 AuthenticationManager
의 대표 구현체이다. AuthenticationProvider
리스트나 가변 인자를 전달하면 전달된 여러 AuthenticationProvider
를 가지는 ProviderManager
가 생성된다.
앞에서 @Component
어노테이션을 CustomAuthenticationProvider
에 추가해줬기 때문에 스프링 컨텍스트가 이를 사용한다.
SecurityContext 이용
인증 프로세스가 끝난 이후에도 엔티티 세부정보가 필요하다. 사용자가 어떤 권한을 가지는지에 따라 할 수 있는 작업이 다르기 때문이다. 이 때문에 인증이 끝나도 요청이 유지되는 동안 Authentication
객체를 SecurityContext
에 저장한다.
SecurityContext
계약의 핵심 책임은 Authentication
을 저장하는 것이다. 그리고 스프링 시큐리티는 SecurityContextHolder
라는 관리자 역할 객체로 세 가지 전략을 통해 SpringSecurityContext
를 관리한다.
MODE_THREADLOCAL
각 스레드가 SecurityContext
에 각자의 세부정보를 저장할 수 있게 해준다. 요청 당 스레드 방식의 웹 애플리케이션에서는 각 요청이 개별 스레드를 가지므로 일반적인 접근이다.
MODE_INHERITABLETHREADLOCAL
MODE_THREADLOCAL과 비슷하지만 비동기 메서드의 경우 SecurityContext
를 다음 스레드로 복사하도록 스프링 시큐리티에 지시한다. 이 방식으로 @Async
메서드를 실행하는 새 스레드가 보안 컨텍스트를 상속하게 할 수 있다.
MODE_GLOBAL
애플리케이션의 모든 스레드가 같은 SecurityContext
인스턴스를 보게 한다.
위 세 가지 전략 외에 개발자가 스프링에 알려지지 않은 새 스레드를 정의하면 명시적으로 SecurityContext
의 세부 정보를 새 스레드로 복사해야 한다. 스프링 시큐리티는 스프링 컨텍스트에 있지 않은 객체를 자동으로 관리할 수 없지만, 이를 위해 유용한 유틸리티 클래스를 제공한다.
SecurityContext 기본 전략 사용
MODE_THREADLOCAL
은 스프링 시큐리티가 SecurityContext
를 관리하는 기본 전략이다. ThreadLocal
은 JDK에 있는 구현이며 이 구현은 각 스레드가 컬렉션에 저장된 데이터만 볼 수 있도록 보장한다. 각 요청은 자신의 SecurityContext
에 접근하며, 다른 스레드의 ThreadLocal
에 접근할 수 없다. 아래와 같이 T1’에 새 스레드가 생길 경우 기존 SecurityContext A의 세부 내용이 복사되지 않는다.
SecurityContext
를 관리하는 기본 전략이므로 명시적으로 구성할 필요가 없다. 인증 프로세스가 끝난 이후 필요할 때마다 SecurityContextHolder.getContext()
메서드를 통해 SecurityContext
를 요청만 하면 된다. 이후 SecurityContext
에서 getAuthentication
메서드로 Authentication
을 얻올 수 있다.
기본 전략을 고수하는 것이 더 쉬우며 대부분은 이 전략으로 충분하다. MODE_THREADLOCAL
은 각 스레드의 SecurityContext
를 격리할 수 있게 해주고 SecurityContext
를 더 자연스럽고 이해하기 쉽게 만들어준다.
비동기 호출을 위한 전략 이용
하지만 요청당 여러 스레드가 사용될 때는 상황이 복잡하다. 엔드포인트가 비동기가 되면 요청을 처리하는 스레드와 메서드를 실행하는 스레드가 다른 스레드가 된다.
@GetMapping("/bye")
@Async
public void goodbye() {
SecurityContext context = SecurityContextHolder.getContext();
String name = context.getAuthentication().getName();
// 사용자 이름으로 작업
}
@Async
기능을 활성화하기 위해 @EnableAsync
어노테이션을 Config
클래스에 추가해줘야 한다.
@Configuration
@EnableAsync
public class ProjectConfig {
}
String username = context.getAuthentication.getName();
현재 상태로 코드를 실행해보면 인증에서 이름을 얻는 다음 행에서 NullPointerException
이 발생한다.
이는 SecurityContext
를 상속하지 않는 다른 스레드에서 실행되기 때문이다. 이를 MODE_INHERITABLETHREADLOCAL
전략으로 해결할 수 있다.
SecurityContextHolder
클래스를 살펴보면 strategyname 필드가 있다. 이 필드는 spring.security.strategy
에서 값을 가져오는 것을 볼 수 있다.
또는 setStrategyName
메서드로 설정해줄 수 있다.
이 전략을 사용하면 프레임워크는 요청의 원래 스레드에 있는 세부 정보를 비동기 메서드의 새로 생성된 스레드로 복사한다.
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
ProjectConfig 클래스에 위 메서드를 추가하면 /bye
엔드포인트에 접근했을 때 Authentication
이 제대로 복사된다.
위 빈을 등록하지 않으면 아래와 같은 오류가 발생한다.
java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getName()" because the return value of "org.springframework.security.core.context.SecurityContext.getAuthentication()" is null
빈을 등록하면 정상 출력된다.
위와 같이 시작 시 옵션을 줘서 실행할 수도 있다.
독립형 애플리케이션을 위한 전략 사용
SecurityContext
가 애플리케이션의 모든 스레드에 공유되는 전략을 원한다면 MODE_GLOBAL
을 이용하면 된다. 이 전략은 일반적인 웹 애플리케이션과 맞지 않기 때문에 웹 서버에서는 사용되지 않는다. 독립형 애플리케이션에는 공유하는 것이 좋은 전략일 수 있다.
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_GLOBAL);
}
하지만 이 전략에서는 애플리케이션의 모든 스레드가 SecurityContext
객체에 접근할 수 있으므로 개발자가 동시 접근을 해결해야 한다.
DelegatingSecurityContextRunnable로 SecurityContext 전달
새로 생성된 스레드의 경우 스프링 컨텍스트가 알고 있는 경우에는 MODE_INHERITABLETHREADLOCAL
전략을 통해 SecurityContext
를 복사할 수 있었다. 그러나 프레임워크가 모르는 방법으로 코드가 새 스레드를 시작하면 여전히 빈틈이 생긴다. 이러한 스레드는 프레임워크가 관리해주지 않아 개발자가 관리해야 하므로 자체 관리 스레드라고 한다.
자체 관리 스레드는 개발자가 SecurityContext
를 전파해야 한다. 한 가지 해결책은 별도의 스레드에서 실행하고 싶은 작업을 DelegatingSecurityContextRunnable
또는 DelegatingSecurityContextCallable<T>
을 사용하는 것이다. 반환 값이 없다면 Runnable
을, 있다면 Callable<T>
를 사용하면 된다.
두 클래스 모두 다른 Runnable
또는 Callable
과 마찬가지로 비동기적으로 실행되는 작업을 나타내며, 작업을 실행하는 스레드를 위해 현재 SecurityContext
를 복사시켜 준다.
@GetMapping("/ciao")
public String ciao() throws Exception {
Callable<String> task = () -> { // Callable 작업 선언
SecurityContext context = SecurityContextHolder.getContext();
return context.getAuthentication().getName(); // 현재 Authentication 이름 반환
};
ExecutorService e = Executors.newCachedThreadPool();
try {
return "Ciao, " + e.submit(task).get() + "!";
} finally {
e.shutdown();
}
}
위 상태로 실행하면 SecurityContext
를 복사해주지 않았기 때문에 NPE
가 발생한다. 따라서, DelegatingSecurityContextCallable
로 감싸줘야 한다.
@GetMapping("/ciao")
public String ciao() throws Exception {
Callable<String> task = () -> {
SecurityContext context = SecurityContextHolder.getContext();
return context.getAuthentication().getName();
};
ExecutorService e = Executors.newCachedThreadPool();
try {
var contextTask = new DelegatingSecurityContextCallable<>(task);
return "Ciao, " + e.submit(contextTask).get() + "!";
} finally {
e.shutdown();
}
}
위와 같이 바꿔주면 제대로 출력된다.
DelegatingSecurityContextExecutorService로 SecurityContext 전달
프레임워크에 알리지 않고 코드에서 시작한 스레드를 다룰 때는 SecurityContext
에서 다음 스레드로의 전파를 관리해야 한다. 위에서 살펴보았던 두 클래스는 모두 작업 단위에서 SecurityContext
를 복사했다. 이번에는 스레드 풀에서 전파를 관리하는 법을 살펴보자.
작업을 데코레이트하는 대신 특정 유형의 Executor
를 사용할 수 있다. 이번에는 DelegatingSecurityContextExecutorService
가 ExecutorService
를 감싼다.
@GetMapping("/hola")
public String hola() throws Exception {
Callable<String> task = () -> {
SecurityContext context = SecurityContextHolder.getContext();
return context.getAuthentication().getName();
};
ExecutorService e = Executors.newCachedThreadPool();
e = new DelegatingSecurityContextExecutorService(e);
try{
return "Hola, " + e.submit(task).get() + "!";
} finally {
e.shutdown();
}
}
클래스 | 설명 |
---|---|
DelegatingSecurityContextExecutor | Executor 인터페이스를 구현하며 Executor 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다. |
DelegatingSecurityContextExecutorService | ExecutorService 인터페이스를 구현하며 ExecutorService 객체를 장식하면 SecurityContext 를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다. |
DelegatingSecurityContextScheduledExecutorService | ScheduledExecutorService 인터페이스를 구현하며 ScheduledExecutorService 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 스레드로 전달하는 기능을 제공한다. |
DelegatingSecurityContextRunnable | Runnable 인터페이스를 구현하고 다른 스레드에 의해 실행되며 응답을 반환하지 않는 작업을 나타낸다. Runnable 기능에 더해 새 스레드에서 사용하기 위한 SecurityContext 를 복사한다. |
DelegatingSecurityContextcallable | Callable인터페이스를 구현하고 다른 스레드에 의해 실행되며 최종적으로 응답을 반환하는 작업을 나타낸다. Callable 기능에 더해 새 스레드에서 사용하기 위한 SecurityContext 를 복사한다. |
HTTP Basic 인증과 form 기반 로그인 인증 이해하기
지금까지는 인증 방식으로 HTTP Basic
을 이용했다. 이는 실제 애플리케이션에는 적합하지 않을 수 있다.
HTTP Basic 이용 및 구성
이론적인 시나리오에서는 HTTP Basic
으로 충분하지만 더 복잡한 애플리케이션에서는 추가 구성이 필요할 수 있다.
HTTP Basic
의 명시적인 설정은 아래와 같이 할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
return http.build();
}
Customizer<T>
의 함수형 인터페이스를 통해 반환값을 지정하고 HttpSecurity
의 httpBasic()
메서드를 호출할 수도 있다. 아래와 같이 영역(realm)을 특정 인증 방식을 이용하는 보호 공간으로 생각할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(c -> c.realmName("OTHER"));
return http.build();
}
여기서 이용된 람다식은 Customizer<HttpBasicConfigurer<HttpSecurity>>
형식의 객체인데, HttpBasicConfigurer<HttpSecurity>
형식의 매개 변수로 realmName()
을 호출해 영역 이름을 변경할 수 있게 해준다. cURL에 -v
플래그를 지정하면 반환된 자세한 HTTP 응답을 볼 수 있는데 영역 이름이 OTHER로 변경된 걸 확인할 수 있다. 하지만 WWW-Authenticate 헤더는 응답에서 HTTP 401일 때만 있다.
또한 Customizer
로 인증이 실패했을 때의 응답을 맞춤 구성할 수 있다. 인증이 실패했을 때 클라이언트가 응답에서 특정한 항목을 기대하는 경우 이를 위해 하나 이상의 헤더를 추가하거나 제거해야할 수 있다. 또는 애플리케이션이 민감한 데이터를 클라이언트에 노출하지 않도록 응답 본문을 필터링하는 로직을 작성할 수 있다.
인증이 실패했을 때 응답을 맞춤 구성하려면 AuthenticationEntryPoint
를 구현하면 된다.
AuthenticatioEntryPoint
의 commence
메서드는 HttpServletRequest
, HttpServletResponse
, 그리고 인증 실패를 일으킨 AuthenticationException
을 받는다.
AuthenticationEntryPoint
인터페이스는 스프링 시큐리티 아키텍처에서 ExceptionTranslationManager
라는 구성 요소에서 직접 사용되며 이 구성 요소는 필터 체인으로 던져진 모든 AccessDeniedException
및 AuthenticationException
을 처리한다.
public class CustomEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.addHeader("message", "something went wrong!");
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
}
이제 설정 클래스에서 HTTP Basic
인증을 위해 직접 만든 CustomEntryPoint를 등록할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(c -> {
c.realmName("OTHER");
**c.authenticationEntryPoint(new CustomEntryPoint());**
});
return http.build();
}
양식 기반 로그인으로 인증 구현
작은 웹 기반 애플리케이션에서는 스프링 시큐리티가 제공하는 양식 기반 인증 방식을 사용할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
이를 위해서는 HttpBasic
대신 formLogin
을 추가하면 된다. 위와 같이 구현하면 아직 다른 정의된 페이지가 없으므로 로그인한 후에는 기본 오류 페이지로 리다이렉션된다.
기존 방식과의 차이는 JSON 형식이 아닌 HTML을 반환하는 엔드포인트를 원한다는 점이다. 이를 위해 resources/static
경로에 home.html을 생성하고 아래와 같이 Controller를 하나 더 정의해줬다.
@Controller
Public class HomeController {
@GetMapping("/home")
public String home() {
return "home.html";
}
}
로그인하지 않고 아무 경로에 접근하려고 하면 로그인 페이지로 리다이렉션되고 이 때 로그인을 하면 원래 가려던 페이지로 리다이렉션된다. formLogin()
메서드는 FormLoginConfigurer<HttpSecurity>
형식의 객체를 반환하며 이를 이용해 맞춤 구성을 할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(configurer -> configurer.defaultSuccessUrl("/home", true))
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
);
return http.build();
}
defaultSuccessUrl()
메서드로 로그인이 성공하면 보낼 기본 페이지를 설정할 수 있다.
더 세부적인 맞춤 구성이 필요하면 AuthenticationSuccessHandler
및 AuthenticationFailureHandler
객체를 이용할 수 있다. 이러한 인터페이스를 이용하면 인증을 위해 실행되는 논리를 적용할 객체를 구현할 수 있다.
AuthentciationSuccessHandler
의 onAuthenticationSuccess()
메서드는 매개변수로 서블릿 요청과 응답 그리고 Authentication
객체를 받는다. 아래와 같이 권한에 따라 다른 리다이렉션을 수행하도록 할 수 있다.
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
var authorities = authentication.getAuthorities();
var auth = authorities.stream()
.filter(a -> a.getAuthority().equals("read"))
.findFirst(); // read 권한이 없으면 빈 Optional 객체 반환
if (auth.isPresent()) {
response.sendRedirect("/home");
return;
}
response.sendRedirect("/error");
}
}
실제 시나리오에서는 인증에 실패하면 클라이언트에 특정 형식의 응답이 필요한 상황이 있다. 인증에 실패했을 때 실행할 로직을 맞춤 구성하려면 AuthenticationFailureHandler
를 구현하면 된다. 이 클래스 역시 서블릿 요청과 응답을 갖지만 AuthenticationException
객체를 받는다.
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setHeader("failed", LocalDateTime.now().toString());
}
}
이 후 두 객체를 사용하려면 successHandler()
와 failureHandler()
로 지정해줘야 한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(configurer -> {
configurer.defaultSuccessUrl("/home", true);
configurer.successHandler(authenticationSuccessHandler);
configurer.failureHandler(authenticationFailureHandler);
})
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
);
return http.build();
}
이제 올바른 사용자 이름과 암호를 이용해서 HTTP Basic
방식으로 /home
에 접근하려고 해도 HTTP 302를 반환한다. 여기에 HTTP Basic
을 추가하면 양식 기반과 HTTP Basic 기반 모두 정상 작동하게 할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(configurer -> {
configurer.defaultSuccessUrl("/home", true);
configurer.successHandler(authenticationSuccessHandler);
configurer.failureHandler(authenticationFailureHandler);
})
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
).httpBasic(Customizer.withDefault());
return http.build();
}
'Java > Spring' 카테고리의 다른 글
스프링 시큐리티 인 액션] 4장_암호처리 (0) | 2024.06.21 |
---|---|
스프링 시큐리티 인 액션] 3장_사용자 관리 (0) | 2024.06.19 |
MVC 패턴이란? (0) | 2023.11.05 |