[Spring]그림으로 배우는 스프링 6 - 14장 Spring Security(2) -SSR

Gaeng·2024년 11월 28일

[Spring] 공부

목록 보기
16/21
post-thumbnail

Spring Security

Security Filter를 체인 설정

  • 스프링시큐리티 필터는 JavaConfig 클래스에서 할 수 있음.
  • @Configuration / @EnableWebSecurity- 생략가능

1. @Configuration

  • 역할:
    • 이 애노테이션은 클래스를 스프링의 설정 파일로 표시합니다.
    • 보통 자바 기반 설정을 사용하여 빈(Bean)을 정의할 때 사용됩니다.
    • 스프링 시큐리티에서는 보안 설정 클래스를 정의할 때 사용됩니다.
  • 생략 가능 여부:
    • 최신 스프링 부트 프로젝트에서는 Spring Boot가 자동 구성(Autoconfiguration)을 지원하기 때문에 특정 설정이 명시적으로 필요하지 않다면 생략 가능합니다.
    • 하지만 보안 관련 세부 사항을 직접 정의해야 할 경우 명시적으로 선언하는 것이 좋습니다.

2. @EnableWebSecurity

  • 역할:
    • 스프링 시큐리티의 웹 보안 기능을 활성화합니다.
    • 이 애노테이션이 붙은 클래스는 스프링 시큐리티의 설정 클래스임을 나타냅니다.
    • 내부적으로 @Configuration과 관련 설정을 포함합니다.
  • 생략 가능 여부:
    • 최신 스프링 부트에서는 spring-boot-starter-security를 의존성으로 포함하면 자동으로 보안 설정이 활성화됩니다.
    • 기본적인 보안 구성을 사용하는 경우 생략 가능합니다.
    • 하지만, 커스터마이징된 보안 구성을 추가해야 한다면 명시적으로 선언하고 추가 설정을 구현해야 합니다.
그림: 기본적이 Security 기본 로그인 화면
@Configuration
@EnableWebSecurity
public class SecurityConfig {


    }

커스터마이징을 하는 이유

실제 프로젝트에서는 기본 보안 설정으로는 복잡한 보안 요구 사항을 충족하기 어려우므로, 다음과 같은 세부 항목들을 독자적으로 설정해야 합니다. 이를 위해 @Bean 메서드를 사용하여 스프링 필터 체인 객체(SecurityFilterChain)를 생성하고 커스터마이징합니다.

주요 보안 설정 항목

  1. 요청 인가 (Authorization)
    • 각 요청에 대한 접근 권한을 설정합니다.
    • 예: URL 패턴별 접근 권한, 특정 역할에 따른 허용/차단.
  2. 로그인 화면 (Login Page)
    • 기본 로그인 화면 대신 사용자 정의 로그인 화면을 제공합니다.
    • 커스터마이징된 로그인 페이지 URL을 설정하여 사용자 경험 개선.
  3. 인가 실패 화면 (Access Denied Page)
    • 접근 권한이 없는 사용자가 특정 리소스에 접근하려 할 때 표시할 화면 설정.
    • 사용자가 적절한 안내를 받을 수 있도록 오류 페이지를 제공.
  4. 인증 시 사용하는 데이터 저장 위치 (Authentication Data Store)
    • 사용자 인증 정보를 저장하고 확인하는 위치를 설정합니다.
    • 일반적으로 메모리(In-Memory), 데이터베이스(Database), 또는 외부 인증 시스템(예: LDAP, OAuth2)을 사용.

요청인가 (Authorization)

요청 인가는 스프링 시큐리티에서 사용자가 특정 요청에 접근할 권한이 있는지 확인하는 보안 설정으로, 애플리케이션의 보안을 강화하는 핵심 기능입니다. SecurityFilterChain을 통해 역할(Role)과 권한(Authority)에 따른 세부 정책을 설정하며, 명확하고 일관된 정책 순서를 통해 의도치 않은 접근 허용이나 차단을 방지하고 안전한 보안 체계를 구축할 수 있습니다.


