Skip to content

Invocation Authorization Method Level#

Invocation Authorization Method Level In Spring Security#

  • Using invocation authorization we can decide if a user is authorized to invoke a method before the method executed (preauthorization) or after the method execution is completed (postauthorization).
  • For filtering the parameters before calling the method we can use Prefiltering with annotation @PreAuthorize. See the sample configuration below.
LoanService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Service
public class LoanService {

    @PreAuthorize("hasAuthority('admin')")
    @PreAuthorize("hasRole('admin')")
    @PreAuthorize("hasAnyRole('admin')")
    @PreAuthorize("# username == authentication.principal.username")
    @PreAuthorize("hasPermission(returnObject, 'admin')")
    public Load getLoadDetails(String username) {
        return this.loadRepository.loadLoanByUsername(username);
    }

}
  • For applying postauthorization rules we can use annotation @PostAuthorize. Below is the sample configuration.
LoanService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
public class LoanService {

    @PostAuthorize("returnObject.username == authentication.principal.username")
    @PostAuthorize("hasPermission(returnObject, 'admin')")
    public Load getLoadDetails(String username) {
        return this.loadRepository.loadLoanByUsername(username);
    }

}
  • When implementing complex authorization logic, we can separate the logic using a separate class that implements PermissionEvaluator and override the method hasPermission() inside it which can be leveraged inside the hasPermission configuration.
  • You should note that when applying the @PostAuthorize for the method and the user is not authorized to invoke that method. So the method will still be executed the business logic present inside it. Like it will commit the transactions to database, but while retaining the values to the user, if the alteration rules are not satisfying, then it will throw 403 error, which is an exception. But please don't expect database transactions to be rolled back, because even if you keep adding transaction annotation on top of the method, the committee transactions to the database will no be rolled back.

PreAuthorization And PostAuthorization Examples#

Database Tables#

  • We will create loan table which will have manyToOne relationship with customers table. It means one customer can have many loans. The loan table will contain some columns such as description, loan, paid, start_date and customer_id. So, let's run the SQL script below to create the table.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CREATE TABLE `loan` (
  `id` int NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `loan` float NOT NULL,
  `paid` float NOT NULL,
  `start_date` datetime(6) DEFAULT NULL,
  `customer_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK9vp9nqmxjlgsm2620ppi02hl4` (`customer_id`),
  CONSTRAINT `FK9vp9nqmxjlgsm2620ppi02hl4` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  • Now for the current customer with email han.do@example.com in the customers table we will use the SQL statements below to set a loan for this customer.
1
2
3
INSERT INTO worldbank.loan
(id, description, loan, paid, start_date, customer_id)
VALUES(1, 'loan for buying car', 5000.0, 2500.0, '2022-09-01 12:12:12', 2);
  • So, after all our table will have data as below:
  • customers table
id email password role
1 duc.nguyen@example.com 12345 admin
2 han.do@example.com $2a$12$V.A53NkiPnA45W44aRYi2OLwUbbu08aDoY409/SKY/bT7cdF1PpLO admin
3 john.wick@example.com $2a$12$V.A53NkiPnA45W44aRYi2OLwUbbu08aDoY409/SKY/bT7cdF1PpLO user
  • roles table
id role customer_id
1 ROLE_ADMIN 2
2 ROLE_USER 3
3 ROLE_USER 2
  • loan table
id description loan paid start_date customer_id
1 loan for buying car 5000.0 2500.0 2022-09-01 12:12:12 2

Entity#

  • After updating the database tables, we will also extend our CustomerEntity with LoanEntity. So in CustomerEntity we will have a list of LoanEntity with @OneToMany relationship.
CustomerEntity.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
package com.spring.security.spring.security.invocation.method.level.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.List;
import java.util.Set;

@Entity
@Table(name = "customers")
@Getter
@Setter
public class CustomerEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String email;
    private String password;
    private String role;

    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private Set<AuthorityEntity> authorities;

    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private Set<RoleEntity> roles;

    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private List<LoanEntity> loans;

}
  • Then in we create the LoanEntity class as below:
LoanEntity.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
package com.spring.security.spring.security.invocation.method.level.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "loan")
@Getter
@Setter
public class LoanEntity {

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

    private Date startDate;

    private String description;

    private float loan;

    private float paid;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private CustomerEntity customer;

}

Repository#

  • Let's create LoanRepository below for query LoanEntities by customerId.
LoanRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.spring.security.spring.security.invocation.method.level.repository;

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

import java.util.List;

@Repository
public interface LoanRepository extends JpaRepository<LoanEntity, Integer> {

    List<LoanEntity> findByCustomerId(Integer customerId);

}

Service#

  • Next, we need to create a service class which call to LoanRepository for getting LoanEntities and map them into Loan DTO.
LoanService.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
package com.spring.security.spring.security.invocation.method.level.service;  

import com.spring.security.spring.security.invocation.method.level.entity.LoanEntity;  
import com.spring.security.spring.security.invocation.method.level.model.Loan;  
import com.spring.security.spring.security.invocation.method.level.repository.LoanRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;

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

@Service
public class LoanService {

    @Autowired
    private LoanRepository loanRepository;

    public List<Loan> getLoans(Integer customerId) {
        List<LoanEntity> loanEntities = this.loanRepository.findByCustomerId(customerId);
        List<Loan> loans = new ArrayList<>();
        for (LoanEntity loanEntity: loanEntities) {
            Loan loan = new Loan();
            loan.setId(loanEntity.getId());
            loan.setLoan(loanEntity.getLoan());
            loan.setPaid(loanEntity.getPaid());
            loan.setDescription(loanEntity.getDescription());
            loan.setStartDate(loanEntity.getStartDate());
            loans.add(loan);
        }
        return loans;

    }

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


import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class Loan {

    private Integer id;
    private Date startDate;
    private String description;
    private float loan;
    private float paid;

}

Controller#

  • Finally, we will create a new api in the LoanController for getting Loans by customerId.
LoanController.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
package com.spring.security.spring.security.invocation.method.level.controller;  

import com.spring.security.spring.security.invocation.method.level.model.Loan;  
import com.spring.security.spring.security.invocation.method.level.service.LoanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
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 LoanController {

    @Autowired
    private LoanService loanService;

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

    @RequestMapping(method = RequestMethod.GET, path = "/v1/loan/customers/{customerId}")
    public ResponseEntity<List<Loan>> getLoansByCustomerId(@PathVariable("customerId") Integer customerId) {
        return ResponseEntity.ok(this.loanService.getLoans(customerId));
    }

}

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.invocation.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 SpringSecurityInvocationMethodLevelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityInvocationMethodLevelApplication.class, args);
    }
}
  • The param prePostEnabled = true means we enables Spring Security @PreAuthorize & @PostAuthorize annotations.
  • Now, let's try to add @PreAuthorize and @PostAuthorize into the LoanController as below.
LoanController.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
package com.spring.security.spring.security.invocation.method.level.controller;  

import com.spring.security.spring.security.invocation.method.level.model.Loan;  
import com.spring.security.spring.security.invocation.method.level.service.LoanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
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 LoanController {

    @Autowired
    private LoanService loanService;

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(method = RequestMethod.GET, path = "/v1/loan")
    public ResponseEntity<String> getLoanDetail() {
        return ResponseEntity.ok("This is the loan details");
    }

    @PostAuthorize("hasRole('ADMIN')")
    @RequestMapping(method = RequestMethod.GET, path = "/v1/loan/customers/{customerId}")
    public ResponseEntity<List<Loan>> getLoansByCustomerId(@PathVariable("customerId") Integer customerId) {
        return ResponseEntity.ok(this.loanService.getLoans(customerId));
    }

}
  • As you can see, both Apis will require user has role ADMIN to get the resource. However, with @PreAuthorize the Spring Security will check the authorization before the code in the method is executed. For @PostAuthorize the Spring Security will check the authorization after the code in the method is executed.

Testing#

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

 #zoom

  • Then we will use this jwt token to call /v1/loan and /v1/loan/customers/{customerId} for @PreAuthorize and @PostAuthorize. You will receive 200 OK status as below.

 #zoom

 #zoom

  • Next, we will try to call api /v1/user with user email john.wick@example.com which has USER role for login and get the jwt token.

 #zoom

  • Then we will use this jwt token to call /v1/loan and /v1/loan/customers/{customerId} for @PreAuthorize and @PostAuthorize. You will receive 403 Forbidden status as below.

 #zoom

 #zoom

  • However, if you put a debugger in the LoanService which is call by /v1/loan/customers/{customerId} api with @PostAuthorize. You will see the code of this api will be executed and spring security only check the Authorization after the api is going to response.

 #zoom

See Also#

References#