Skip to content

Filtering Authorization Method Level#

Filtering Authorization Method Level In Spring Security#

  • If we have a scenario where we don't want to control the invocation of the method but we want to make sure the parameters send and received to/from the method need to follow authorization rules, then we can consider filtering.
  • The @PreFilter and @PostFilter annotations are used to filter lists of objects based on custom security rules we define.
  • @PreFilter defines a rule for filtering a list that is being passed as an input parameter to the annotated method. If the evaluated value is true, the item will be kept in the list. Otherwise, the item will be removed.
  • For filtering the parameters before calling the method we can use @PreFilter as below.
ContactController.java
1
2
3
4
5
@RequestMapping(method = RequestMethod.POST, path = "/v1/contact")  
@PreFilter("filterObject.subject == 'Hello'")  
public ResponseEntity<List<Contact>> createContact(@RequestBody List<Contact> contacts) {  
    return ResponseEntity.ok(this.contactService.createContacts(contacts));  
}
  • As you can see in the example above, we will use @PreFilter() to check the subject field of Contact object is equal with the Hello value or not. If the value of subject field is not equal with Hello, the Contact object will be removed out of the list.

  • @PostFilter defines a rule for filtering the return list of a method, by applying that rule to every element in the list. If the evaluated value is true, the item will be kept in the list. Otherwise, the item will be removed.

  • For filtering the parameters after executing the method we can use @PostFilter as the example below.
ContactController.java
1
2
3
4
5
@PostFilter("filterObject.contactName == 'Han'")  
public List<Contact> getContacts() {  
    List<ContactEntity> contactEntities = this.contactRepository.findAll();  
    return this.toContacts(contactEntities);  
}
  • We can use the @PostFilter on the Spring Data repository methods as well to filter any unwanted data coming from the database. It means, after querying Contacts from the database. Only Contact has contactName is Han will be kept in the return list. Otherwise, the item will be removed.

PreFiltering And PostFiltering Examples#

Database Tables#

  • We will create contacts table which will contain some columns such as contactName, contactEmail, subject and message. So, let's run the SQL script below to create the table.
1
2
3
4
5
6
7
8
CREATE TABLE `contacts` (
  `id` int NOT NULL,
  `contact_email` varchar(255) DEFAULT NULL,
  `contact_name` varchar(255) DEFAULT NULL,
  `message` varchar(255) DEFAULT NULL,
  `subject` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Entity#

  • Then in we create the ContactEntity class as below:
ContactEntity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.spring.security.spring.security.filtering.method.level.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name = "contacts")
@Getter
@Setter
public class ContactEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    private String contactName;

    private String contactEmail;

    private String subject;

    private String message;

}

Repository#

  • Let's create ContactRepository below.
ContactRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.spring.security.spring.security.filtering.method.level.repository;

import com.spring.security.spring.security.filtering.method.level.entity.ContactEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ContactRepository extends JpaRepository<ContactEntity, Integer> {

}

Service#

  • Next, we need to create a service class which call to ContactRepository for getting ContactEntities and map them into Contact DTOs.
ContactService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.spring.security.spring.security.filtering.method.level.service;

import com.spring.security.spring.security.filtering.method.level.entity.ContactEntity;
import com.spring.security.spring.security.filtering.method.level.model.Contact;
import com.spring.security.spring.security.filtering.method.level.repository.ContactRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ContactService {

    @Autowired
    private ContactRepository contactRepository;

    public List<Contact> createContacts(List<Contact> contacts) {
        List<ContactEntity> contactEntities = this.toContactEntities(contacts);
        contactEntities = this.contactRepository.saveAll(contactEntities);
        return this.toContacts(contactEntities);
    }

    private List<ContactEntity> toContactEntities(List<Contact> contacts) {
        List<ContactEntity> contactEntities = new ArrayList<>();
        contacts.forEach(c -> {
            ContactEntity contactEntity = new ContactEntity();
            contactEntity.setContactEmail(c.getContactEmail());
            contactEntity.setContactName(c.getContactName());
            contactEntity.setSubject(c.getSubject());
            contactEntity.setMessage(c.getMessage());
            contactEntities.add(contactEntity);
        });
        return contactEntities;
    }

    public List<Contact> getContacts() {
        List<ContactEntity> contactEntities = this.contactRepository.findAll();
        return this.toContacts(contactEntities);
    }

    private List<Contact> toContacts(List<ContactEntity> contactEntities) {
        List<Contact> contacts = new ArrayList<>();
        contactEntities.forEach(c -> {
            Contact contact = new Contact();
            contact.setId(c.getId());
            contact.setContactName(c.getContactName());
            contact.setContactEmail(c.getContactEmail());
            contact.setSubject(c.getSubject());
            contact.setMessage(c.getMessage());
            contacts.add(contact);
        });
        return contacts;
    }

}
  • The Contact DTO will look like below.
Contact.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.spring.security.spring.security.filtering.method.level.model;


import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Contact {

    private Integer id;

    private String contactName;

    private String contactEmail;

    private String subject;

    private String message;

}

Controller#

  • Finally, we will create a new api in the ContactController for getting Contacts by customerId.
ContactController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.spring.security.spring.security.filtering.method.level.controller;

import com.spring.security.spring.security.filtering.method.level.model.Contact;
import com.spring.security.spring.security.filtering.method.level.service.ContactService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ContactController {

    @Autowired
    private ContactService contactService;

    @RequestMapping(method = RequestMethod.GET, path = "/v1/contact")
    public ResponseEntity<String> getContactDetail() {
        return ResponseEntity.ok("This is the contact details");
    }

    @RequestMapping(method = RequestMethod.POST, path = "/v1/contact")
    public ResponseEntity<List<Contact>> createContact(@RequestBody List<Contact> contacts) {
        return ResponseEntity.ok(this.contactService.createContacts(contacts));
    }

    @RequestMapping(method = RequestMethod.GET, path = "/v1/contacts")
    public ResponseEntity<List<Contact>> getContacts() {
        return ResponseEntity.ok(this.contactService.getContacts());
    }

}

