AuthenticationProvider 인터페이스가 인증 논리를 담당한다.

 

AuthenticationProvider 이해

실제 인증은 여러 방식이 있을 있다.

단순한 id/pw 인증이 전부가 아니다.

 

스프링 시큐리티는 AuthenticationProvider 모든 인증을 추상화하고, 이게 맞게 구현만 하면 어떠한 인증 시나리오도 구축할 있다.

 

인증에 사용되는 Authentication

Authentication 인증에 사용되는 필수 인터페이스다.

아래 처럼 인자로 사용되어 인증에 사용된다.

 

Authentication Principal 확장한다. Principal 자바 시큐리티 인터페이스다.

 

 

인증 논리 구현

authenticate() 인증 성공 완전히 인증된 Authentication 반환해야 한다.

isAuthenticated() true 반환하고, 모든 필수 정보가 포함되야한다.

, 보안 상의 이유로 자격증명은 제거될 있다.

인증이 이미 성공했기 때문에 이상 자격증명 같은 민감한 정보가 필요없기 때문이다.

 

supports() 매개변수 Authentication 인증 사용할 있는지 여부를 리턴한다.

주의할 것은 메서드가 true 리턴해도 authenticate()메서드에서 null 리턴해 인증을 거부할 있다.

supports() false 경우 AuthenticaitonManager 인증을 시도하지 않는다. 바로 다음 AuthenticationProvider에게 인증을 위임한다.

 

맞춤형 인증 논리 구현

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.stereotype.Component;

 

@Component

public class CustomAuthenticationProvider implements AuthenticationProvider{

        Logger logger = LoggerFactory.getLogger(getClass());

        

        @Autowired

        private UserDetailsService userDetailsService;

        

        @Autowired

        private PasswordEncoder passwordEncoder;

        

        @Override

        public Authentication authenticate(Authentication authentication) throws AuthenticationException {

                logger.info("=========== 인증 시작 ===========");

                

                String username = authentication.getName();

                String password = authentication.getCredentials().toString();

                

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                

                if(passwordEncoder.matches(password, userDetails.getPassword())) {

                        return new UsernamePasswordAuthenticationToken(

                                        username, password, userDetails.getAuthorities());

                }

             

                throw new BadCredentialsException("인증 실패");

        }

        @Override

        public boolean supports(Class<?> authentication) {

                return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);

        }

}

 

참고로 권한 정보까지 사용해서 UsernamepasswordAuthenticationToken 생성하면

아래와 같이 완전히 인증됨을 의미하는 플래그 값을 true 설정한다.

실행

 

 

 

SecurityContext 이용

SecurityContext(이하 보안 컨텍스트)

 

인증 프로세스가 끝난 인증된 Authentication 대한 세부 정보가 필요할 가능성이 크다.

주로 이름과 권한을 지속적으로 확인해야할 있다.

SecurityContext Authentication 객체가 요청이 유지되는 동안 내부에 저장한다.

 

 

 

보안 컨텍스트는 이게 전부다. 보면 있듯 인증 객체를 저장하는 것이 역할이다.

 

보안 컨텍스트를 관리하는 SecurityContextHolder는 3 가지 모드를 지원한다.

 

MODE_THREADLOCAL

스레드에 안전한 저장소에 보안 문맥을 저장한다.

일반적으로 WAS 하나의 요청 하나의 쓰레드를 가지므로 기본이 되는 전략이다.

 

MODE_INHERITABLETHREADLOCAL

MODE_THREADLOCAL와 유사하다.

비동기 메서드의 경우, 다음 스레드로 복사해서 사용한다.

예를들어, @Async 메서드를 실행하는 스레드가 보안 컨텍스트를 상속한다.

 

MODE_GLOBAL

애플리케이션의 모든 스레드가 같은 보안 컨텍스트를 공유한다.

 

스프링 시큐리티는 이외에도 사용자가 커스텀한 전략을 사용하도록 설정할 있다.

 

보안 컨텍스트를 위한 보유 전략 이용

