개념 정리

스프링 시큐리티 정리

조요피 2023. 8. 21. 14:46

스프링 시큐리티

스프링 공식 웹사이트에서는 스프링 시큐리티를 인증과 접근 제어를 위해 세부적인 맞춤 구성이 가능한 프레임워크라고 소개하고 있다.

스프링 부트(스프링 시큐리티)는 미리 준비된 구성을 제공하므로 모든 구성을 작성하는 대신 자신의 구현과 일치하지 않는 구성만 재정의하면 된다.
이 접근법을 CoC(Convention Over Configuration)라고 한다.

스프링 아키텍처

  1. 인증 필터가 요청을 가로챈다
  2. 인증 책임이 인증 관리자에 위임된다
  3. 인증 관리자는 인증 논리를 구현하는 인증 공급자를 이용한다
  4. 인증 공급자는 사용자 세부 정보 서비스로 사용자를 찾고 암호 인코더로 암호를 검증한다
  5. 인증 결과가 필터에 반환된다
  6. 인증된 엔티티에 관한 세부 정보가 보안 컨텍스트에 저장된다

기본 구성

아래 두 가지 인터페이스는 Basic 인증에서 반드시 필요하다.

UserDetailsService 기본 구현을 대체할 때는 반드시 PasswordEncoder 구현도 지정해야한다.

UserDetailsService

  • 해당 인터페이스를 구현하는 객체가 사용자에 관한 세부 정보를 관리한다.

PasswordEncoder

  • 암호를 인코딩한다.
  • 암호가 기존 인코딩과 일치하는지 확인한다.

기본 구성 재정의 예제)


@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService = new InMemoryUserDetailsManager();

        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();

        userDetailsService.createUser(user);

        return userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        http.authorizeRequests().anyRequest().authenticated();
    }
}

AuthenticationProvider

AuthenticationProvider는 AuthenticationManager에서 요청을 받은 후 사용자를 찾는 작업을 UserDetailsService에, 암호를 검증하는 작업을
PasswordEncoder에 위임한다.

  • 인증 논리 정의
  • 사용자와 암호 관리 위임
  • UserDetailsService, PasswordEncoder 기본 구현 사용

AuthenticationProvider 구현 재정의

다음 예제에서는 UserDetailsSservice와 PasswordEncoder를 사용하지 않도록 재정의 했다.


@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        // 일반적으로 UserDetailsService 및 PasswordEncoder를 호출해서 사용자 이름과 암호를 테스트한다.
        if ("john".equals(username) && "12345".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

사용자 관리

사용자 관리를 위해서는 UserDetailsService 및 UserDetailsManager 인터페이스를 이용한다.

UserDetailsService는 사용자 이름으로 사용자를 검색하는 역할만 한다.

UserDetailsManager는 대부분의 어플리케이션에 필요한 사용자 추가, 수정, 삭제 작업을 추가한다.

사용자를 인증하는 기능만 필요한 경우 UserDetailsService 인터페이스만 구현하면 된다.

사용자를 관리하려면 UserDetailsService와 UserDetailsManager를 구현해야 한다.

  • UserDetails는 하나 이상의 권한(GrantedAuthority)을 가진다
  • UserDetailsService는 UserDetails 인터페이스를 이용한다
  • UserDetialsManager는 UserDetailsService 인터페이스를 확장한다

UserDetails

스프링 시큐리티에서 사용자 정의는 UserDetails 인터페이스를 준수해야 한다.

UserDetails 인터페이스는 스프링 시큐리티가 이해하는 방식으로 사용자를 나타낸다.

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired(); //계정 만료

    boolean isAccountNonLocked(); //계정 잠금

    boolean isCredentialsNonExpired(); //자격 증명 만료

    boolean isEnabled(); //계정 비활성화
}

사용자는 다음과 같은 작업을 할 수 있다.

  • 계정 만료
  • 계정 잠금
  • 자격 증명 만료
  • 계정 비활성화

애플리케이션에서 이러한 기능을 구현할 필요가 없다면 단순하게 네 메서드가 true를 반환하게 하면 된다.

GrantedAuthority

사용자에게 허가된 작업을 권한(Authority)이라고 한다.

스프링 시큐리티에서는 GrantedAuthority 인터페이스로 권한을 나타낸다.

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

UserDetailsService

AuthenticationProvider는 인증 논리에서 UserDetailsService를 이용해 사용자 세부 정보를 로드하는 구성 요소이며 사용자 이름으로 사용자를 찾기 위해 loadUserByUsername
메서드를 호출한다.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
public class InMemoryUserDetailsService implements UserDetailsService {

    private final List<UserDetails> users;

