AuthenticationProvider는 인증논리를 담당
→ 요청을 허용할지 말지 결정하는 조건과 명령 발견
→ AuthenticationManager는 HTTP 필터 계층에서 요청을 수신하고, 이 책임을 AuthenticationProdivder에 위임하는 구성요소
AuthenticationProvider
인증과 관련하여 메세지, 코드 등과 같이 다양한 인증논리를 이용한다.
이를 구현할 수 있는 것이 AuthenticationProvider 이다
인증 프로세스 중 요청 나타내기
Authentication 인터페이스
- 인증 요청 이벤트를 나타내며
- 애플리케이션에 접근을 요청한 엔티티의 세부정보를 담는다
- 인증 요청 이벤트와 관련한 정보를 인증 프로세스 도중과 이후에 이용가능하다
애플리케이션 접근을 요청하는 사용자 = Principal(주체)
Authentication 은 Principal를 상속한다. 암호 같은 요구사항, 인증 요청에 대한 세부 정보를 더 추가할 수 있다
→ 계약의 주체를 나타냄
→ 또한 인증 프로세스 완료 여부, 권한 의 컬렉션 같은 정보를 추가로 가진다
public interface Authentication extends Principal, Serializable{
Collection<? extends GrantedAuthority> getAuthorities(); //인증된 요청에 허가된 권한의 컬렉션 반환
Object getCredentials(); // 인증 프로세스에 이용된 암호나 비밀 반환
Object getDetails();
Object getPrincipal();
boolean isAuthenticated(); // 인증 프로세스가 끝나면 true 반환, 진행중이면 false
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
맞춤형 인증 논리 구현
AuthenticationProvider인터페이스는
시스템의 사용자를 찾는 책임을 UserDetailService에 위임하고, PasswordEncoder로 인증 프로세스에서 암호를 관리한다
public interface AuthenticationProvider{
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider는 Authentication과 강력하게 결합되어있다
Authentication 객체를 매개변스로 받고 Authentication객체를 반환한다.
1. authenticate(Authentication authentication)
- 인증에 실패하면 exception을 투척
- 메서드가 현재 AuthenticationProvider 구현에서 지원하지 않는 인증 객체를 받으면 null를 반환
- 메서드는 완전히 인증된 객체면 Authentication인스턴스를 반환 -> isAuthenticated()메서드는 true를 반환
- 반환된 객체에는 인증된 엔티티의 모든 필수 세부 정보가 포함됨
- 다만 애플리케이션은 인스턴스에서 암호와 같은 민감한 데이터를 제거해야함
2.supports 메서드
- 현재 AuthenticationProvider가 Authentication객체로 제공된 형식을 지원하면 true
- 다만 true를 반환해도 authenticate메서드에서 null반환할 수 있음(요청 거부)
정리하자면 supperts 메서드는 어떤 잠금에 있어 유효한 카드인지 확인하고
" 이카드 유효해" 하면 authenticate인증을 시작하는 것이다
맞춤형 인증 논리 적용
AuthenticationProvider를 구현하여 어떤 종류의 Authentication 객체를 지원할지 결정
이후 스프링 시큐리티에 새 AuthenticationProvider를 등록
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
@Autowired
private UserDetailsService userDetailService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public boolean supports(Class<?> authenticationType){
return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
}
@Override
public Authentication 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 UsernamePasswordAuthenticationToken(
username, password, u.getAuthorities()
);
}else{
throw new BadCredentialsException("잘못됨");
}
}
}
만약 암호가 일치한다면 AuthenticationProvider는 요청의 세부정보를 포함하는 Authentication을 '인증됨'으로 표시하고 반환
이후 새 AuthenticationProvider를 WbSecurityConfig에 재정의 하면 된다
@Configuration
public class SecurityConfig {
....
@Bean
public AuthenticationProvider customAuthenticationProvider() {
CustomAuthenticationProvider provider = new CustomAuthenticationProvider();
...
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
}
스프링 시큐리티 버전이 업그레이드 되면서 원래는 Adapter라는 것을 오버라이드 하여 cofigure()메서드에 등록하면 되는데
최신버전에서는 찾아본 결과 @Bean으로 등록해야한다고 한다
또한 configure메서드에서 auth.authenticationProvider(customAuthenticationProvider())을 호출하여AuthenticationProvider을 등록해야함
SecurityContext
인증 프로세스가 끝난후 인증된 엔티티에 대한 세부 정보가 필요할 가능성이 크다
예를 들어 인증된 사용자의 이름이나 권한을 참조해야할 수 있다
인증 프로세스가 완료된 후에도 이 정보에 접근할 수 있을끼??
→ Authentication 객체를 저장하는 보안 컨텍스트(Security Context)를 통해 가능하다
예를 들어보자
"/products"라는 URI에 접속한다면 인증필터가 요청을 가로채고 인증관리자가 인증 책임을 다한다
만약 인증이 되면 세부정보가 보안 컨텍스트에 저장된다
그럼 해당 컨트롤러가 이제 보안컨텍스트에 있는 세부 정보를 이용할 수 있다
public interface SecurityContext extends Serializable{
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
→ SecurityContext 주책임 : Authentication 객체를 저장
그렇다면 SecuriyContext 자체는 어떻게 관리될까?? 세가지 전략을 제공하는 SecurityContextHolder라는 객체가 있다
1. MODE_THREADLOCAL
- 각 스레드가 보안 컨텍스트에 각자의 세부 정보를 저장할 수 잇게 해준다
- 요청당 스레드 방식의 웹 애프리케이션에서는 각요청이 개별 스레드를 가지므로 이는 일반적인 접근법이다
2. MODE_INHERITABLETHREADLOCAL
- 위의 방식과 비슷하지만 비동기 메서드의 경우 보안 컨텍스트를 다음 스레드로 복사하도록 스프링 시큐리티에 지시한다
- 이 방식으로 @Async 메서드를 실행하는 새 스레드가 보안 컨텍스트를 상속하게 할 수 있다
- 비동기 메서드 : 작업을 시작한후 다른 작업을 수행할 수 있으며, 작업이 완료되면 해당 작업의 결과를 처리가능
3. MODE_GLOBAL
- 애플리케이션의 모든 스레드가 같은 보안 컨텟스트 인스턴스를 보게한다
보안 컨텍스트를 위한 보유 전략 이용
MODE_THREADLOCAL
- 스프링 시큐리티의 기본전략
- 각 스레드가 자신의 보안컨텍스트에만 접근가능하며 다른 스레드에는 접근 불가능하다
- 다만 한 요청에서 예를들어 비동기 메서드 호출로 새 스레드가 호출되면 해당 새로운 스레드도 자체 보안 컨텍스트를 가져 상위 스레드의 세부 정보가 새 스레드의 보안 컨텍스트로 복사되지 않는다
만약 애플리케이션 엔드포인트 중 하나에서 보안 컨텍스트를 얻는 방법은
@GetMapping("/test")
public String test(){
SecurityContext context=SecurityContextHolder.getContext();
Authentication a= context.getAuthentication();
return a.getName();
}
스프링은 인증을 메서드 매개변수로 곧바로 주입할 수 있어서 매개변수에 "Authenctication a"를 넣어도 가능하다
curl -u username:password http://localhost:8080/test
비동기 호출을 위한 보유 전략 이용
MODE_INHERITABLETHREADLOCAL
요청당 여러 스레드가 사용될때
→ 엔드포인트가 비동기되면 메서드를 시행하는 스레드와 요청을 수행하는 스레드가 다른 스레드가 됨
@GetMapping("/bye")
@Async
public void goodbye() {
SecurityContext context = SecurityContextHolder.getContext();
String username = context.getAuthentication().getName();
}
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
}
만약 @Bean 설정이 없다고 가정하고 실행해보면
NullPointerException이 발생한다
그 이유는 메서드가 보안 컨텍스르를 상속하지 않는 다른 스레드에서 실행되기 때문이다.
따라서 Authorization객체는 null이 된다
이런 문제를 MODE_INHERITABLETHREADLOCAL 을 통해 해결가능하다
원래 스레드의 세부정보를 비동기 메서드의 새로 생성된 스레드로 복사한다
이 전략을 사용하고 싶다면 위의 처럼 @Bean으로 등록하거나 spring.security.strategy시스템 속성을 이용하면 된다
독립형 애플리케이션을 위한 보유 전략 이용
MODE_GLOBAL
모든 스레드가 같은 보안컨텍스트에 접근하는 것이다.
모든 스레드가 해당 정보를 변경할 수 있기에 경합 상황이 일어날 수 잇으므로 동기화 처리를 해야한다
또한 SecurityContext는 스레드 안전을 지원하지 않으므로 이 전략에서는 모든 스레드가 SecurityContext객체에 접근할 수 있으므로 동시접근을 해결해야한다
위의 전략과 비슷하게 @Bean으로 등록하거나 시스템 속성을 이용하면 된다
HTTP Basic 인증과 양식 기반 로그인 인증 이해하기
HTTP Basic 이용 및 구성
API와 같이 클라이언트가 직접 HTTP 요청을 보내는 경우 유용
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic(c -> {
c.realmName("OTHER");
c.authenticationEntryPoint(new CustomEntryPoint());
});
http.authorizeRequests().anyRequest().authenticated();
}
}
- realm 이란 보호되는 자원 또는 보호되는 영역을 나타내며 사용자가 해당 영역에 접근하기 위해 제공해야하는 자격증명(인증 정보)를 나타냄
- realmName은 일반적으로 보안 구성에서 사용자에게 어떤 자격 증명을 제공해야 하는지에대한 안내메세지 또는 영역이름을 지정하는데 사용
그래서 만약 클라이언트가 서버에 HTTP를 요청하면 (authencticaionEntryPoint가 없다면)
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="OTHER"
와 같이 응답이 나올거이다
만약 인증 실패에 대한 응답을 맞춤구성하려면 AuthenticationEntryPoint를 구현하면 된다
public class CustomEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
httpServletResponse.addHeader("응답을 구성하자");
httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
}
}
이렇게 하면 헤더에 "응답을 구성하자"라는 내용이 추가될 것이다
양식 기반 로그인으로 인증 구현
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.httpBasic();
http.authorizeRequests()
.anyRequest().authenticated();
}
}
만약 formLogin()을 이용하면 스프링 시큐리티에서 제공하는 간단한 로그인, 로그아웃 양식을 제공하여 이용가능하다
위의 같이 Handler말고 " .defaultSuccesUrl("/home",true) " 을 통해 URI호출이 가능하다.
다만 더 세부적인 맞춤 구성이 필요하다면 hanlder를 이요하면 된다
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
var authorities = authentication.getAuthorities();
var auth = authorities.stream()
.filter(a -> a.getAuthority().equals("read"))
.findFirst();
if (auth.isPresent()) {
httpServletResponse.sendRedirect("/home");
} else {
httpServletResponse.sendRedirect("/error");
}
}
}
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) {
httpServletResponse.setHeader("failed", LocalDateTime.now().toString());
}
}
정리
- AuthenticationProvider 구성 요소를 이용하면 맞춤형 인증 논리를 구현할 수 있다
- 맞춤형 인증논리를 구현할때 책임을 분리하는 것이 좋다→ AuthenticationProvider는 사용자 관리는 UserDetailsService에 위임하고 암호 검증책임은 PasswordEncoder에 위임한다
- SecurityContext는 인증이 성공한 후 인증된 엔티티에 대한 세부 정보를 유지한다
- 보안 컨텍스트를 관리하는 전략들은 다른 스레드에서 보안 컨텍스트 세부 정보에 대한 접근 방법들이 다르다
- formLogin 인증 메서드는 세부적으로 맞춤구성이 가능하며 HTTP basic방식과 함께 이용해 두 인증 유형을 모두 지원할 수 도 있다
'개발 이론 > Spring Security' 카테고리의 다른 글
[Spring Security] 권한 부여 : 제한 적용 (1) | 2023.11.12 |
---|---|
[Spring Security] 권한 부여 구성 : 액세스 (0) | 2023.11.10 |
[Spring Security] UserDetails, UserDetailsService, PasswordEncoder, AuthenticationProvider로 간단 예제 (0) | 2023.11.08 |
[Spring Security] PasswordEncoder (1) | 2023.11.02 |
[Spring Security] UserDetailService, UserDetails (1) | 2023.11.01 |