요청 인가의 기본 개념

  1. 요청 인가란?

    • 사용자가 요청한 URL이나 리소스에 대해 접근 가능한지 확인하고, 설정된 정책에 따라 허용 또는 차단하는 보안 기능.
    • 인증(Authentication)이 사용자의 신원을 확인하는 과정이라면, 인가(Authorization)는 사용자의 권한(Role 또는 Authority)을 확인하는 과정.
  2. 요청 인가와 관련된 요소

    • 역할(Role): 사용자의 역할을 정의 (예: ADMIN, USER). hasRole로 접근 제어.
    • 권한(Authority): 세부 작업 권한을 정의 (예: READ, WRITE). hasAuthority로 접근 제어.
    • 익명 사용자: 로그인하지 않은 사용자.
    • 인증된 사용자: 로그인된 사용자.
  3. 요청 인가의 처리 흐름

    • 요청마다 정의된 조건에 따라 인가를 검사하며, 조건은 상위에서 하위로 평가됩니다.

요청 인가 설정 방법

요청 인가는 SecurityFilterChain의 Bean 정의에서 설정합니다. 스프링 시큐리티는 URL 패턴과 HTTP 메서드를 기준으로 요청별로 권한을 정의할 수 있습니다.

예시 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(requests -> requests
            .requestMatchers(HttpMethod.POST, "/admin/**").hasRole("ADMIN") // ADMIN 역할만 POST 요청 허용
            .requestMatchers(HttpMethod.GET, "/user/**").hasAnyRole("USER", "ADMIN") // USER 또는 ADMIN 역할 허용
            .requestMatchers("/public/**").permitAll() // 누구나 접근 가능
            .requestMatchers("/private/**").authenticated() // 인증된 사용자만 접근 가능
            .anyRequest().denyAll() // 나머지 모든 요청은 차단
        )
        .formLogin(); // 기본 로그인 설정
    return http.build();
}

주요 접근 제어 메서드

메서드명설명예시 사용법
hasRole지정한 역할(Role)을 가진 사용자만 접근 가능.hasRole("ADMIN")
hasAnyRole지정한 역할(Role) 중 하나를 가진 사용자만 접근 가능.hasAnyRole("ADMIN", "USER")
hasAuthority지정한 권한(Authority)을 가진 사용자만 접근 가능.hasAuthority("READ")
hasAnyAuthority지정한 권한(Authority) 중 하나를 가진 사용자만 접근 가능.hasAnyAuthority("READ", "WRITE")
permitAll모든 사용자에게 접근 허용.permitAll()
denyAll모든 사용자에게 접근 차단.denyAll()
isAuthenticated인증된 사용자(로그인한 사용자)만 접근 가능.isAuthenticated()
isAnonymous익명 사용자(로그인하지 않은 사용자)만 접근 가능.isAnonymous()

요청 인가 설정의 순서

요청 인가는 가장 제한적인 정책부터 가장 포괄적인 정책으로 작성해야 합니다. 순서가 잘못되면 상위 조건이 하위 조건을 무시하거나, 의도치 않은 접근이 허용될 수 있습니다.

순서메서드설명예시 코드
1denyAll특정 경로에 대해 무조건 접근 차단..requestMatchers("/admin/secret/**").denyAll()
2permitAll누구나 접근 가능한 경로 설정 (로그인, 회원가입, 에러 페이지 등)..requestMatchers("/login", "/register").permitAll()
3isAnonymous익명 사용자(로그인하지 않은 사용자)만 접근 가능..requestMatchers("/guest/**").isAnonymous()
4isAuthenticated인증된 사용자(로그인한 사용자)만 접근 가능..requestMatchers("/user/**").isAuthenticated()
5hasRole / hasAnyRole특정 역할(Role)을 가진 사용자만 접근 가능. 여러 역할 중 하나를 허용하려면 hasAnyRole 사용..requestMatchers("/manager/**").hasRole("MANAGER")
6hasAuthority / hasAnyAuthority세부 권한(Authority)을 기준으로 접근 권한 설정..requestMatchers("/edit/**").hasAuthority("EDIT")
7anyRequest나머지 모든 요청에 대한 기본 정책 설정..anyRequest().authenticated()

