ABOUT ME

-

  • [Udemy] Spring Security Basic
    수업 내용 정리 2024. 11. 21. 15:08

    * 해당 강의에 대한 정리 글입니다.

    https://www.udemy.com/course/spring-security-6-jwt-oauth2-korean/?couponCode=CPSALEBRAND24

     

    0. 기본 설정

    spring security dependency 추가

    application.properties 설정

    초기 id, pw 변경

    spring.security.user.name=${SECURITY_USERNAME:(id)}

    spring.security.user.password=${SECURITY_PASSWORD:(password)}

    -> SecurityProperties.java 내부에 정의된 로직에 따라 properties에서 자체 비밀번호를 정의할 경우, 프레임워크에서 검증 후 비밀번호를 자동으로 생성하지 않음

     

    1. 서블릿 및 필터

    Servlet

    • 서블릿 컨테이너: Web Server, App Server
    • 모든 커버는 웹 또는 앱 서버 내에 배포된다.
    • 모든 http 요청을 ServletRequest 객체로 변환 -> 내부에 클라이언트 애플리케이션이 보낸 데이터 존재
    • ServletRequest 객체는 해당 서블릿으로 전달되어 SpringBoot나 SpringSecurity와 같은 프레임워크에서 활용
    • 즉, 모든 비즈니스 로직은 서블릿에서 호출됨
    • 클라이언트 요청 -> 백엔드 서버에서 요청 처리 -> 서블릿 컨테이너: 클라이언트 애플리케이션에 응답을 전달하기 전, servletResponse 객체를 http, https 프로토콜로 변환 -> 클라이언트에 응답(ServletResponse 객체)

    Filter

    • 비즈니스 로직 요청과 응답이 실제 서블릿에 도달하기 전에 필터에서 모든 요청과 응답을 intercept
    • 보안 관련 로직 수행
    • e.g. REST API, MVC 경로에 접근 시 요청을 가로채고 사용자 인증 여부를 식별하고 리다이렉션 수행

     

    2. Spring Security Internal Flow

    1. 클라이언트: 애플리케이션을 통해 REST API, MVC 경로에 접근하는 요청 전송
      RequestHeader, RequestBody에 자격 증명을 제공해야 함 -> 존재하지 않는 경우, 로그인 페이지로 리다이렉션
      Spring Security Filters: 전송된 요청을 가로채 자격 증명 검증 -> 예외 유형에 따라 401, 403 오류 발생 가능
    2. 필터 통과 시 Authentication 객체 생성: 사용자 이름, 비밀번호, 인증 여부(Boolean: 초기 false) 등의 필드 존재
      일반적으로 자격 증명은 http 요청 내에 존재하므로, 필터는 http 서블릿 요청 객체에서 자격 증명을 Authentication 객체로 변환
    3. 요청을 Authentication Manager에 전달할 필터 작동: 인증 성공 여부에 관계없이 다른 구성 요소의 도움을 받아 인증 로직을 완료하고 결과를 다시 필터로 전달하는 책임
      실제 인증 조작을 하지 않으며, 인증을 완료하는 책임만 가짐
    4. 실제 인증 조작을 위해 Authentication Provider에 요청 전달: 적용되는 모든 Provider을 통하여 인증 성공 여부 식별(실제 인증 수행)
    5. 인증 객체 형태로 요청을 받았을 때, 해당하는 User의 UserName을 기반으로 UserDetails을 로드하기 위한 클래스
      사용자 세부 정보를 불러와 다시 Authentication Provider에 제공
      이 때 제공한 세부 정보의 비밀번호와 데이터베이스 상의 비밀번호를 비교하지는 않음
    6. Password Encoder: 제공한 세부 정보의 비밀번호와 데이터베이스 상의 비밀번호를 비교하여 처리함 -> 평문으로 저장하거나 해싱 알고리즘을 통하여 처리 가능
    7. 해당 단계들을 거쳐 Authentication Provider은 인증이 성공했다고(Authentication 객체의 isAuthenticated = true 값을 포함) Authentication Manager에게 전달
    8. Authentication Manager은 이를 확인하고 다시 객체를 필터로 전송
    9. 필터는 인증 성공 여부를 알 수 있으며, 성공 여부과 관계없이 인증 세부 정보를 Security Context에 저장
      Authentication 객체는 주어진 브라우저에 대해 생성된 세션 id에 따라 저장되어 동일한 브라우저에서 동일한 페이지에 접근하려고 하면 주어진 세션 id를 기반으로 필터가 Security Context에서 Authentication 객체의 세부 정보를 로드하고 인증 여부 반환
    10. 클라이언트 애플리케이션에 응답 전송하여 REST API, MVC 경로 응답을 받게되며, 실패할 경우 로그인 페이지로 리다이렉션(401, 403 오류 발생)

    UserDetails 세부

     

    3. Spring Security Filters

    logging.level.org.springframework.security=${SPRING_SECURITY_LOG_LEVEL:TRACE}

    : Spring Security 프레임워크의 로그 전체 콘솔 출력 목적(실제로 쓸 때는 조정하자)

     

    • AuthorizationFilter: 인증 필터 관련 로직
    • DefaultLoginPageGenerationgFilter: 사용자에게 AuthorizationFilter에서 예외 발생 시 로그인 페이지로 리다이렉션하는 역할
    • AbstractAuthenticationProcessingFilter: 인증이 필요한지 여부를 결정하고, 인증이 필요하다고 판단되면  -> UsernamePasswordAuthenticationFilter 내부에 비즈니스 로직이 존재하는 attemptAuthentication() 메소드 호출
      Security Context가 Authentication 세부 정보로 채워지는 필터
    • UsernamePasswordAuthenticationFilter: 최종 사용자가 입력한 자격 증명을 추출하여 매개변수 값(username, password)을 기반으로 obtainUsername()에서 사용자 이름 세부정보를 가져오고, 마찬가지로 password 정보를 가져오온 후 UsernamePasswordAuthenticationToken 객체를 생성
      * UsernamePasswordAuthenticationToken -> AbstractAuthenticationToken을 extends -> Authentication을 implements
    • DaoAuthenticationProvider: UserDetailsManager, UserDetailsService를 통해 UserDetails 반환 -> 예외 처리: 비밀번호 검증 - addititionalAuthenticationChecks -> 처리 후 다시 AbstractAuthenticationProcessingFilter으로 이동, Authentication 객체의 isAuthenticated = true 값을 포함

     

    4. SecurityConfig 구성

    SpringBootWebSecurityConfiguration.java

    • defaultSecurityFilterChain(): SecurityFilterChain Bean 생성, 익명의 사용자가 api에 접근하지 못하도록 보안, 폼 기반 로그인 활성화 목적(formLogin), httpBasic 인증 활성화(증명을 Base64 인코딩 및 httpRequest 헤더에 전송하는 방식) 목적
    • => SecurityConfig 구성

    securityConfig.java

    @Configuration
    public class SecurityConfig {
    
        @Bean
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    
            // http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
            // http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll());
            // http.authorizeHttpRequests((requests) -> requests.anyRequest().denyAll());
            http.authorizeHttpRequests((requests) -> requests.requestMatchers(
                "/myAccount", "/myBalance", "/myCards").authenticated());
            // http.formLogin(withDefaults());
            http.formLogin(flc -> flc.disable()); // formLogin 방식 비활성화
            http.httpBasic(withDefaults());
            return http.build();
        }
        
    }
    • requests.anyRequest().permitAll()/denyAll(): 웹 애플리케이션 내의 모든 요청을 보안 없이 허용/모든 요청을 거부(403 error)
    • requestMatchers().authenticated(): requestMatchers 메서드에 api 주소를 제공하고, 해당 주소 세트를 보안함(authenticated() 메서드 호출) -> 다른 설정으로 보안 설정하고 싶은 경우, requestMatchers 메서드를 여러 번 호출 가능
    • formLogin 방식으로 요청에서 자격 증명을 추출하는 방식은 UsernamePasswordAuthenticationFilter에서 처리한다.
    • httpBasic 방식으로 요청하는 경우 BasicAuthenticationFilter의 doFilterInternal() 메서드에서 자격 증명을 추출하고 Authentication 객체를 반환한다. -> 자격 증명은 AUTHORIZATION 헤더 이름으로 requestHeader 내부에 전송되므로 요청의 헤더에서 해당 헤더명을 찾는 방식으로 수행

     

    5. InMemoryUserDetailsManager를 사용한 사용자 정의, 관리

    SpringBoot 애플리케이션의 메모리를 사용하여 여러 사용자 생성 -> 데이터베이스를 사용하여 여러 사용자를 생성하는 방법으로 확장

    • UserDetailsService interface -> loadUserByUsername(): 사용자 이름을 기반으로 사용자 세부 정보 로드(인메모리 혹은 데이터베이스) -> UserDetailsManager(새로운 사용자 생성, 최종 사용자 삭제, 정보 업데이트, 비밀번호 변경 등 유저 관련 작업)에서 extends UserDetailsService

    SecurityConfig.java

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user").password("{noop}12345").authorities("read").build();
        UserDetails admin = User.withUsername("admin").password("{noop}12345").authorities("admin").build();
        
        return new InMemoryUserDetailsManager(user, admin);
    }
    • authorities() 메서드: 특정 사용자가 가질 권한이나 접근 수준 지정 가능 -> 역할명 혹은 api 주소로 주어질 수도 있음


    6. Database를 사용한 사용자 정의, 관리

    1) JdbcUserDetailsManager을 이용한 인증 수행 방식

    • JDBC API, MySQL Driver, Spring Data JPA 의존성 추가, application.properties에 DB 관련 설정 추가
    • SecurityConfig에서 UserDetailsService() 메서드 설정 -> DataSource 객체에서 데이터베이스 연결 정보를 확인, 사용
    • 미리 정의된 스키마, 테이블 구조를 강제하므로 추후 커스텀 빈 생성 필요(2번부터 내용)
    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }
    • pw 해시 시 길이가 길어지므로 충분히 주어야 함, role 칼럼 필요

    2) user 관리를 위한 JPA 엔티티, 레포지토리, 서비스, 컨트롤러 생성

    • 알고 있는 그거 만들기
    • 서비스단 혹은 컨트롤러에서 passwordEncorder 의존성 주입
    @Service
    @RequiredArgsConstructor
    public class MemberService {
    
        private final PasswordEncoder passwordEncoder;
        private final MemberRepository memberRepository;
    
    ...
    
            String hashPassword = passwordEncoder.encode(memberRegisterRequestDto.getPassword());
    
            Member member = Member.builder()
                    .email(memberRegisterRequestDto.getEmail())
                    .password(hashPassword)
                    .build();
    
    ...
        }

     

    SecurityUserDetailsService

    • @Service 어노테이션, UserDetailsService implements 필요
    • MemberRepository 의존성 주입
    @Service
    @RequiredArgsConstructor
    public class SecurityUserDetailService implements UserDetailsService {
    
        private final MemberRepository memberRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Member member = memberRepository.findByEmail(username).orElseThrow(() ->
                new UsernameNotFoundException("User details not found for the user: " + username));
        }
        List<GrantedAuthority> authorities = List.of(new SimpleGraontedAuthority(member.getRole()));
        
        return new User(member.getEmail(), member.getPwd(), authorities);
    }
    • GrantedAuthority: 권한, 역할 관련으로, 역할에 대한 정보를 해당 컬렉션 형태로 변환 -> 구현 클래스: SimpleGrantedAuthority

    SecurityConfig.java 수정

    • JdbcUserDetailsManager 관련 빈 사용 X

     

    7. PasswordEncoder

    Hashing의 단점

    • 입력이 동일하면 항상 동일한 해시 값을 반환하므로 해시 탈취 가능성
    • 속도가 매우 빠른 함수 -> 무차별 대입 공격 수행 가능성

    해결

    • salt 사용: 해싱하기 전 무작위 값 생성한 후 해당 값에 비밀번호를 조합하여 해싱
    • 다회 로그인 시도 시 계정 lock
    • 회원가입 과정에서 복잡한 password 조합 요구
    • 해싱 알고리즘 수행 속도 지연 요구

     

    PasswordEncoder.java

    • BCryptPasswordEncoder 권장
    • encode() 메서드: 해시 값을 반환 값으로 제공 -> 동일한 해시 값을 db에 저장(service에서 passwordEncoder.encode(member.getPwd());으로 구현)
    • matches(): 로그인 작업에서 최종 사용자가 입력한 원시 비밀번호와 db에서 가져온 인코딩된 비밀번호(해싱된 비밀번호)를 요구하며 salt를 추출하고 비밀번호가 일치하는 지 확인(boolean 값 반환) -> 일치 여부를 확인하고 디코딩하지 않는다.
      해당 메서드를 호출하는 주체는 AuthenticationProvider으로, additionalAuthenticationChecks() 메서드에서 호출
    • upgradeEncoding(): 재암호화 시(두 번 해싱) true값 사용(기본 false 반환)
    • PasswordEncoder의 bean을 Spring Security 구성에 하드코딩하지 말아야 함(e.g. BCryptPasswordEncoder의 bean을 생성한다거나) -> DelegatingPasswordEncoder 사용

     

    8. 사용자 정의 AuthenticationProvider

    • authenticate(): 실제 인증 로직을 정의하는 메서드 -> authentication 객체는 userDetails와 isAuthentication boolean 값을 가지며 수신한 인증 세부 정보를 기반으로 userDetails를 로드하고 있는 지 확인하고, authentication이 일치하는 경우 추가 검증 수행 가능 -> 최종적으로 인증 성공 여부를 나타내는 authentication 객체 반환
    • supports(): 어떤 유형의 인증을 지원하는 지 전송 -> ProviderManager는 주어진 인증 스타일에 지원되는 구현 클래스 선택
    • provider 객체의 도움으로 authenticate() 메서드를 호출하고 결과를 얻으며 주어진 Authentication 객체가 여러 AuthenticationProvider에 의해 지원될 때 ProviderManager는 인증이 성공하거나 모든 AuthenticationProvider가 성공적으로 실행될 때까지 모든 AuthenticationProvider를 확인
    @Component
    @RequiredArgsConstructor
    public class UsernamePwdAuthenticationProvider implements AuthenticationProvider {
    
        private final SecurityUserDetailService userDetailsService;
        private final PasswordEncoder passwordEncoder;
        
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 주로 단일 로직으로 구현하며, 해당 메소드 내부에서 userDetails를 직접 로드할 수도 있지만
            // 자체 사용자 세부 정보 서비스 구현을 통해 userDetails 로드
            String username = authentication.getName();
            String pwd = authentication.getCredentials.toString();
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 사용자가 입력한 비밀번호와 저장 시스템에서 로드된 비밀번호 일치 여부 비교
            if (passwordEncoder.matches(pwd, userDetails.getPassword())) {
                // 추가 검사 로직 수행 가능
                return new UsernamePasswordAuthenticationToken(username, pwd, userDetails.getAuthorities());
            } else {
                throw new BadCredentialsException("Invalid Password");
            }
        }
        
        @Override
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
        }
    
    }

     

    '수업 내용 정리' 카테고리의 다른 글

    [Udemy] Spring Security 사용자 정의, JWT  (0) 2024.11.23
    [Udemy] Spring Security 예외 처리, CORs, CSRF  (0) 2024.11.22
    Argo cd  (3) 2024.10.17
    [Udemy] Twitter4j & Kafka  (0) 2024.09.23
    docker 설정  (0) 2024.08.26