1. 개요

간단히 말해서, Spring Security는 메소드 수준에서 권한 부여 의미를 지원합니다.

일반적으로 예를 들어 특정 메서드를 실행할 수 있는 역할을 제한하여 서비스 계층을 보호하고 전용 메서드 수준 Security 테스트 지원을 사용하여 테스트할 수 있습니다.

이 사용방법(예제)에서는 일부 Security 어노테이션의 사용을 검토할 것입니다. 그런 다음 다양한 전략으로 메서드 Security을 테스트하는 데 집중할 것입니다.

2. 메소드 Security 활성화

먼저 Spring Method Security를 ​​사용하려면 spring-security-config 의존성 을 추가해야 합니다 .

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>

Maven Central 에서 최신 버전을 찾을 수 있습니다 .

Spring Boot를 사용하려면 spring-security-config 를 포함 하는 spring-boot-starter-security 의존성을 사용할 수 있습니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

다시 말하지만 최신 버전은 Maven Central 에서 찾을 수 있습니다 .

다음으로 전역 메서드 Security을 활성화해야 합니다 .

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class MethodSecurityConfig 
  extends GlobalMethodSecurityConfiguration {
}
  • prePostEnabled의 속성은 Spring Security 사전 / 사후 어노테이션을 수 있습니다.
  • securedEnabled 경우 생성 속성이 결정 @Secured 어노테이션이 사용하도록 설정해야합니다.
  • jsr250Enabled의 속성은 우리가 사용할 수 있습니다 @RoleAllowed 어노테이션을.

다음 섹션에서 이러한 어노테이션에 대해 자세히 살펴보겠습니다.

3. 메소드 Security 적용

3.1. @Security 어노테이션 사용

@Secured 어노테이션은하는 방법에 대한 역할 List을 지정하는 데 사용됩니다. 따라서 사용자는 지정된 역할 중 하나 이상이 있는 경우에만 해당 방법에 액세스할 수 있습니다.

getUsername 메서드를 정의해 보겠습니다 .

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

여기서 @Secured("ROLE_VIEWER") 어노테이션은 ROLE_VIEWER 역할을 가진 사용자만 getUsername 메서드 를 실행할 수 있음을 정의합니다 .

게다가 @Secured 어노테이션 에서 역할 List을 정의할 수 있습니다 .

@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
    return userRoleRepository.isValidUsername(username);
}

이 경우 구성에는 사용자에게 ROLE_VIEWER 또는 ROLE_EDITOR가 있는 경우 해당 사용자가 isValidUsername 메서드를 호출할 수 있다고 나와 있습니다 .

@Secured 어노테이션은 스프링 표현 언어 (SpEL을)를 지원하지 않습니다.

3.2. @RoleAllowed 어노테이션 사용

@RoleAllowed 어노테이션은의 JSR-250의 해당 어노테이션입니다 @Secured 어노테이션.

기본적으로 @Secured 와 유사한 방식으로 @RoleAllowed 어노테이션을 사용할 수 있습니다 .

이런 식으로 getUsernameisValidUsername 메서드를 재정의할 수 있습니다.

@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
    //...
}
    
@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
    //...
}

마찬가지로 ROLE_VIEWER 역할을 가진 사용자만 getUsername2 를 실행할 수 있습니다 .

다시 말하지만 사용자는 ROLE_VIEWER 또는 ROLER_EDITOR 역할 중 하나 이상이 있는 경우에만 isValidUsername2 를 호출할 수 있습니다.

3.3. 사용 @PreAuthorize@PostAuthorize 어노테이션

@PreAuthorize@PostAuthorize 어노테이션 표현 기반 액세스 제어를 제공합니다. 따라서 SpEL(Spring Expression Language)을 사용하여 술어를 작성할 수 있습니다 .

@PreAuthorize의 특수 검사 방법에 들어가기 전에 주어진 식 반면 @PostAuthorize의 것이 본 방법의 실행 후의 결과를 변경할 수 어노테이션 검증한다.

이제 아래와 같이 getUsernameInUpperCase 메서드를 선언해 보겠습니다 .

@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
    return getUsername().toUpperCase();
}

@PreAuthorize ( "hasRole ( 'ROLE_VIEWER은')") 와 같은 의미가 @Secured ( "ROLE_VIEWER") 우리는 이전 섹션에서 사용. 이전 기사에서 더 많은 Security 표현식 세부사항을 자유롭게 발견 하십시오 .

결과적으로 @Secured({"ROLE_VIEWER","ROLE_EDITOR"}) 어노테이션은 @PreAuthorize("hasRole('ROLE_VIEWER') 또는 hasRole('ROLE_EDITOR')") 로 대체될 수 있습니다 .

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
    //...
}

게다가, 우리는 실제로 표현식의 일부로 메소드 인수를 사용할 수 있습니다 :

@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
    //...
}

여기서 사용자는 username 인수의 값이 현재 Security 주체의 사용자 이름과 동일한 경우에만 getMyRoles 메소드를 호출할 수 있습니다 .