로그인 화면

스프링 시큐리티에서는 기본적으로 제공되는 로그인 화면을 사용하거나, 애플리케이션 요구사항에 따라 커스텀 로그인 페이지를 설정할 수 있습니다. 로그인 화면은 사용자가 애플리케이션에 인증(로그인)할 수 있도록 하는 진입점입니다.

기본 로그인 화면

  • 스프링 시큐리티는 초기 설정 시 기본 로그인 화면을 제공합니다.
  • 기본 로그인 화면은 개발 중에 빠르게 인증 기능을 테스트할 때 유용하지만, 실제 애플리케이션에서는 사용자 경험(UX)을 고려해 커스터마이징이 필요합니다.

기본 로그인 화면 활성화

http
    .formLogin(); // 기본 로그인 화면 사용

커스텀 로그인 화면

애플리케이션 요구사항에 맞는 로그인 화면을 설정하려면, 로그인 페이지를 직접 정의하고 스프링 시큐리티에 해당 페이지를 등록해야 합니다.

커스텀 로그인 화면 설정

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(requests -> requests
            .anyRequest().authenticated() // 모든 요청 인증 필요
        )
        .formLogin(login -> login
            .loginPage("/login") // 커스텀 로그인 페이지 경로
            .defaultSuccessUrl("/home") // 로그인 성공 시 이동할 경로
            .failureUrl("/login?error") // 로그인 실패 시 이동할 경로
            .permitAll() // 로그인 페이지는 누구나 접근 가능
        )
        .exceptionHandling(handling -> handling
         	.accessDeniedPage("/display-access-denied") // 인가 실패 시 표시할 페이지 경로
        );
    return http.build();
}

설명

  • loginPage("/login"): 커스텀 로그인 페이지 경로를 설정합니다.
  • defaultSuccessUrl("/home"): 로그인 성공 후 이동할 기본 페이지를 설정합니다.
  • failureUrl("/login?error"): 로그인 실패 시 이동할 페이지를 설정합니다.
  • permitAll(): 로그인 페이지는 인증되지 않은 사용자도 접근할 수 있도록 설정합니다.
  • 인가 실패 처리 : 인가 실패란 사용자의 권한이 없는 리소스에 접근하려고 할때 발생

로그인 화면의 구성

  1. HTML 템플릿 예시
   <!DOCTYPE html>
   <html>
   <head>
       <title>Login</title>
   </head>
   <body>
       <h2>Login</h2>
       <form action="/login" method="post">
           <label for="username">Username:</label>
           <input type="text" id="username" name="username" required><br>
           <label for="password">Password:</label>
           <input type="password" id="password" name="password" required><br>
           <button type="submit">Login</button>
       </form>
       <p th:if="${param.error}">Invalid username or password</p>
       <p th:if="${param.logout}">You have been logged out</p>
   </body>
   </html>
  1. 설명
    • action="/login": 스프링 시큐리티가 제공하는 로그인 처리 엔드포인트.
    • name="username"name="password": 스프링 시큐리티에서 기본적으로 요구하는 입력 필드 이름.
    • param.error: 로그인 실패 시 오류 메시지를 표시.
    • param.logout: 로그아웃 성공 메시지를 표시.

인증용 데이터 가져오기 (UserDetails)

스프링 시큐리티에서 UserDetails는 사용자 인증과 관련된 정보를 캡슐화한 인터페이스로, 사용자 ID, 비밀번호, 권한 등의 정보를 제공하며, 인증 메커니즘의 핵심 요소로 사용됩니다.

그림 출처 : 그림으로 배우는 스프링6

