Security 의 흐름을 알아보자

현정재·2024년 7월 13일
1

1. 인증 요청

사용자가 애플리케이션에 로그인 요청을 보냅니다. 이 요청은 AuthenticationFilter로 전달됩니다. 일반적으로 UsernamePasswordAuthenticationFilter가 사용됩니다. 이 필터는 로그인 폼에서 입력된 사용자명과 비밀번호를 처리합니다.

2. UsernamePasswordAuthenticationToken 생성

AuthenticationFilter는 사용자의 로그인 요청을 받아 UsernamePasswordAuthenticationToken 객체를 생성합니다. 이 객체는 사용자가 입력한 사용자명과 비밀번호를 포함합니다.

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

3. AuthenticationManager로 전달

AuthenticationFilter는 생성된 UsernamePasswordAuthenticationToken 객체를 AuthenticationManager에게 전달합니다. AuthenticationManager는 인증 요청을 처리하고, 인증 성공 여부를 결정합니다.

return this.getAuthenticationManager().authenticate(authRequest);

4. AuthenticationProvider로 전달

AuthenticationManager는 여러 개의 AuthenticationProvider를 관리합니다. 각각의 AuthenticationProvider는 특정 인증 방식을 처리합니다. 여기서 UsernamePasswordAuthenticationToken 객체를 처리할 수 있는 AuthenticationProvider로 전달합니다.

Authentication authResult = provider.authenticate(authentication);

5. 비밀번호 인코딩 (암호화)

AuthenticationProvider는 사용자가 입력한 비밀번호를 인코딩(암호화)합니다. 이 과정에서 PasswordEncoder가 사용됩니다. 스프링 시큐리티에서는 주로 BCryptPasswordEncoder를 사용하여 비밀번호를 암호화합니다.

if (passwordEncoder.matches(rawPassword, userDetails.getPassword())) {
    // 비밀번호가 일치하면
}

6. UserDetailsService 호출

AuthenticationProviderUserDetailsService를 호출하여 데이터베이스에서 사용자 정보를 조회합니다. UserDetailsService는 주로 사용자명(아이디)으로 사용자를 검색하여 사용자 정보를 반환합니다.

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

7. UserDetails 반환

UserDetailsService는 데이터베이스에서 사용자를 조회한 후, 사용자 정보를 담은 UserDetails 객체를 반환합니다. 이 객체는 스프링 시큐리티가 요구하는 사용자 정보(아이디, 비밀번호, 권한 등)를 포함합니다.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username);
    if (user == null) {
        throw new UsernameNotFoundException("User not found");
    }
    return new PrincipalDetails(user);
}

8. 비밀번호 검증

AuthenticationProviderUserDetails 객체에서 반환된 사용자 정보와 사용자가 입력한 비밀번호를 비교합니다. 이 과정에서 비밀번호는 인코딩된 상태로 비교됩니다. BCryptPasswordEncoder와 같은 인코더는 matches 메서드를 통해 원문 비밀번호와 인코딩된 비밀번호를 비교할 수 있습니다.

if (passwordEncoder.matches(rawPassword, userDetails.getPassword())) {
    // 비밀번호가 일치하면 인증 성공
}

9. 인증 성공

비밀번호 검증이 성공하면, AuthenticationProvider는 인증이 성공한 Authentication 객체를 생성하여 AuthenticationManager로 반환합니다. 이 객체는 사용자의 인증 정보를 담고 있습니다.

Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
return auth;

10. AuthenticationManager로부터 인증 객체 반환

AuthenticationManager는 인증이 성공한 Authentication 객체를 AuthenticationFilter로 반환합니다.

Authentication authResult = this.getAuthenticationManager().authenticate(authRequest);

11. SecurityContext에 인증 객체 저장

AuthenticationFilter는 인증이 성공한 Authentication 객체를 SecurityContext에 저장합니다. SecurityContext는 현재 인증된 사용자 정보를 담고 있는 컨텍스트입니다. 이를 통해 애플리케이션의 다른 부분에서 현재 사용자의 인증 정보를 참조할 수 있습니다.

SecurityContextHolder.getContext().setAuthentication(authResult);

상세 설명