그 지적이의 가치 @PreAuthorize의 표현으로 대체 될 수 @PostAuthorize의 사람.

getMyRoles를 다시 작성해 보겠습니다 .

@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
    //...
}

그러나 이전 예에서는 대상 메서드 실행 후 권한 부여가 지연됩니다.

또한, @PostAuthorize의 어노테이션은 방법 결과에 액세스 할 수있는 기능을 제공합니다 :

@PostAuthorize
  ("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

여기서 loadUserDetail 메서드는 반환된 CustomUser사용자 이름현재 인증 주체의 별명 과 같은 경우에만 성공적으로 실행됩니다 .

이 섹션에서는 주로 간단한 Spring 표현식을 사용합니다. 더 복잡한 시나리오의 경우 사용자 지정 Security 표현식을 만들 수 있습니다 .

3.4. 사용 @PreFilter을 하고 @PostFilter 어노테이션

Spring Security는 메소드를 실행하기 전에 컬렉션 인수를 필터링하기 위해 @PreFilter 어노테이션을 제공합니다 .

@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
    return usernames.stream().collect(Collectors.joining(";"));
}

이 예에서는 인증된 사용자 이름을 제외한 모든 사용자 이름을 조인합니다.

여기 에서 표현식에서 filterObject 라는 이름을 사용 하여 컬렉션의 현재 개체를 나타냅니다.

그러나 메서드에 컬렉션 유형인 인수가 두 개 이상 있는 경우 filterTarget 속성을 사용하여 필터링할 인수를 지정해야 합니다.

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List<String> usernames, List<String> roles) {
 
    return usernames.stream().collect(Collectors.joining(";")) 
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

또한 @PostFilter 어노테이션 을 사용하여 반환된 메서드 컬렉션을 필터링할 수도 있습니다 .

@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
    return userRoleRepository.getAllUsernames();
}

이 경우 filterObject 라는 이름 은 반환된 컬렉션의 현재 개체를 나타냅니다.

해당 구성으로 Spring Security는 반환된 List을 반복하고 Security 주체의 사용자 이름과 일치하는 값을 제거합니다.

우리의 Spring Security - @PreFilter 및 @PostFilter의 기사는 자세히 모두 어노테이션을 설명합니다.

3.5. 메서드 Security 메타 어노테이션

우리는 일반적으로 동일한 Security 구성을 사용하여 다른 방법을 보호하는 상황에 처해 있습니다.

이 경우 Security 메타 어노테이션을 정의할 수 있습니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}

다음으로 @IsViewer 어노테이션을 직접 사용하여 메서드를 보호할 수 있습니다.

@IsViewer
public String getUsername4() {
    //...
}

Security 메타 어노테이션은 더 많은 의미를 추가하고 Security 프레임워크에서 비즈니스 로직을 분리하기 때문에 좋은 아이디어입니다.

3.6. 클래스 수준의 Security 어노테이션

한 클래스 내의 모든 메서드에 대해 동일한 Security 어노테이션을 사용하는 경우 해당 어노테이션을 클래스 수준에 두는 것을 고려할 수 있습니다.

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {

    public String getSystemYear(){
        //...
    }
 
    public String getSystemDate(){
        //...
    }
}

위의 예에서 Security 규칙 hasRole('ROLE_ADMIN')getSystemYeargetSystemDate 메소드 모두에 적용됩니다 .

3.7. 메소드에 대한 다중 Security 어노테이션

또한 하나의 방법에 여러 Security 어노테이션을 사용할 수도 있습니다.

@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

이런 식으로 Spring은 secureLoadUserDetail 메소드 실행 전후에 권한 부여를 확인 합니다.

4. 중요 고려 사항

메서드 Security과 관련하여 기억하고 싶은 두 가지 사항이 있습니다.

  • 기본적으로 Spring AOP 프록시는 메소드 Security을 적용하는 데 사용됩니다. Security 메서드 A가 같은 클래스 내의 다른 메서드에 의해 호출되면 A의 Security은 모두 무시됩니다. 이는 방법 A가 Security 검사 없이 실행됨을 의미합니다. private 메소드에도 동일하게 적용됩니다.
  • Spring SecurityContext 는 스레드 바운드입니다. 기본적으로 Security 컨텍스트는 자식 스레드로 전파되지 않습니다. 자세한 내용은 Spring Security Context Propagation 문서를 참조하십시오.

5. 테스트 방법 Security

5.1. 구성

JUnit으로 Spring Security를 ​​테스트하려면 spring-security-test 의존성이 필요합니다 .

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
</dependency>

Spring Boot 플러그인을 사용하기 때문에 의존성 버전을 지정할 필요가 없습니다. Maven Central 에서 이 의존성의 최신 버전을 찾을 수 있습니다 .

다음으로 러너와 ApplicationContext 구성 을 지정하여 간단한 Spring 통합 테스트를 구성해 보겠습니다 .

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MethodSecurityIntegrationTest {
    // ...
}

5.2. 사용자 이름 및 역할 테스트

이제 구성이 준비 되었으므로 @Secured(“ROLE_VIEWER”) 어노테이션으로 보호한 getUsername 메서드 를 테스트해 보겠습니다 .

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