UserDetails의 역할

  1. 사용자 정보 관리:
    • 사용자 이름(username), 비밀번호(password), 권한(roles) 등 인증에 필요한 정보를 제공.
  2. 스프링 시큐리티와 연동:
    • UserDetailsService를 통해 데이터베이스나 메모리에서 사용자 정보를 로드하여 인증 과정에서 활용.
  3. 인증/인가 과정에서 사용:
    • AuthenticationManager가 인증을 처리할 때 UserDetails를 사용하여 사용자의 인증 여부를 확인.
    • 인증 성공 시 SecurityContextHolder에 사용자 정보를 저장하여 요청 처리 중 참조 가능.

UserDetails 인터페이스 구조

UserDetails는 인증과 인가에 필요한 사용자 정보를 정의한 인터페이스입니다.

public interface UserDetails {
    String getUsername();                     // 사용자 이름
    String getPassword();                     // 비밀번호
    Collection<? extends GrantedAuthority> getAuthorities(); // 권한 목록
    boolean isAccountNonExpired();            // 계정 만료 여부
    boolean isAccountNonLocked();             // 계정 잠김 여부
    boolean isCredentialsNonExpired();        // 비밀번호 만료 여부
    boolean isEnabled();                      // 계정 활성화 여부
}

UserDetailsService 인터페이스

UserDetailsService는 사용자 정보를 로드하기 위해 사용하는 스프링 시큐리티의 핵심 인터페이스입니다.

정의

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

역할

  • 사용자 정보 로드: 사용자 이름(username)을 기반으로 사용자 정보를 조회.
  • UserDetails 반환: 조회된 사용자 정보를 UserDetails로 반환하여 인증 과정에 사용.

주요 구현체

스프링 시큐리티는 다양한 데이터 소스에서 사용자 정보를 가져오기 위해 몇 가지 기본 구현체를 제공합니다.

1. InMemoryUserDetailsManager

  • 기능:

    • 메모리에 사용자 정보를 저장.
    • 테스트나 간단한 애플리케이션에 적합.
  • 코드 예제:

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
    
        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();
    
        return new InMemoryUserDetailsManager(user, admin);
    }

2. JdbcUserDetailsManager

  • 기능:

    • 데이터베이스에서 사용자 정보를 조회.
    • 스프링 시큐리티의 기본 스키마를 사용하거나, 필요에 따라 커스텀 구현 가능.
    • 데이터베이스 구조가 기본 스키마와 다르거나, 외부 API(LDAP, OAuth)와 통합이 필요한 경우 커스텀 구현체 작성.
  • 코드 예제:

    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 데이터베이스 또는 외부 API 호출
            UserEntity userEntity = userRepository.findByUsername(username)
                    .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    
            return new CustomUserDetails(
                userEntity.getUsername(),
                userEntity.getPassword(),
                userEntity.isEnabled(),
                mapRolesToAuthorities(userEntity.getRoles())
            );
        }
    
        private Collection<? extends GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
            return roles.stream()
                    .map(role -> new SimpleGrantedAuthority(role.getName()))
                    .collect(Collectors.toList());
        }
    }
    클라이언트 요청부터 UserDetails 흐름도

인증한 사용자 정보를 화면에 표시

  • Thymeleaf에서는 authentication 객체의 데이터를 sec:authentication을 통해 표시 가능
    스프링 시큐리티와 Thymeleaf를 함께 사용하면 인증된 사용자의 정보를 화면에 쉽게 표시할 수 있습니다. sec:authentication 속성을 활용하여 사용자 이름, 권한 등 인증 정보를 출력할 수 있습니다.

예시 코드

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <title>Authenticated User Info</title>
</head>
<body>
    <h1>Welcome, <span sec:authentication="name"></span>!</h1>
    <p>Your roles: <span sec:authentication="principal.authorities"></span></p>
    <p>Your username (from Principal): <span sec:authentication="principal.username"></span></p>

    <!-- 로그아웃 버튼 -->
    <form th:action="@{/logout}" method="post">
        <button type="submit">Logout</button>
    </form>
</body>
</html>

주요 sec:authentication 속성 설명