AuthenticationFilter

  • 역할: 로그인 요청을 가로채고, 인증 과정을 시작합니다. 사용자가 로그인 폼에서 제출한 사용자명과 비밀번호를 캡처합니다.
  • 주요 메서드:
    • attemptAuthentication(HttpServletRequest request, HttpServletResponse response): 로그인 시도 메서드로, 사용자명과 비밀번호를 기반으로 Authentication 객체를 생성합니다.
      • 내부 동작: request.getParameter("username")request.getParameter("password")를 호출하여 사용자 입력을 읽어옵니다. 그 후, UsernamePasswordAuthenticationToken 객체를 생성하여 AuthenticationManager에게 전달합니다.
      • 예제:
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
            // 요청에서 사용자명과 비밀번호를 가져옵니다.
            String username = obtainUsername(request);
            String password = obtainPassword(request);
            // 사용자명과 비밀번호를 사용하여 Authentication 객체를 생성합니다.
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            // AuthenticationManager에게 인증 요청을 전달합니다.
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    • successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult): 인증 성공 시 호출됩니다. 인증 정보를 SecurityContext에 저장하고, 성공 후 처리 작업을 수행합니다.
      • 내부 동작: SecurityContextHolder.getContext().setAuthentication(authResult)를 호출하여 인증 정보를 저장합니다.
      • 예제:
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
            // 인증 성공 정보를 SecurityContext에 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authResult);
            // 다음 필터로 요청을 전달합니다.
            chain.doFilter(request, response);
        }
    • unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed): 인증 실패 시 호출됩니다. 실패 원인을 처리하고, 실패 후 작업을 수행합니다.
      • 내부 동작: 실패 원인에 대한 로그를 남기고, 실패 응답을 클라이언트에 반환합니다.
      • 예제:
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
            // 인증 실패 시 SecurityContext를 지웁니다.
            SecurityContextHolder.clearContext();
            // 실패 상태 코드를 설정합니다.
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // 실패 원인을 응답에 작성합니다.
            response.getWriter().write("Authentication failed: " + failed.getMessage());
        }

UsernamePasswordAuthenticationToken

  • 역할: 사용자가 입력한 사용자명과 비밀번호를 포함하는 객체입니다. 인증 요청 시 사용되며, 인증 성공 시 사용자 정보와 권한을 포함합니다.

  • 생성자:

    • UsernamePasswordAuthenticationToken(Object principal, Object credentials): 인증 요청 시 사용됩니다. principal은 사용자명, credentials는 비밀번호를 의미합니다.
    • UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities): 인증 성공 후 사용됩니다. principal은 사용자 정보, credentials는 비밀번호, authorities는 사용자 권한을 의미합니다.
  • 예제:

    // 인증 요청 시 사용되는 생성자 예제
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    
    // 인증 성공 시 사용되는 생성자 예제
    UsernamePasswordAuthenticationToken authSuccess = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());

AuthenticationManager

  • 역할: 여러 AuthenticationProvider를 관리하며, 각각의 AuthenticationProvider는 특정 인증 방식을 처리합니다. 인증 요청을 적절한 AuthenticationProvider에게 위임합니다.
  • 주요 메서드:
    • authenticate(Authentication authentication): 전달된 Authentication 객체를 인증합니다. 적절한 AuthenticationProvider를 찾아 인증을 위임하고, 인증 결과를 반환합니다.
      • 내부 동작: 모든 등록된 AuthenticationProvider를 순회하면서, supports 메서드를 사용하여 해당 Authentication 타입을 지원하는지 확인한 후, authenticate 메서드를 호출합니다.
      • 예제:
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 등록된 모든 AuthenticationProvider를 순회합니다.
            for (AuthenticationProvider provider : providers) {
                // 해당 AuthenticationProvider가 전달된 Authentication 타입을 지원하는지 확인합니다.
                if (provider.supports(authentication.getClass())) {
                    // 지원하는 경우, 해당 AuthenticationProvider에게 인증을 위임하고 결과를 반환합니다.
                    return provider.authenticate(authentication);
                }
            }
            // 지원하는 AuthenticationProvider를 찾지 못한 경우 예외를 발생시킵니다.
            throw new ProviderNotFoundException("No suitable AuthenticationProvider found");
        }