MODE_THREADLOCAL

기본 전략은 MODE_THREADLOCAL 이다.

내부적으로 ThreadLocal 사용한다.

ThreadLocal 저장된 데이터는 다른 쓰레드에선 접근이 불가능하다.

이는 요청마다 새로운 쓰레드를 생성하는 일반적인 WAS 맞아떨어지는 동작방식이다.

 

만약 서버가 비동기 기술을 사용한다면 문제가 있다. 기존 쓰레드에서 새로운 쓰레드를 생성해 실행하면, 새로 생성된 쓰레드는 접근을 하지 못하기 때문이다.

6번에 저장된 보안 컨텍스트를 7번에서 꺼내서 사용할 있다.

 

 

 

MODE_INHERITABLETHREADLOCAL

비동기를 사용하는 상황에서 전략

 

비동기 설정

 

 

 

아직 보안 컨텍스트 모드를 설정하지 않아 기본 모드인 MODE_THREADLOCAL 동작하기에 값이 없다.

 

InitializingBean 빈을 구현하면, 스프링이 컨테이너를 초기화하는 과정에 호출해준다.

 

 

정상적으로 값이 나온다.

 

주의할 것은 방법은 선언적으로 작성한 코드에 대해서만 동작을 한다.

선언적으로 작성한 코드는 프레임워크에서 런타임 중에 비동기임을 알기 때문에 자동으로 처리가 된다.

 

개발자가 메서드 안에서 코드로 쓰레드를 만들어 호출하면, 프레임워크는 비동기가 발생한지 없어, Null 나오게 된다.

 

MODE_GLOBAL

보안 컨텍스트가 모든 스레드에 공유된다.

 

모든 스레드가 공유하기 때문에 이게 따른 동시성 이슈를 개발자가 직접 해결해야 한다.

 

 

 

DelegatingSecurityContextRunnable 보안 컨텍스트 전달

보안 컨텍스트 저장 전략은 가지로 기본 모드는 MODE_THREADLOCAL 이다

모드는 하나의 요청 마다 새로운 쓰레드를 사용하는 보편적인 서버에서 사용된다.

상황에서 비동기 작업을 하는 경우, 개발자가 직접 쓰레드로 보안 컨텍스트를 전파하는 작업을 해줘야 한다

 

이를 위해 스프링이 제공하는 유틸들이 있다.

DelegatingSecurityContextRunnable 반환 값이 없는 경우 사용

DelegatingSecurityContextCallable 반환 값이 있는 경우 사용한다.

 

 

 

 

비동기 작업을 비동기 메서드를 통해, 쓰레드로 복사한 실행한다.

실행

 

 

 

DelegatingSecurityContextExecutorService

DelegatingSecurityContextCallable, DelegatingSecurityContextRunnable은 동기화 모드에서 비동기 메서드를 사용해야 보안 컨텍스트를 복사해주는 유용한 클래스다.

 

이보다 우아한 방법으로 DelegatingSecurityContextExecutorService는 스레드 풀에서 전파 작업을 해준다.

 

 

 

 

 

스레드 풀에서 새로운 스레드에 보안 컨텍스트를 전파해 준다.

이것도 기존 클래스에 기능을 데코레이터 패턴이 적용된 클래스다.

 

보안 컨텍스트를 전파해주는 스레드 클래스

 

보안 컨텍스트를 전파해주는 클래스

DelegatingSecurityContextCallable, DelegatingSecurityContextRunnable

 

인증 실패 제어

클라이언트가 인증에 실패하거나, 액세스 권한이 없는 리소스에 인증되지 않은 요청을 하는 경우가 있다.

이 경우 클라이언트로부터 자격 증명을 요청하는 데 AuthenticationEntryPoint 구현이 사용된다.

AuthenticationEntryPoint 구현은 로그인 페이지로 리디렉션을 수행하거나, WWW-Authenticate 헤더로 응답하거나, 다른 작업을 수행할 수 있다.

 