    public InMemoryUserDetailsService(List<UserDetails> users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.stream()
                .filter(u -> u.getUsername().equals(username))
                .findFirst()
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}

@Configuration
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails u = new User("john", "12345", "read");
        List<UserDetails> users = List.of(u);
        return new InMemoryUserDetailsService(users);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

UserDetailsManager

이 인터페이스는 UserDetailsService 인터페이스를 확장하고 메서드를 추가한다.

스프링 시큐리티가 인증을 수행하려면 UserDetailsService 인터페이스가 필요한데, 일반적으로 애플리케이션에는 사용자를 관리하는 기능이 필요하고 대부분의 앱은 최소한 새 사용자를 추가하거나 기존 사용자를
삭제할 수 있어야 한다.

이때, UserDetailsManager 인터페이스를 구현한다.

public interface UserDetailsManager extends UserDetailsService {
    void createUser(UserDetails var1);

    void updateUser(UserDetails var1);

    void deleteUser(String var1);

    void changePassword(String var1, String var2);

    boolean userExists(String var1);
}

JdbcUserDetailsManager는 스프링 시큐리티에서 사용자 데이터를 저장하고 관리하기 위해 사용되는 클래스 중 하나입니다. 이 클래스는 JDBC(Java Database Connectivity)를 통해
사용자 정보를 데이터베이스에서 읽고 쓸 수 있습니다. 기본적으로 JdbcUserDetailsManager는 다음과 같은 테이블 이름을 기대합니다:

  1. users 테이블: 사용자 계정 정보를 저장하는 테이블입니다. 일반적으로 다음과 같은 컬럼을 포함합니다.

    • username: 사용자 이름
    • password: 암호화된 사용자 비밀번호
    • enabled: 계정 활성화 여부
    • accountNonExpired: 계정 만료 여부
    • credentialsNonExpired: 비밀번호 만료 여부
    • accountNonLocked: 계정 잠금 여부
  2. authorities 테이블: 사용자 권한 정보를 저장하는 테이블입니다. 일반적으로 다음과 같은 컬럼을 포함합니다.

    • username: 사용자 이름 (users 테이블과 조인하여 사용자와 권한을 연결)
    • authority: 사용자의 권한 (예: ROLE_USER, ROLE_ADMIN 등)

JdbcUserDetailsManager는 이러한 테이블 이름과 컬럼명을 기본값으로 가정하며, 설정을 통해 변경할 수 있습니다. 사용자 데이터베이스 스키마가 위의 기본값과 다를
경우, JdbcUserDetailsManager의 설정을 통해 테이블 이름과 컬럼명을 지정할 수 있습니다. 이렇게 하면 사용자 및 권한 데이터를 적절하게 읽고 쓸 수 있습니다.

예를 들어, 다음과 같이 JdbcUserDetailsManager를 설정하여 기본값 외의 테이블 및 컬럼 이름을 사용할 수 있습니다:


@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {
        String usersByUsernameQuery = "select username, password, enabled from spring.users where username = ?";
        String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
        var userDetailsManager = new JdbcUserDetailsManager(dataSource);
        userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
        userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
        return userDetailsManager;

    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

인증 구현

인증 논리를 담당하는 것은 AuthenticationProvider 계층이며 여기에서 요청을 허용할지 결정한다.

AuthenticationManager 는 HTTP 필터 계층에서 요청을 수신하고 이 책임을 AuthenticationProvider에 위임하는 구성 요소다.

AuthenticationProvider

일반적으로 프레임워크는 가장 많이 이용되는 구현을 지원하지만 가능한 모든 시나리오를 해결할 수는 없다.

스프링 시큐리티에서는 AuthenticationProvider 인터페이스를 구현하여 모든 맞춤형 인증 논리를 정의할 수 있다.

Authentication 인터페이스

인증 프로세스의 필수 인터페이스다.

인증 요청 이벤트를 나타내며 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담는다.

애플리케이션에 접근을 요청하는 사용자를 주체(Principal)라고 한다.

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // 인증 요청에 허거된 권한의 컬렉션 반환

    Object getCredentials(); // 인증 프로세스에 이용된 암호나 비밀 반환

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated(); //인증 프로세스 종료 유무. 끝났으면 true

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

맞춤형 인증 논리 구현

AuthenticationProvider 인터페이스의 기본 구현은 시스템의 사용자를 찾는 책임을 UserDetailsService에 위임하고 PasswordEncoder로 인증 프로세스에서 암호를 관리한다.

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    boolean supports(Class<?> var1);
}

인증 요청을 허용하거나 거부하기 위해 인증 관리자와 인증 공급자가 함께 작업하는 방식은 출입문에 더 복잡한 잠금 장치를 설치하는 것과 비슷하다. 잠금을 풀려면 카드를 이용하거나 전통적인 열쇠를 이용하면 된다. 잠금
장치는 문을 열지 결정하는 인증 관리자다. 인증 관리자는 이 결정을 내리기 위해 카드를 검증하는 인증 공급자와 물리적인 열쇠를 검증하는 인증 공급자에 작업을 위임한다.

AuthenticationProvider에서 사용 가능한 주요 인증 객체 타입과 그 역할에 대한 설명

