The spring security permission annotation of SpringBoot performs permission authentication in multiple ways

Keywords: Java Spring Boot RESTful

preface #

Spring Security supports method level permission control. In this mechanism, we can add permission annotations to any method at any layer. The annotated methods will be automatically protected by Spring Security and only allow specific users to access, so as to achieve the purpose of permission control. Of course, if the existing permission annotations are not enough, we can also customize them

Quick start #

  1. First, add the security dependency as follows
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. Then create a new security configuration class

Spring Security disables annotation by default. To enable annotation, add @ EnableMethodSecurity annotation to the class that inherits WebSecurityConfigurerAdapter, and define AuthenticationManager as Bean in this class.

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

We see @ EnableGlobalMethodSecurity   They are prePostEnabled, securedEnabled and jsr250Enabled respectively   There are three fields. Each field code is supported by an annotation. The default is false and true is on. Then let's talk about the three general comments one by one.

prePostEnabled = true enables the @ PreAuthorize and @ PostAuthorize annotations of Spring Security.

securedEnabled = true enables the @ Secured annotation of Spring Security.

jsr250Enabled = true enables the @ RoleAllowed annotation

Please refer to my two articles for more details on using integration
Easy to use SpringBoot+SpringSecurity+JWT restful API permission control practice

Spring Security core interface, user permission acquisition, authentication process execution principle

Set permission authentication on method #

JSR-250 notes #

JSR-250 standard notes are followed
Main notes

  1. @DenyAll
  2. @RolesAllowed
  3. @PermitAll

Inside @ DenyAll   and  @ PermitAll   I believe there is no need to say more, on behalf of rejection and adoption.

@RolesAllowed   Use example

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

The method representing annotation can be accessed as long as it has any permission of user and Admin. You can omit the prefix ROLE_, The actual permission may be ROLE_ADMIN

In terms of function and use method  @ Secured   Exactly the same

securedEnabled annotation #

Main notes

@Secured

  1. The @ Secured annotation for Spring Security. The annotation specifies the role list of the access method, and at least one role is specified in the list

  2. @Secure specifies the security on the method, and requires roles / permissions. Only users with corresponding roles / permissions can call these methods. If someone tries to call a method but does not have the required role / permission, access will be denied and an exception will be thrown.

For example:

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

@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
public String getUsername2() {
    //...
}

@Secured("ROLE_VIEWER")   Indicates that only have role_ Only users with the viewer role can access the getUsername() method.

@Secured({ "ROLE_DBA", "ROLE_ADMIN" })   Indicates that the user owns "role_dba" and "role_admin"   Either of the two roles can be accessed   getUsername2   method.

Another point is @ Secured, which does not support Spring EL expressions

prePostEnabled annotation #

It is quite powerful to support Spring EL expressions after this is enabled. If you do not have permission to access the method, an AccessDeniedException will be thrown.

Main notes

  1. @PreAuthorize  -- Verify authorization before entering method

  2. @PostAuthorize  -- The authorization method is not executed until it is checked, and the return value of the execution method can be affected

three  @ PostFilter  -- It is executed after the method is executed, and the return value of the method can be called here, and then the return value can be filtered or processed or modified and returned

  1. @PreFilter  -- It is executed before the method is executed, and the parameters of the method can be called here, and then the parameter values can be filtered or processed or modified

@PreAuthorize annotation usage #

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

@PreAuthorize("hasRole('ROLE_VIEWER ')) is equivalent to @ Secured(" ROLE_VIEWER ").

alike  @ Secured({“ROLE_VIEWER”,”ROLE_EDITOR”})   It can also be replaced with: @ PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('role_editor')).

In addition, we can also use expressions on the parameters of the method:

Before the method is executed, the parameters of the method can be called and the parameter values can be obtained. Here, the parameter name reflection feature of JAVA8 is used. If there is no JAVA8, the parameters can also be marked with @ P of spring security or @ Param of Spring Data.

//No java8
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority('ADMIN')")
void changePassword(@P("userId") long userId ){}
//There is java8
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority('ADMIN')")
void changePassword(long userId ){}

This means that before the changePassword method is executed, judge whether the value of the method parameter userId is equal to the userId of the current user saved in the principal, or whether the current user has a role_ You can access this method with admin permission, which is one of the two.

@PostAuthorize annotation usage #

After the method is executed, it can be executed to obtain the return value of the method, and the final authorization result (whether access is allowed or not) can be determined according to the method:

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

In the above code, access is allowed only when the username in the return value of the loadUserDetail method is the same as the username of the currently logged in user

Note that if EL is false, the method has been executed and may be rolled back. The EL variable returnObject represents the returned object.

@PreFilter and @ PostFilter annotations are used #

Spring Security provides a @ PreFilter   Annotation to filter the passed in parameters:

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

If the subitem in usernames is different from the user name of the currently logged in user, it will be retained; if the subitem in usernames is the same as the user name of the currently logged in user, it will be removed. For example, if the user name of the currently used user is zhangsan, and the value of usernames is {"zhangsan", "lisi", "wangwu"}, then after @ PreFilter filtering, the value of the actually passed in usernames is {"lisi", "wangwu"}

If the execution method contains multiple parameters of type Collection, the filterObject is not sure which Collection parameter to filter. In this case, you need to add the filterTarget property to specify the specific parameter name:

@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(";"));
}

Similarly, we can also use @ PostFilter   Comments are filtered through the returned Collection:

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

At this time, filterObject represents the return value. If you follow the above code, you can remove the sub item in the return value that is the same as the user name of the currently logged in user.

Custom meta annotation #

If we need to use the same security annotation in multiple methods, we can improve the maintainability of the project by creating meta annotations.

For example, create the following meta annotation:

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

Then you can directly add the annotation to the corresponding method:

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

In production projects, meta annotations are a good choice because they separate business logic from security framework.

Use security annotations on classes #

If we apply the same security annotation to all methods in a class, we should raise the security annotation to the class level:

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {
  
    public String getSystemYear(){
        //...
    }
  
    public String getSystemDate(){
        //...
    }
}

The above code realizes that the ROLE_ADMIN permission is required to access the getSystemYear and getSystemDate methods.

Method #

When one security annotation cannot meet our needs, we can also apply multiple security annotations:

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

At this time, Spring Security will execute the security policy of @ PreAuthorize before executing the method and @ PostAuthorize after executing the method.

summary #

Based on our experience, the following two tips are given:

  1. By default, the use of security annotations in methods is implemented by the Spring AOP proxy, which means that if we call method 2 of the same class using security annotations in method 1, the security annotations on method 2 will become invalid.

  2. The Spring Security context is thread bound, which means that the security context will not be passed to child threads.

public boolean isValidUsername4(String username) {
    // The following method will skip security authentication
    this.getUsername();
    return true;
}

Posted by xeq on Tue, 28 Sep 2021 14:35:07 -0700