Spring Security ACL 소개

1. 소개

ACL ( 액세스 제어 목록 ) 은 개체에 연결된 권한 목록입니다. ACL의 신원이 주어진 객체에 어떤 작업을 부여 지정합니다.

Spring Security Access Control ListDomain Object Security 를 지원 하는 Spring 컴포넌트입니다 . 간단히 말해, Spring ACL은 일반적인 작업 수준에서 보드 전체가 아닌 단일 도메인 개체에서 특정 사용자 / 역할에 대한 권한을 정의하는 데 도움이됩니다.

예를 들어 관리자 역할을 가진 사용자 는 중앙 알림 상자의 모든 메시지를 보고 ( READ) 편집 ( WRITE) 할 수 있지만 일반 사용자는 메시지를보고 관련되고 편집 할 수만 있습니다. 한편 편집자 역할을 가진 다른 사용자는 일부 특정 메시지를보고 편집 할 수 있습니다.

따라서 서로 다른 사용자 / 역할은 각 특정 개체에 대해 서로 다른 권한을 갖습니다. 이 경우 Spring ACL 은 작업을 수행 할 수 있습니다. 이 기사에서는 Spring ACL 을 사용하여 기본 권한 검사를 설정하는 방법을 살펴 보겠습니다 .

2. 구성

2.1. ACL 데이터베이스

Spring Security ACL 을 사용하려면 데이터베이스에 4 개의 필수 테이블을 생성해야합니다.

첫 번째 테이블은 도메인 객체의 클래스 이름을 저장하는 ACL_CLASS 이며 열에는 다음이 포함됩니다.

  • 신분증
  • CLASS : 보안 도메인 개체의 클래스 이름 (예 : com.baeldung.acl.persistence.entity.NoticeMessage)

둘째, 시스템의 모든 원칙이나 권한을 보편적으로 식별 할 수 있는 ACL_SID 테이블이 필요합니다 . 테이블에는 다음이 필요합니다.

  • 신분증
  • SID : 사용자 이름 또는 역할 이름입니다. SIDSecurity Identity를 의미합니다.
  • PRINCIPAL : 0 또는 1 , 해당 SID 가 보안 주체 (예 : mary, mike, jack… ) 또는 권한 (예 : ROLE_ADMIN, ROLE_USER, ROLE_EDITOR… ) 임을 나타냅니다.

다음 테이블은 각 고유 한 도메인 개체에 대한 정보를 저장하는 ACL_OBJECT_IDENTITY입니다 .

  • 신분증
  • OBJECT_ID_CLASS : 도메인 개체 클래스 정의,ACL_CLASS 테이블에 대한 링크
  • OBJECT_ID_IDENTITY : 도메인 개체는 클래스에 따라 여러 테이블에 저장할 수 있습니다. 따라서이 필드는 대상 개체 기본 키를 저장합니다.
  • PARENT_OBJECT : 이 테이블 내에서이 개체 ID의 부모 지정
  • OWNER_SID : 객체 소유자의 ID , ACL_SID 테이블에 대한 링크
  • ENTRIES_INHERITTING : 이 개체의 ACL 항목 이 상위 개체에서 상속 되는지 여부 ( ACL 항목ACL_ENTRY 테이블에 정의 )

마지막으로 ACL_ENTRY 저장소 개별 권한 은 개체 ID의SID 에 할당됩니다 .

  • 신분증
  • ACL_OBJECT_IDENTITY : 객체 ID 지정, ACL_OBJECT_IDENTITY 테이블 링크
  • ACE_ORDER : 해당 객체 IDACL 항목 목록 에서 현재 항목의 순서
  • SID : 권한이 부여되거나 거부 된 대상 SID , ACL_SID 테이블에 대한 링크
  • MASK : 허용 또는 거부되는 실제 권한을 나타내는 정수 비트 마스크
  • GRANTING : 값 1은 승인을 의미하고 값 0 은 거부를 의미합니다.
  • AUDIT_SUCCESSAUDIT_FAILURE : 감사용

2.2. 의존

프로젝트에서 Spring ACL 을 사용할 수 있도록 먼저 종속성을 정의하겠습니다.

 org.springframework.security spring-security-acl   org.springframework.security spring-security-config   org.springframework spring-context-support   net.sf.ehcache ehcache-core 2.6.11 

Spring ACLObject IdentityACL 항목 을 저장하기 위해 캐시가 필요 하므로 여기서는 Ehcache를 사용 합니다. 그리고, 지원 으로 Ehcache를봄, 우리는 또한 필요 스프링 컨텍스트 지원합니다.