  1. UsernamePasswordAuthenticationToken:

    • 역할: 사용자 이름(username)과 비밀번호(password)를 사용한 인증에 사용됩니다. 가장 일반적인 형태의 인증입니다.
    • 예: 사용자가 웹 어플리케이션 로그인 페이지에서 제출한 사용자 이름과 비밀번호를 이용한 로그인.
  2. JwtAuthenticationToken:

    • 역할: JSON Web Token (JWT) 기반의 인증에 사용됩니다. 클라이언트 측에서 발급된 JWT를 사용하여 사용자를 인증하는 데 사용됩니다.
    • 예: JWT를 사용한 웹 API 인증.
  3. OAuth2AuthenticationToken:

    • 역할: OAuth 2.0 기반의 인증에 사용됩니다. OAuth 2.0 토큰을 사용하여 외부 서비스 또는 소셜 미디어 서비스와 통합하는 데 사용됩니다.
    • 예: Facebook 또는 Google을 사용한 로그인.
  4. PreAuthenticatedAuthenticationToken:

    • 역할: 이 인증은 이미 인증된 사용자를 나타내며, 주로 SSO (Single Sign-On) 시나리오에서 사용됩니다.
    • 예: 다른 인증 메커니즘(예: SAML)을 통해 사용자를 인증한 후에 Spring Security에 전달.
  5. RememberMeAuthenticationToken:

    • 역할: "Remember Me" 기능을 지원하기 위해 사용됩니다. 사용자가 세션 만료 후에도 인증을 유지할 수 있도록 합니다.
    • 예: "Remember Me" 기능을 사용한 웹 로그인.
  6. AnonymousAuthenticationToken:

    • 역할: 인증되지 않은 익명 사용자를 나타냅니다. 주로 보안을 위한 기본값으로 사용됩니다.
    • 예: 웹 애플리케이션에 로그인하지 않은 방문자.
  7. Custom AuthenticationTokens:

    • 역할: 프로젝트 또는 사용 사례에 따라 사용자 지정 인증 토큰을 정의하고 사용할 수 있습니다. 예를 들어, 특별한 인증 방법을 사용하는 경우에 이러한 토큰을 만들어 사용할 수 있습니다.

예제)


@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 각각의 인증 방법에 따라 다른 로직을 수행
        // 예를 들어, 사용자 이름과 비밀번호로 인증하는 방법과
        // JWT 기반 인증 방법을 구분하여 처리
        if (authentication instanceof UsernamePasswordAuthenticationToken) {
            // 사용자 이름과 비밀번호로 인증하는 로직
        } else if (authentication instanceof JwtAuthenticationToken) {
            // JWT 기반 인증 로직
        } else {
            // 다른 인증 방법 처리
            throw new UnsupportedAuthenticationMethodException("Unsupported authentication method");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 이 AuthenticationProvider가 지원하는 모든 인증 객체 타입을 확인
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)
                || JwtAuthenticationToken.class.isAssignableFrom(authentication)
                || CustomAuthenticationToken.class.isAssignableFrom(authentication); // 사용자 정의 토큰 추가
    }
}

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @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("Something went wrong!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
}

SecurityContext 이용

AuthenticationManager는 인증 프로세스를 성공적으로 완료한 후 요청이 유지되는 동안 Authentication 인스턴스를 저장한다. Authentication 객체를 저장하는 인스턴스를 보안
컨텍스트라고 한다.

  1. 사용자의 요청을 인증 필터가 가로챈다.
  2. 인증 필터가 인증 책임을 인증 관리자에 위임한다.
  3. 요청이 인증되면 세부 정보가 보안 컨텍스트에 저장된다.
  4. 인증 필터가 컨트롤러에 위임한다.
  5. 컨트롤러는 보안 컨텍스드에 있는 세부 정보를 이용할 수 있다.

예제)


@RestController
public class MyController {

    @GetMapping("/current-user")
    public String getCurrentUser() {
        // 현재 사용자의 SecurityContext를 가져옵니다.
        SecurityContext securityContext = SecurityContextHolder.getContext();

        // 현재 사용자의 인증된 Principal (사용자 정보)를 가져옵니다.
        String username = securityContext.getAuthentication().getName();

        return "현재 사용자: " + username;
    }
}

@RestController
public class MyController {

    @GetMapping("/current-user")
    public String getCurrentUser(Authentication authentication) {
        String username = authentication.getName();
        return "현재 사용자: " + username;
    }
}

'개념 정리' 카테고리의 다른 글

웹과 인터넷 그리고 네트워크  (0) 2023.10.05
OAuth 2란 무엇인가?  (0) 2023.09.26
TDD란 무엇인가?  (0) 2023.04.13
애자일(Agile) 개발 방법론  (0) 2023.03.23
GraphQL 개념 정리  (0) 2022.12.12