1. 개요

이 빠른 사용방법(예제)에서는 Spring Security 애플리케이션에서 여러 진입점을 정의 하는 방법을 살펴보겠습니다 .

이것은 주로 XML 구성 파일에서 여러 http 블록을 정의하거나 SecurityFilterChain 빈을 여러 번 생성하여 여러 HttpSecurity 인스턴스를 정의하는 것을 수반합니다.

2. 메이븐 의존성

개발을 위해서는 다음 의존성이 필요합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
    <version>2.7.2</version> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.7.2</version>
</dependency>    
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.4.0</version>
</dependency>

최신 버전의 spring-boot-starter-security , spring-boot-starter-webspring-boot-starter-thymeleaf , spring-boot-starter-test , spring-security-test 는 Maven Central에서 다운로드할 수 있습니다.

3. 여러 진입점

3.1. 여러 HTTP 요소가 있는 여러 진입점

사용자 소스를 보유할 기본 구성 클래스를 정의해 보겠습니다.

@Configuration
@EnableWebSecurity
public class MultipleEntryPointsSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User
          .withUsername("user")
          .password(encoder().encode("userPass"))
          .roles("USER").build());
        manager.createUser(User
          .withUsername("admin")
          .password(encoder().encode("adminPass"))
          .roles("ADMIN").build());
        return manager;
    }
    
    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
}

이제 Security 구성에서 여러 진입점을 정의하는 방법을 살펴보겠습니다.

여기서는 기본 인증에 의해 구동되는 예제를 사용할 것이며 Spring Security가 구성에서 여러 HTTP 요소의 정의를 지원 한다는 사실을 잘 활용할 것입니다 .

Java 구성을 사용할 때 여러 Security 영역을 정의하는 방법은 각각 자체 Security 구성 이 있는 여러 @Configuration 클래스를 갖는 것입니다. 이러한 클래스는 정적일 수 있으며 기본 구성 내에 배치할 수 있습니다.

하나의 애플리케이션에 여러 진입점을 갖는 주요 동기는 애플리케이션의 다른 부분에 액세스할 수 있는 다양한 유형의 사용자가 있는 경우입니다.

각기 다른 권한과 인증 모드를 가진 세 개의 진입점으로 구성을 정의해 보겠습니다.

  • HTTP 기본 인증을 사용하는 관리 사용자용
  • 양식 인증을 사용하는 일반 사용자용
  • 인증이 필요하지 않은 게스트 사용자용