Spring Boot로 작업하지 않을 때는 명시 적으로 버전을 추가해야합니다. 이는 Maven Central에서 확인할 수 있습니다 : spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. ACL 관련 구성

전역 메서드 보안 을 활성화하여 보안 도메인 개체를 반환하거나 개체를 변경하는 모든 메서드를 보호해야합니다 .

@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { @Autowired MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { return defaultMethodSecurityExpressionHandler; } }

Spring Expression Language (SpEL) 를 사용하려면 prePostEnabledtrue 로 설정 하여 Expression-Based Access Control 을 활성화 해 보겠습니다 . 또한 , 우리와 표현 처리기가 필요 ACL의 지원을 :

@Bean public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService()); expressionHandler.setPermissionEvaluator(permissionEvaluator); return expressionHandler; }

따라서 , 우리는 지정 AclPermissionEvaluator을 받는 DefaultMethodSecurityExpressionHandler . 평가자 는 데이터베이스에서 권한 설정 및 도메인 개체의 정의를로드하려면 MutableAclService 가 필요 합니다.

단순화를 위해 제공된 JdbcMutableAclService를 사용합니다 .

@Bean public JdbcMutableAclService aclService() { return new JdbcMutableAclService( dataSource, lookupStrategy(), aclCache()); }

이름으로 JdbcMutableAclServiceJDBCTemplate 을 사용하여 데이터베이스 액세스를 단순화합니다. 그것은 필요 (데이터 소스 에 대한 JdbcTemplate을)를 , LookupStrategy는 (데이터베이스를 조회 할 때 조회 최적화 제공) 및 AclCache은 ( 캐시 ACL 항목객체 신원을) .

다시 말하지만, 단순성을 위해 제공된 BasicLookupStrategyEhCacheBasedAclCache를 사용 합니다.

@Autowired DataSource dataSource; @Bean public AclAuthorizationStrategy aclAuthorizationStrategy() { return new AclAuthorizationStrategyImpl( new SimpleGrantedAuthority("ROLE_ADMIN")); } @Bean public PermissionGrantingStrategy permissionGrantingStrategy() { return new DefaultPermissionGrantingStrategy( new ConsoleAuditLogger()); } @Bean public EhCacheBasedAclCache aclCache() { return new EhCacheBasedAclCache( aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy() ); } @Bean public EhCacheFactoryBean aclEhCacheFactoryBean() { EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); ehCacheFactoryBean.setCacheName("aclCache"); return ehCacheFactoryBean; } @Bean public EhCacheManagerFactoryBean aclCacheManager() { return new EhCacheManagerFactoryBean(); } @Bean public LookupStrategy lookupStrategy() { return new BasicLookupStrategy( dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger() ); } 

여기서 AclAuthorizationStrategy 는 현재 사용자가 특정 개체에 필요한 모든 권한을 소유하고 있는지 여부를 결정하는 역할을합니다.

It needs the support of PermissionGrantingStrategy, which defines the logic for determining whether a permission is granted to a particular SID.

3. Method Security With Spring ACL

So far, we've done all necessary configuration. Now we can put required checking rule on our secured methods.

By default, Spring ACL refers to BasePermission class for all available permissions. Basically, we have a READ, WRITE, CREATE, DELETE and ADMINISTRATION permission.

Let's try to define some security rules:

@PostFilter("hasPermission(filterObject, 'READ')") List findAll(); @PostAuthorize("hasPermission(returnObject, 'READ')") NoticeMessage findById(Integer id); @PreAuthorize("hasPermission(#noticeMessage, 'WRITE')") NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