여기서 @Secured 어노테이션 을 사용하기 때문에 메서드를 호출하려면 사용자가 인증되어야 합니다. 그렇지 않으면 AuthenticationCredentialsNotFoundException이 발생 합니다.

따라서 Security 방법을 테스트할 사용자를 제공해야 합니다.

이를 달성하기 위해 테스트 메서드를 @WithMockUser로 장식하고 사용자와 역할을 제공합니다 .

@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();
    
    assertEquals("john", userName);
}

사용자 이름이 john 이고 역할이 ROLE_VIEWER인증된 사용자를 제공했습니다 . 사용자 이름 이나 역할을 지정하지 않으면 기본 사용자 이름user 이고 기본 역할ROLE_USER 입니다.

Spring Security가 자동으로 해당 접두사를 추가하기 때문에 여기에 ROLE_ 접두사 를 추가할 필요가 없습니다 .

해당 접두사를 사용하지 않으려면 역할 대신 권한 사용을 고려할 수 있습니다 .

예를 들어 getUsernameInLowerCase 메서드를 선언해 보겠습니다 .

@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
    return getUsername().toLowerCase();
}

권한을 사용하여 테스트할 수 있습니다.

@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
    String username = userRoleService.getUsernameInLowerCase();

    assertEquals("john", username);
}

편리하게도 많은 테스트 사례에 대해 동일한 사용자를 사용하려는 경우 테스트 클래스에서 @WithMockUser 어노테이션을 선언할 수 있습니다 .

@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class MockUserAtClassLevelIntegrationTest {
    //...
}

익명 사용자로 테스트를 실행하려면 @WithAnonymousUser 어노테이션을 사용할 수 있습니다 .

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
    userRoleService.getUsername();
}

위의 예에서, 우리는 기대 AccessDeniedException을 익명 사용자는 역할이 부여되지 않기 때문에 ROLE_VIEWER 또는 권한 SYS_ADMIN을 .

5.3. 사용자 지정 UserDetailsService로 테스트

대부분의 애플리케이션에서 사용자 지정 클래스를 인증 주체로 사용하는 것이 일반적입니다. 이 경우 커스텀 클래스는 org.springframework.security.core.userdetails 를 구현해야 한다. 사용자 세부 정보 인터페이스.

이 글에서 우리는 선언 CustomUser의 기존의 구현 확장 클래스 의 UserDetails 입니다 org.springframework.security.core.userdetails을. 사용자 :

public class CustomUser extends User {
    private String nickName;
    // getter and setter
}

섹션 3 @PostAuthorize 어노테이션이 있는 예제를 다시 살펴보겠습니다 .

@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

이 경우 반환된 CustomUser사용자 이름현재 인증 주체의 별명 과 같은 경우에만 메서드가 성공적으로 실행됩니다 .

해당 메서드를 테스트 하려면 사용자 이름을 기반으로 CustomUser로드할 수 있는 UserDetailsService 구현을 제공할 수 있습니다 .

@Test
@WithUserDetails(
  value = "john", 
  userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
 
    CustomUser user = userService.loadUserDetail("jane");

    assertEquals("jane", user.getNickName());
}

여기서 @WithUserDetails 어노테이션은 인증된 사용자를 초기화 하기 위해 UserDetailsService사용할 것이라고 명시합니다 . 서비스는 userDetailsServiceBeanName 속성에 의해 참조됩니다 . UserDetailsService 는 실제 구현이거나 테스트 목적으로 가짜일 수 있습니다.

또한 서비스는 속성 값의 값 을 사용자 이름으로 사용하여 UserDetails 를 로드 합니다.

편리하게, 우리는 또한으로 꾸밀 수 @WithUserDetails의 유사 우리가 함께 한 일에, 클래스 레벨의 어노테이션 @WithMockUser의 어노테이션 .

5.4. 메타 어노테이션으로 테스트하기

다양한 테스트에서 동일한 사용자/역할을 반복해서 재사용하는 경우가 많습니다.

이러한 상황에서는 메타 어노테이션 을 만드는 것이 편리합니다 .

이전 예제 @WithMockUser(username=”john”, roles={“VIEWER”}) 를 다시 살펴보면 메타 어노테이션을 선언할 수 있습니다.

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }

그런 다음 테스트에서 @WithMockJohnViewer간단히 사용할 수 있습니다 .

@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();

    assertEquals("john", userName);
}

마찬가지로 메타 어노테이션을 사용하여 @WithUserDetails를 사용하여 도메인별 사용자를 생성할 수 있습니다 .

6. 결론

이 기사에서는 Spring Security에서 Method Security를 ​​사용하기 위한 다양한 옵션을 살펴보았습니다.

또한 메서드 Security을 쉽게 테스트할 수 있는 몇 가지 기술을 살펴보고 다른 테스트에서 모의 ​​사용자를 재사용하는 방법을 배웠습니다.

이 기사의 모든 예제는 GitHub 에서 찾을 수 있습니다 .

Generic footer banner