관리 사용자에 대해 정의된 진입점은 /admin/** 형식의 URL을 보호하여 ADMIN 역할을 가진 사용자만 허용 하고 authenticationEntryPoint() 메서드 를 사용하여 설정된 BasicAuthenticationEntryPoint 유형의 진입점이 있는 HTTP 기본 인증이 필요합니다.

@Configuration
@Order(1)
public static class App1ConfigurationAdapter {

    @Bean
    public SecurityFilterChain filterChainApp1(HttpSecurity http) throws Exception {
        http.antMatcher("/admin/**")
            .authorizeRequests().anyRequest().hasRole("ADMIN")
            .and().httpBasic().authenticationEntryPoint(authenticationEntryPoint());
        return http.build();
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        BasicAuthenticationEntryPoint entryPoint = 
          new BasicAuthenticationEntryPoint();
        entryPoint.setRealmName("admin realm");
        return entryPoint;
    }
}

각 정적 클래스 의 @Order 어노테이션은 요청된 URL과 일치하는 구성을 찾기 위해 구성이 고려되는 순서를 나타냅니다. 각 클래스 의 주문 값은 고유해야 합니다.

BasicAuthenticationEntryPoint 유형의 Bean에는 realName 특성 이 설정되어야 합니다.

3.2. 여러 진입점, 동일한 HTTP 요소

다음 으로 양식 인증을 사용하여 USER 역할을 가진 일반 사용자가 액세스할 수 있는 /user/** 형식의 URL에 대한 구성을 정의해 보겠습니다 .

@Configuration
@Order(2)
public static class App2ConfigurationAdapter {

    public SecurityFilterChain filterChainApp2(HttpSecurity http) throws Exception {
        http.antMatcher("/user/**")
            .authorizeRequests().anyRequest().hasRole("USER")
            .and()
            // formLogin configuration
            .and()
            .exceptionHandling()
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPointWithWarning(),
              new AntPathRequestMatcher("/user/private/**"))
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPoint(), 
              new AntPathRequestMatcher("/user/general/**"));
        return http.build();
    }
}

보시다시피, authenticationEntryPoint() 메서드 외에 진입점을 정의하는 또 다른 방법은 defaultAuthenticationEntryPointFor() 메서드를 사용하는 것입니다. 이는 RequestMatcher 개체를 기반으로 다양한 조건과 일치하는 여러 진입점을 정의할 수 있습니다.

RequestMatcher 인터페이스에는 일치하는 경로, 미디어 유형 또는 정규 표현식과 같은 다양한 유형의 조건을 기반으로 하는 구현이 있습니다 . 이 예제에서는 AntPathRequestMatch를 사용하여 /user/private/**/user/general/** 형식의 URL에 대해 두 개의 서로 다른 진입점을 설정 했습니다.

다음으로 동일한 정적 구성 클래스에서 진입점 빈을 정의해야 합니다.

@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
    return new LoginUrlAuthenticationEntryPoint("/userLogin");
}
        
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
    return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}

여기서 주요 요점은 이러한 여러 진입점을 설정하는 방법입니다. 반드시 각 항목의 구현 세부 사항은 아닙니다.

이 경우 진입점은 모두 LoginUrlAuthenticationEntryPoint 유형 이고 다른 로그인 페이지 URL을 사용 합니다. 단순 로그인 페이지의 경우 /userLogin 및 로그인 페이지의 경우 / userLoginWithWarning 입니다 .

이 구성에서는 /userLogin/userLoginWithWarning MVC 매핑과 표준 로그인 양식이 있는 두 페이지를 정의해야 합니다.

양식 인증의 경우 로그인 처리 URL과 같이 구성에 필요한 모든 URL도 /user/** 형식을 따르거나 액세스할 수 있도록 구성되어야 한다는 점을 기억하는 것이 매우 중요합니다.

적절한 역할이 없는 사용자가 보호된 URL에 액세스하려고 하면 위의 두 구성 모두 /403 URL 로 리디렉션됩니다 .

서로 다른 정적 클래스에 있더라도 Bean에 대해 고유한 이름을 사용하도록 주의하십시오 . 그렇지 않으면 하나가 다른 하나를 무시합니다.

3.3. 새로운 HTTP 요소, 진입점 없음

마지막으로 인증되지 않은 사용자를 포함하여 모든 유형의 사용자를 허용하는 /guest/** 형식의 URL에 대한 세 번째 구성을 정의해 보겠습니다.

@Configuration
@Order(3)
public static class App3ConfigurationAdapter {

    public SecurityFilterChain filterChainApp3(HttpSecurity http) throws Exception {
        http.antMatcher("/guest/**").authorizeRequests().anyRequest().permitAll();
        return http.build();
    }
}

3.4. XML 구성

이전 섹션에서 세 개의 HttpSecurity 인스턴스에 해당하는 XML 구성을 살펴보겠습니다 .

예상대로 여기에는 세 개의 개별 XML <http> 블록이 포함됩니다.

/admin/** URL 의 경우 XML 구성은 http-basic 요소 의 entry-point-ref 속성을 사용합니다.

<security:http pattern="/admin/**" use-expressions="true" auto-config="true">
    <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
    <security:http-basic entry-point-ref="authenticationEntryPoint" />
</security:http>

<bean id="authenticationEntryPoint"
  class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
     <property name="realmName" value="admin realm" />
</bean>

여기서 주의할 점은 XML 구성을 사용하는 경우 역할이 ROLE_<ROLE_NAME> 형식이어야 한다는 것입니다 .

/user/** URL 에 대한 구성 은 defaultAuthenticationEntryPointFor() 메서드 와 직접적으로 동일한 것이 없기 때문에 xml에서 두 개의 http 블록으로 나누어야 합니다.

URL /user/general/**에 대한 구성은 다음과 같습니다.

<security:http pattern="/user/general/**" use-expressions="true" auto-config="true"
  entry-point-ref="loginUrlAuthenticationEntryPoint">
    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
    //form-login configuration      
</security:http>

<bean id="loginUrlAuthenticationEntryPoint"
  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
  <constructor-arg name="loginFormUrl" value="/userLogin" />
</bean>

/user/private/** URL 에 대해 유사한 구성을 정의할 수 있습니다.

<security:http pattern="/user/private/**" use-expressions="true" auto-config="true"
  entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
    //form-login configuration
</security:http>

<bean id="loginUrlAuthenticationEntryPointWithWarning"
  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg name="loginFormUrl" value="/userLoginWithWarning" />
</bean>

/guest/** URL의 경우 http 요소 가 있습니다.

<security:http pattern="/**" use-expressions="true" auto-config="true">
    <security:intercept-url pattern="/guest/**" access="permitAll()"/>  
</security:http>

여기서 또한 중요한 것은 적어도 하나의 XML <http> 블록이 /** 패턴과 일치해야 한다는 것입니다.

4. 보호된 URL 액세스

4.1. MVC 구성

확보한 URL 패턴과 일치하는 요청 매핑을 생성해 보겠습니다.

@Controller
public class PagesController {

    @GetMapping("/admin/myAdminPage")
    public String getAdminPage() {
        return "multipleHttpElems/myAdminPage";
    }

    @GetMapping("/user/general/myUserPage")
    public String getUserPage() {
        return "multipleHttpElems/myUserPage";
    }

    @GetMapping("/user/private/myPrivateUserPage")
    public String getPrivateUserPage() {
        return "multipleHttpElems/myPrivateUserPage"; 
    }

    @GetMapping("/guest/myGuestPage")
    public String getGuestPage() {
        return "multipleHttpElems/myGuestPage";
    }

    @GetMapping("/multipleHttpLinks")
    public String getMultipleHttpLinksPage() {
        return "multipleHttpElems/multipleHttpLinks";
    }
}

/multipleHttpLinks 매핑 은 보호된 URL에 대한 링크가 있는 간단한 HTML 페이지를 반환합니다.

<a th:href="@{/admin/myAdminPage}">Admin page</a>
<a th:href="@{/user/general/myUserPage}">User page</a>
<a th:href="@{/user/private/myPrivateUserPage}">Private user page</a>
<a th:href="@{/guest/myGuestPage}">Guest page</a>

보호된 URL에 해당하는 각 HTML 페이지에는 간단한 텍스트와 백링크가 있습니다.

Welcome admin!

<a th:href="@{/multipleHttpLinks}" >Back to links</a>

4.2. 애플리케이션 초기화

예제를 Spring Boot 애플리케이션으로 실행할 것이므로 기본 메서드로 클래스를 정의해 보겠습니다.

@SpringBootApplication
public class MultipleEntryPointsApplication {
    public static void main(String[] args) {
        SpringApplication.run(MultipleEntryPointsApplication.class, args);
    }
}

XML 구성을 사용하려면 기본 클래스에 @ImportResource({“classpath*:spring-security-multiple-entry.xml”}) 어노테이션도 추가해야 합니다.

4.3. Security 구성 테스트

보호된 URL을 테스트하는 데 사용할 수 있는 JUnit 테스트 클래스를 설정해 보겠습니다.

@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
public class MultipleEntryPointsTest {
 
    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
          .addFilter(springSecurityFilterChain).build();
    }
}

다음으로 관리 사용자 를 사용하여 URL을 테스트해 보겠습니다 .

HTTP 기본 인증 없이 /admin/adminPage URL을 요청할 때 Unauthorized 상태 코드를 수신해야 하며 인증을 추가한 후 상태 코드는 200 OK여야 합니다.

admin 사용자로 /user/userPage URL 에 액세스하려고 하면 상태 302 Forbidden을 수신해야 합니다.

@Test
public void whenTestAdminCredentials_thenOk() throws Exception {
    mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized());

    mockMvc.perform(get("/admin/myAdminPage")
      .with(httpBasic("admin", "adminPass"))).andExpect(status().isOk());

    mockMvc.perform(get("/user/myUserPage")
      .with(user("admin").password("adminPass").roles("ADMIN")))
      .andExpect(status().isForbidden());
}

일반 사용자 자격 증명을 사용하여 URL에 액세스하는 유사한 테스트를 만들어 보겠습니다.

@Test
public void whenTestUserCredentials_thenOk() throws Exception {
    mockMvc.perform(get("/user/general/myUserPage")).andExpect(status().isFound());

    mockMvc.perform(get("/user/general/myUserPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isOk());

    mockMvc.perform(get("/admin/myAdminPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isForbidden());
}

두 번째 테스트에서는 Spring Security가 로그인 양식으로 리디렉션하므로 양식 인증이 누락되면 Unauthorized 대신 302 Found 상태가 되는 것을 볼 수 있습니다.

마지막으로 /guest/guestPage URL 에 액세스하는 테스트를 만들어 봅시다. 세 가지 유형의 인증을 모두 수행하고 200 OK 상태를 수신하는지 확인합니다.

@Test
public void givenAnyUser_whenGetGuestPage_thenOk() throws Exception {
    mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk());

    mockMvc.perform(get("/guest/myGuestPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isOk());

    mockMvc.perform(get("/guest/myGuestPage")
      .with(httpBasic("admin", "adminPass")))
      .andExpect(status().isOk());
}

5. 결론

이 예제에서는 Spring Security를 ​​사용할 때 여러 진입점을 구성하는 방법을 시연했습니다.

예제의 전체 소스 코드는 GitHub 에서 찾을 수 있습니다 . 애플리케이션을 실행하려면 pom.xml 에서 MultipleEntryPointsApplication 시작 클래스 태그 의 어노테이션을 제거하고 mvn spring-boot:run 명령을 실행한 다음 /multipleHttpLinks URL 에 액세스합니다 .

HTTP 기본 인증을 사용할 때는 로그아웃이 불가능하므로 이 인증을 제거하려면 브라우저를 닫았다가 다시 열어야 합니다.

JUnit 테스트를 실행하려면 다음 명령과 함께 정의된 Maven 프로파일 entryPoints 를 사용하십시오.

mvn 새로 설치 -PentryPoints

Security footer banner