AuthenticationProvider

  • 역할: 실제 인증 로직을 담당하며, 비밀번호 검증과 사용자 정보 조회를 수행합니다. AuthenticationManager가 인증 요청을 위임하는 대상입니다.
  • 주요 메서드:
    • authenticate(Authentication authentication): 전달된 Authentication 객체를 검증합니다. 비밀번호를 검증하고, 사용자 정보를 조회하여 인증 결과를 반환합니다.
      • 내부 동작: UserDetailsService를 사용하여 사용자 정보를 조회하고, PasswordEncoder를 사용하여 비밀번호를 검증합니다. 성공하면 UsernamePasswordAuthenticationToken을 반환합니다.
      • 예제:
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 인증 요청에서 사용자명과 비밀번호를 가져옵니다.
            String username = authentication.getName();
            String password = (String) authentication.getCredentials();
            // UserDetailsService를 사용하여 사용자 정보를 조회합니다.
            UserDetails user = userDetailsService.loadUserByUsername(username);
            // 비밀번호를 검증합니다.
            if (passwordEncoder.matches(password, user.getPassword())) {
                // 비밀번호가 일치하면 인증 성공 객체를 생성하여 반환합니다.
                return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
            } else {
                // 비밀번호가 일치하지 않으면 예외를 발생시킵니다.
                throw new BadCredentialsException("Invalid credentials");
            }
        }
    • supports(Class<?> authentication): 특정 Authentication 객체를 이 AuthenticationProvider가 지원하는지 여부를 결정합니다.
      • 내부 동작: 전달된 클래스 타입이 UsernamePasswordAuthenticationToken인지 확인합니다.
      • 예제:
        @Override
        public boolean supports(Class<?> authentication) {
            // 전달된 클래스 타입이 UsernamePasswordAuthenticationToken인지 확인합니다.
            return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
        }

PasswordEncoder

  • 역할: 비밀번호를 안전하게 저장하기 위해 암호화합니다. 원문 비밀번호를 해시하여 데이터베이스에 저장하거나, 인증 시 입력된 비밀번호를 검증하는 데 사용됩니다.
  • 주요 메서드:
    • encode(CharSequence rawPassword): 원문 비밀번호를 암호화합니다.
      • 예제:
        // 원문 비밀번호를 암호화합니다.
        String encodedPassword = passwordEncoder.encode(rawPassword);
    • matches(CharSequence rawPassword, String encodedPassword): 원문 비밀번호와 암호화된 비밀번호가 일치하는지 확인합니다.
      • 예제:
        // 원문 비밀번호와 암호화된 비밀번호가 일치하는지 확인합니다.
        boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);

UserDetailsService

  • 역할: 데이터베이스에서 사용자 정보를 조회합니다. 사용자명으로 사용자를 검색하여 UserDetails 객체를 반환합니다.
  • 주요 메서드:
    • loadUserByUsername(String username): 사용자명을 기반으로 사용자 정보를 조회하고, UserDetails 객체를 반환합니다. 사용자가 존재하지 않으면 UsernameNotFoundException을 던집니다.
      • 내부 동작: UserRepository 등을 사용하여 데이터베이스에서 사용자 정보를 조회합니다.
      • 예제:
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 사용자명을 사용하여 데이터베이스에서 사용자를 조회합니다.
            User user = userRepository.findByUsername(username);
            // 사용자가 존재하지 않으면 예외를 던집니다.
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
            // 조회된 사용자 정보를 기반으로 UserDetails 객체를 생성하여 반환합니다.
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getAuthorities());
        }

UserDetails

  • 역할: 사용자 정보를 담고 있는 객체입니다. 스프링 시큐리티가 사용자 정보를 처리하는 데 필요한 다양한 메서드를 제공합니다.
  • 주요 메서드:
    • getAuthorities(): 사용자의 권한 목록을 반환합니다.
      • 예제:
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // 사용자의 권한 목록을 반환합니다.
            return authorities;
        }
    • getPassword(): 사용자의 비밀번호를 반환합니다.
      • 예제:
        @Override
        public String getPassword() {
            // 사용자의 비밀번호를 반환합니다.
            return password;
        }
    • getUsername(): 사용자의 사용자명을 반환합니다.
      • 예제:
        @Override
        public String getUsername() {
            // 사용자의 사용자명을 반환합니다.
            return username;
        }
    • isAccountNonExpired(): 사용자의 계정이 만료되지 않았는지 여부를 반환합니다.
      • 예제:
        @Override
        public boolean isAccountNonExpired() {
            // 사용자의 계정이 만료되지 않았는지 여부를 반환합니다.
            return true;
        }
    • isAccountNonLocked(): 사용자의 계정이 잠기지 않았는지 여부를 반환합니다.
      • 예제:
        @Override
        public boolean isAccountNonLocked() {
            // 사용자의 계정이 잠기지 않았는지 여부를 반환합니다.
            return true;
        }
    • isCredentialsNonExpired(): 사용자의 자격 증명이 만료되지 않았는지 여부를 반환합니다.
      • 예제:
        @Override
        public boolean isCredentialsNonExpired() {
            // 사용자의 자격 증명이 만료되지 않았는지 여부를 반환합니다.
            return true;
        }
    • isEnabled(): 사용자의 계정이 활성화되었는지 여부를 반환합니다.
      • 예제:
        @Override
        public boolean isEnabled() {
            // 사용자의 계정이 활성화되었는지 여부를 반환합니다.
            return true;
        }