Configuration#

  • Now, to use Method Level Security we will enable it by add the annotation @EnableGlobalMethodSecurity into the main class as below.
CustomDefaultSpringSecurityApplication.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.spring.security.spring.security.filtering.method.level;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@SpringBootApplication
@EnableJpaRepositories
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SpringSecurityFilteringMethodLevelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityFilteringMethodLevelApplication.class, args);
    }
}
  • The param prePostEnabled = true means we enables Spring Security @PreFilter & @PostFilter annotations.
  • Now, let's try to add @PreFilter into the ContactController as below.
ContactController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.spring.security.spring.security.filtering.method.level.controller;

import com.spring.security.spring.security.filtering.method.level.model.Contact;
import com.spring.security.spring.security.filtering.method.level.service.ContactService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ContactController {

    @Autowired
    private ContactService contactService;

    @RequestMapping(method = RequestMethod.GET, path = "/v1/contact")
    public ResponseEntity<String> getContactDetail() {
        return ResponseEntity.ok("This is the contact details");
    }

    @RequestMapping(method = RequestMethod.POST, path = "/v1/contact")
    @PreFilter("filterObject.subject == 'Hello'")
    public ResponseEntity<List<Contact>> createContact(@RequestBody List<Contact> contacts) {
        return ResponseEntity.ok(this.contactService.createContacts(contacts));
    }

    @RequestMapping(method = RequestMethod.GET, path = "/v1/contacts")
    public ResponseEntity<List<Contact>> getContacts() {
        return ResponseEntity.ok(this.contactService.getContacts());
    }

}
  • As you can see the @PreFilter will require all input object have to contain subject equal Hello value. If not it will be removed out of the list automatically.

  • Then in the ContactService. We will add @PostFilter as below.

ContactService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.spring.security.spring.security.filtering.method.level.service;

import com.spring.security.spring.security.filtering.method.level.entity.ContactEntity;
import com.spring.security.spring.security.filtering.method.level.model.Contact;
import com.spring.security.spring.security.filtering.method.level.repository.ContactRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ContactService {

    @Autowired
    private ContactRepository contactRepository;

    public List<Contact> createContacts(List<Contact> contacts) {
        List<ContactEntity> contactEntities = this.toContactEntities(contacts);
        contactEntities = this.contactRepository.saveAll(contactEntities);
        return this.toContacts(contactEntities);
    }

    private List<ContactEntity> toContactEntities(List<Contact> contacts) {
        List<ContactEntity> contactEntities = new ArrayList<>();
        contacts.forEach(c -> {
            ContactEntity contactEntity = new ContactEntity();
            contactEntity.setContactEmail(c.getContactEmail());
            contactEntity.setContactName(c.getContactName());
            contactEntity.setSubject(c.getSubject());
            contactEntity.setMessage(c.getMessage());
            contactEntities.add(contactEntity);
        });
        return contactEntities;
    }

    @PostFilter("filterObject.contactName == 'Han'")
    public List<Contact> getContacts() {
        List<ContactEntity> contactEntities = this.contactRepository.findAll();
        return this.toContacts(contactEntities);
    }

    private List<Contact> toContacts(List<ContactEntity> contactEntities) {
        List<Contact> contacts = new ArrayList<>();
        contactEntities.forEach(c -> {
            Contact contact = new Contact();
            contact.setId(c.getId());
            contact.setContactName(c.getContactName());
            contact.setContactEmail(c.getContactEmail());
            contact.setSubject(c.getSubject());
            contact.setMessage(c.getMessage());
            contacts.add(contact);
        });
        return contacts;
    }

}
  • As you can see the @PostFilter will require all object have to contain contactName equal Han value. If not it will be removed out of the list automatically.

Testing#

  • Now, let's start our Spring Security application and call api /v1/user with user email han.do@example.com for login and get the jwt token.

 #zoom

  • Then we will use this jwt token to call POST /v1/contact for creating Contacts with request's body as below
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[
    {
        "contactName": "Han",
        "contactEmail": "han.do@example.com",
        "subject": "Hello",
        "message": "Hello from Han"
    },
     {
        "contactName": "John",
        "contactEmail": "john.wick@example.com",
        "subject": "Hello",
        "message": "Hello from John"
    },
    {
        "contactName": "Tom",
        "contactEmail": "tom.wick@example.com",
        "subject": "Hi",
        "message": "Hi from Tom"
    }
]
  • So, with this request body and the @PreFilter that we set up, there are only two first contacts that will passed the @PreFilter and saved into the database because they have subject Hello. Let's execute the request and you will received the result as below.

 #zoom

 #zoom

  • As you can see, only two first contacts are saved in the database.

  • Then we will use the jwt token to continue to call GET /v1/contacts for getting Contacts. With the current set up for @PostFilter we will only received the first Contact record in the database because on it has contactName equal Han. Let's execute the request and you will received the result as below.

 #zoom

Conclusion#

  • @PreFilter and @PostFilter are useful in some special case and easy to use. It helps developers saving for time for writing codes to checking data. However, If we use it for a very large list so it can be a performance issue and inefficient.
  • For example if we have thousands of Contacts in our database and we want to retrieve the five Contacts that are currently have contactName with Han value. If we use @PreFilter the database operation will fetch all the Contacts first, and iterate through all of them to filter out the ones that have not contactName with Han value.

See Also#

References#