속성설명
sec:authentication="name"인증된 사용자의 이름을 출력 (기본적으로 UserDetails.getUsername() 값).
sec:authentication="principal.username"인증된 사용자의 이름을 출력 (Principal 객체에서 직접 가져옴).
sec:authentication="principal.authorities"인증된 사용자의 역할/권한 목록 출력.
sec:authentication="credentials"인증 요청 시 사용된 자격 증명 (비밀번호와 같은 정보). 보안상 화면에 표시하지 않는 것이 좋음.

3. 사용 예시 화면

사용자 이름과 권한 표시:

  • 로그인된 사용자가 "admin"이고, 역할이 ROLE_ADMIN이라면:
Welcome, admin!
Your roles: [ROLE_ADMIN]

사용자 정보가 필요한 다른 부분에서 활용**

1) 메뉴 표시

로그인 상태에 따라 메뉴를 표시하거나 숨기기:

<ul>
    <li><a th:href="@{/home}">Home</a></li>
    <li sec:authorize="hasRole('ADMIN')"><a th:href="@{/admin}">Admin Page</a></li>
    <li sec:authorize="isAuthenticated()"><a th:href="@{/profile}">Profile</a></li>
    <li sec:authorize="isAnonymous()"><a th:href="@{/login}">Login</a></li>
</ul>

2) 사용자 이름 표시

헤더나 푸터에 로그인된 사용자의 이름 표시:

<div>
    Logged in as: <span sec:authentication="name"></span>
</div>

로그아웃 기능

스프링 시큐리티에서 로그아웃 기능을 Thymeleaf로 간단히 구현:

<form th:action="@{/logout}" method="post">
    <button type="submit">Logout</button>
</form>

메서드 인가

스프링 시큐리티에서 메서드 인가서비스 계층 또는 비즈니스 로직 수준에서 메서드 호출 권한을 검사하는 기능입니다. 이는 컨트롤러나 서비스 클래스의 메서드에 특정 역할(Role) 또는 권한(Authority)을 가진 사용자만 접근할 수 있도록 제한합니다.

메서드 인가의 특징

  1. 세밀한 접근 제어:
    • URL 패턴이 아닌 특정 메서드에 대해 접근 권한을 부여.
  2. 주석 기반 선언:
    • Spring AOP와 스프링 시큐리티를 활용해 메서드 호출 전후에 인가 로직을 추가.
  3. 적용 위치:
    • 주로 서비스 계층(Service Layer)에서 비즈니스 로직을 보호하기 위해 사용.

메서드 인가 활성화

설정 활성화

스프링 시큐리티에서 메서드 인가를 사용하려면 @EnableGlobalMethodSecurity를 활성화해야 합니다.

  @Configuration
  @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
  public class SecurityConfig extends WebSecurityConfigurerAdapter {
      // Security Configuration
  }

메서드 인가의 작동 흐름

  1. 사용자 요청:

    • 사용자가 컨트롤러 또는 서비스 계층의 메서드를 호출.
  2. 메서드 호출 전 인가 검사:

    • @PreAuthorize 또는 @Secured 등의 애노테이션에 따라 스프링 시큐리티가 인증 및 권한을 검사.
  3. 인가 조건 만족 여부 확인:

    • 조건 만족: 메서드 실행.
    • 조건 불만족: AccessDeniedException 발생.
  4. 반환 시 검사 (옵션):

    • @PostAuthorize가 설정된 경우 반환 값을 기반으로 추가 권한 검사를 수행.

서비스 계층에서 메서드 인가 사용

@Service
public class ProductService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteProduct(Long productId) {
        // 관리자만 상품 삭제 가능
        productRepository.deleteById(productId);
    }

    @PreAuthorize("#username == authentication.name")
    public void updateUserProfile(String username, UserProfile userProfile) {
        // 현재 로그인한 사용자만 자신의 프로필 수정 가능
        userRepository.updateProfile(username, userProfile);
    }

    @Secured({"ROLE_USER", "ROLE_ADMIN"})
    public List<Product> getAllProducts() {
        // USER와 ADMIN 역할을 가진 사용자만 접근 가능
        return productRepository.findAll();
    }
}
profile
문제를 해결하면서 나온 문제를 기록하는 노트

0개의 댓글