After the execution of findAll() method, @PostFilter will be triggered. The required rule hasPermission(filterObject, ‘READ'), means returning only those NoticeMessage which current user has READ permission on.

Similarly, @PostAuthorize is triggered after the execution of findById() method, make sure only return the NoticeMessage object if the current user has READ permission on it. If not, the system will throw an AccessDeniedException.

On the other side, the system triggers the @PreAuthorize annotation before invoking the save() method. It will decide where the corresponding method is allowed to execute or not. If not, AccessDeniedException will be thrown.

4. In Action

Now we gonna test all those configurations using JUnit. We'll use H2 database to keep configuration as simple as possible.

We'll need to add:

 com.h2database h2   org.springframework spring-test test   org.springframework.security spring-security-test test 

4.1. The Scenario

In this scenario, we'll have two users (manager, hr) and a one user role (ROLE_EDITOR), so our acl_sid will be:

INSERT INTO acl_sid (id, principal, sid) VALUES (1, 1, 'manager'), (2, 1, 'hr'), (3, 0, 'ROLE_EDITOR');

Then, we need to declare NoticeMessage class in acl_class. And three instances of NoticeMessage class will be inserted in system_message.

Moreover, corresponding records for those 3 instances must be declared in acl_object_identity:

INSERT INTO acl_class (id, class) VALUES (1, 'com.baeldung.acl.persistence.entity.NoticeMessage'); INSERT INTO system_message(id,content) VALUES (1,'First Level Message'), (2,'Second Level Message'), (3,'Third Level Message'); INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES (1, 1, 1, NULL, 3, 0), (2, 1, 2, NULL, 3, 0), (3, 1, 3, NULL, 3, 0);

Initially, we grant READ and WRITE permissions on the first object (id =1) to the user manager. Meanwhile, any user with ROLE_EDITOR will have READ permission on all three objects but only possess WRITE permission on the third object (id=3). Besides, user hr will have only READ permission on the second object.

Here, because we use default Spring ACLBasePermission class for permission checking, the mask value of the READ permission will be 1, and the mask value of WRITE permission will be 2. Our data in acl_entry will be:

INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) VALUES (1, 1, 1, 1, 1, 1, 1, 1), (2, 1, 2, 1, 2, 1, 1, 1), (3, 1, 3, 3, 1, 1, 1, 1), (4, 2, 1, 2, 1, 1, 1, 1), (5, 2, 2, 3, 1, 1, 1, 1), (6, 3, 1, 3, 1, 1, 1, 1), (7, 3, 2, 3, 2, 1, 1, 1);

4.2. Test Case

First of all, we try to call the findAll method.

As our configuration, the method returns only those NoticeMessage on which the user has READ permission.

Hence, we expect the result list contains only the first message:

@Test @WithMockUser(username = "manager") public void givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){ List details = repo.findAll(); assertNotNull(details); assertEquals(1,details.size()); assertEquals(FIRST_MESSAGE_ID,details.get(0).getId()); }

Then we try to call the same method with any user which has the role – ROLE_EDITOR. Note that, in this case, these users have the READ permission on all three objects.

Hence, we expect the result list will contain all three messages:

@Test @WithMockUser(roles = {"EDITOR"}) public void givenRoleEditor_whenFindAllMessage_thenReturn3Message(){ List details = repo.findAll(); assertNotNull(details); assertEquals(3,details.size()); }

Next, using the manager user, we'll try to get the first message by id and update its content – which should all work fine:

@Test @WithMockUser(username = "manager") public void givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){ NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(firstMessage); assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); firstMessage.setContent(EDITTED_CONTENT); repo.save(firstMessage); NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(editedFirstMessage); assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId()); assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent()); }

But if any user with the ROLE_EDITOR role updates the content of the first message – our system will throw an AccessDeniedException:

@Test(expected = AccessDeniedException.class) @WithMockUser(roles = {"EDITOR"}) public void givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){ NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(firstMessage); assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); firstMessage.setContent(EDITTED_CONTENT); repo.save(firstMessage); }

Similarly, the hr user can find the second message by id, but will fail to update it:

@Test @WithMockUser(username = "hr") public void givenUsernameHr_whenFindMessageById2_thenOK(){ NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID); assertNotNull(secondMessage); assertEquals(SECOND_MESSAGE_ID,secondMessage.getId()); } @Test(expected = AccessDeniedException.class) @WithMockUser(username = "hr") public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){ NoticeMessage secondMessage = new NoticeMessage(); secondMessage.setId(SECOND_MESSAGE_ID); secondMessage.setContent(EDITTED_CONTENT); repo.save(secondMessage); }

5. Conclusion

이 기사에서 Spring ACL 의 기본 구성 및 사용법을 살펴 보았습니다 .

아시다시피 Spring ACL 은 객체, 원칙 / 권한 및 권한 설정을 관리하기위한 특정 테이블이 필요했습니다. 이러한 테이블과의 모든 상호 작용, 특히 업데이트 작업은 AclService거쳐야합니다. 이후 기사에서 기본 CRUD 작업에 대해이 서비스를 살펴 보겠습니다 .

기본적으로 BasePermissio n 클래스의 미리 정의 된 권한으로 제한됩니다 .

마지막으로이 튜토리얼의 구현은 Github에서 찾을 수 있습니다.