일반적으로 ExceptionTranslationFilter에 인증 체계를 시작하기 전에 호출된다.

 

 

 

import java.io.IOException;


import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{


        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
                response.addHeader("message", "Authentication Failed");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }


}

 

import javax.sql.DataSource;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;


import com.springsecurity.authentication.CustomAuthenticationEntryPoint;


@Configuration
public class SecurityConfig {
        
        @Bean
        SecurityFilterChain config(HttpSecurity http) throws Exception {
                http.httpBasic(c -> {
                        c.realmName("ADMIN");
                        c.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
                });
                http.authorizeHttpRequests(request -> {
                        request.anyRequest().authenticated();
                });
                
                return http.build();
        }
        
   
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    UserDetailsService userDetailsService(DataSource dataSource) {
            return new JdbcUserDetailsManager(dataSource);
    }
}

 

 

인증 실패 해보기

 

 

 

 

Form 기반 로그인 인증 구현

일반적인 사용자가 ID/PW 입력해서, 인증하는 방식을 실습해본다.

 

 

위와 같이 설정 서버를 키고 접속하면, 스프링이 기본으로 제공하는 로그인 페이지가 뜬다.

 

 

이전처럼 RestController 아닌 일반 Controller 실습을 하기 위해 간단한 html파일 생성

 

 

 

 

 

 

 

로그인 성공/실패 처리할 핸들러

성공은 AuthenticationSuccessHandler, 실패는 AuthenticationFailureHandler

각각 구현해서 아래에 메서드에 등록해 주면 된다.

 

 

 

form 타입은 FormLoginConfigurer<HttpSecurity>으로 양식 인증 관련 다양한 설정을 있다.

 

 

AuthenticationSuccessHandler, AuthenticationFailureHandler  구현

import java.io.IOException;


import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{


        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                        Authentication authentication) throws IOException, ServletException {
                var auth = authentication.getAuthorities()
                        .stream()
                        .filter(a -> a.getAuthority().equals("read"))
                        .findFirst();
                
                if(auth.isEmpty()) {
                        //권한 없음
                        response.sendRedirect("/error");
                } else {
                        //권한 존재
                        response.sendRedirect("/home");
                }
        }
}

 

import java.io.IOException;
import java.time.LocalDateTime;


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException exception) throws IOException, ServletException {
                
                response.addHeader("failed", LocalDateTime.now().toString());
        }
}

 

import java.io.IOException;
import java.time.LocalDateTime;


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException exception) throws IOException, ServletException {
                
                response.addHeader("failed", LocalDateTime.now().toString());
        }
}

 

import java.io.IOException;


import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{


        public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
                response.addHeader("message", "Authentication Failed");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }


}

 

 

 
 

import javax.sql.DataSource;



import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

org.springframework.security.config.annotation.web.builders.HttpSecurity를 ​​가져옵니다.

org.springframework.security.core.userdetails.UserDetailsService 가져오기;

org.springframework.security.crypto.password.NoOpPasswordEncoder 가져오기;

org.springframework.security.crypto.password.PasswordEncoder 가져오기;

org.springframework.security.provisioning.JdbcUserDetailsManager 가져오기;

org.springframework.security.web.SecurityFilterChain 가져오기;



import com.springsecurity.authentication.CustomAuthenticationEntryPoint;



@구성

공개 클래스 SecurityConfig {



@콩

SecurityFilterChain 구성(HttpSecurity http)에서 예외가 발생합니다.

http.http기본(c -> {

c.realmName("관리자");

c.authenticationEntryPoint(new CustomAuthenticationEntryPoint());

});

http.authorizeHttpRequests(요청 -> {

request.anyRequest().authenticated();

});



http.build()를 반환합니다.

}





@콩

PasswordEncoder 비밀번호Encoder() {

NoOpPasswordEncoder.getInstance()를 반환합니다.

}

@콩

UserDetailsService userDetailsService(DataSource dataSource) {

새로운 JdbcUserDetailsManager(dataSource)를 반환합니다.

}

}

 

+ Recent posts