SecurityContext

  • 역할: 현재 인증된 사용자 정보를 담고 있는 컨텍스트입니다. 애플리케이션의 다른 부분에서 현재 사용자의 인증 정보를 참조할 수 있도록 합니다.
  • 주요 클래스:
    • SecurityContextHolder: SecurityContext 객체를 저장하고 조회할 수 있는 정적 메서드를 제공합니다. 현재 스레드의 SecurityContext를 관리합니다.
      • 내부 동작: SecurityContext 객체를 ThreadLocal에 저장하여 스레드별로 인증 정보를 분리합니다.
      • 예제:
        // 새로운 SecurityContext 객체를 생성합니다.
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        // 인증 객체를 SecurityContext에 설정합니다.
        context.setAuthentication(authentication);
        // SecurityContextHolder에 SecurityContext를 설정합니다.
        SecurityContextHolder.setContext(context);
      ## Authentication
  • 역할: 인증된 사용자 정보를 담고 있는 객체입니다. PrincipalAuthorities를 포함합니다.
  • 주요 메서드:
    • getAuthorities(): 인증된 사용자의 권한 목록을 반환합니다.
      • 예제:
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // 인증된 사용자의 권한 목록을 반환합니다.
            return authorities;
        }
    • getCredentials(): 인증에 사용된 자격 증명을 반환합니다.
      • 예제:
        @Override
        public Object getCredentials() {
            // 인증에 사용된 자격 증명을 반환합니다.
            return credentials;
        }
    • getDetails(): 추가적인 인증 세부 정보를 반환합니다.
      • 예제:
        @Override
        public Object getDetails() {
            // 추가적인 인증 세부 정보를 반환합니다.
            return details;
        }
    • getPrincipal(): 인증된 사용자 정보를 반환합니다.
      • 예제:
        @Override
        public Object getPrincipal() {
            // 인증된 사용자 정보를 반환합니다.
            return principal;
        }
    • isAuthenticated(): 사용자가 인증되었는지 여부를 반환합니다.
      • 예제:
        @Override
        public boolean isAuthenticated() {
            // 사용자가 인증되었는지 여부를 반환합니다.
            return authenticated;
        }
    • setAuthenticated(boolean isAuthenticated): 인증 상태를 설정합니다.
      • 예제:
        @Override
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            // 인증 상태를 설정합니다.
            this.authenticated = isAuthenticated;
        }

이 과정을 통해 Spring Security는 사용자의 인증을 안전하게 처리하며, 인증된 사용자만 보호된 리소스에 접근할 수 있도록 보장합니다. 이를 통해 애플리케이션의 보안 수준을 크게 향상시킬 수 있습니다.

전체 흐름 요약

  1. 사용자가 로그인 요청을 보냅니다.
  2. AuthenticationFilter가 요청을 가로채고, UsernamePasswordAuthenticationToken 객체를 생성합니다.
  3. AuthenticationManager에게 UsernamePasswordAuthenticationToken 객체를 전달합니다.
  4. AuthenticationManager는 적절한 AuthenticationProvider를 찾아 인증을 위임합니다.
  5. AuthenticationProvider는 비밀번호를 인코딩하고, UserDetailsService를 통해 사용자 정보를 조회합니다.
  6. 비밀번호를 검증한 후, 인증이 성공하면 UsernamePasswordAuthenticationToken 객체를 생성하여 반환합니다.
  7. AuthenticationManager는 인증된 Authentication 객체를 AuthenticationFilter로 반환합니다.
  8. AuthenticationFilter는 인증 정보를 SecurityContext에 저장합니다.
profile
wonttock

0개의 댓글