Search code examples
springspring-bootspring-securityspring-restcontrollerspring-rest

Strange Security Error in Spring Boot 3.0.2


I have a very strange security issue in Spring Boot 3.0.2

When I try to secure a REST service with @PreAuthorize and the method contains parameters I get the following error message:

Name for argument of type [int] not specified, and parameter name information not found in class file either.

If I remove the annotation, everything works again.

Here is my source code:

This work, no parameters on method listAll:

@RestController
@RequestMapping("/api/currency")
@PreAuthorize("hasAnyRole('api','frontend')")  // <<--
public class CurrencyRestService {

    @GetMapping("/")
    public Page<Currency> listAll() { // <<--
        return currencyController.listAll(0, 100);
    }
}

This work too, no PreAuthorize, but with parameters

@RestController
@RequestMapping("/api/currency")
//@PreAuthorize("hasAnyRole('api','frontend')") // <<--
public class CurrencyRestService {

    @GetMapping("/")
    public Page<Currency> listAll(@RequestParam(defaultValue = "0") int page,      // <<--
                                  @RequestParam(defaultValue = "100") int size) {  // <<--
        return currencyController.listAll(page, size);
    }
}

This not work:

@RestController
@RequestMapping("/api/currency")
@PreAuthorize("hasAnyRole('api','frontend')")  // <<--
public class CurrencyRestService {

    @GetMapping("/")
    public Page<Currency> listAll(@RequestParam(defaultValue = "0") int page,      // <<--
                                  @RequestParam(defaultValue = "100") int size) {  // <<--
        return currencyController.listAll(page, size);
    }
}

I don't know what I can do? Did anyone had an idea?

My SecurityFilterChain:

http.securityMatcher("/api/**")
            .oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(authenticationConverter);

http.sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.csrf().disable();

http.securityMatcher("/api/**")
            .authorizeHttpRequests()
                .requestMatchers("/api/**")
                .authenticated();
http.build();

EDIT:

This also doesn't work:

@RestController
@RequestMapping("/api/currency")
@PreAuthorize("hasAnyRole('api','frontend')")  // <<--
public class CurrencyRestService {

    @GetMapping("/")
    public Page<Currency> listAll(@RequestParam(defaultValue = "0",
                                                name = "page") int page,      // <<--
                                  @RequestParam(defaultValue = "100",
                                                name = "size") int size) {  // <<--
        return currencyController.listAll(page, size);
    }
}

Also javac Parameter -g:vars and / or -parameters not help

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <forceJavacCompilerUse>true</forceJavacCompilerUse>
        <compilerArgs>
            <arg>-g:vars</arg>
            <arg>-parameters</arg>
        </compilerArgs>
        <source>${maven.compiler.source}</source>
        <target>${maven.compiler.target}</target>
    </configuration>
</plugin>

EDIT 2:

If I use @EnableGlobalMethodSecurity the Application don't start. I get this error message:

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.support.BeanDefinitionOverrideException:
Invalid bean definition with name 'metaDataSourceAdvisor' defined in null: Cannot register bean definition
[Root bean: class [org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null]
for bean 'metaDataSourceAdvisor' since there is already 
[Root bean: class [org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null] bound.

Is done, I already had the annotation.

EDIT 3:

If I don't use @RequestParam but do it this way it works:

@RestController
@RequestMapping("/api/currency")
@PreAuthorize("hasAnyRole('api','frontend')")  // <<--
public class CurrencyRestService {

    @GetMapping("/")
    public Page<Currency> listAll(Map<String, String> reqParam) { // <<--
        int page = 0;
        int size = 100;
        try {
            page = Integer.parseInt(reqParam.get("page"));
            size = Integer.parseInt(reqParam.get("size"));
        } catch (Exception e) {
            page = 0;
            size = 100;
        }
        if (size <= 0) {
            size = 100;
        }
        return currencyController.listAll(page, size);
    }
}

EDIT 4:

The problem is even stranger. If I now make a POST, then only a completely empty object arrives in the method "add". Of course I get a NotNull Error. If I turn off the security, the object arrives clean and complete.

@PostMapping("/")
@PreAuthorize("hasAnyRole('api-write','frontend')")
public ResponseEntity add(@RequestBody Currency newObject) {
        System.err.println(newObject.toString());
        return new ResponseEntity(this.controller.add(newObject), HttpStatus.OK);
}

Error message:

ConstraintViolationImpl{interpolatedMessage='darf nicht null sein', propertyPath=updatedBy, rootBeanClass=class de.bewidata.framework.entity.zrentity.Currency, messageTemplate='{jakarta.validation.constraints.NotNull.message}'}

Send Object:

{"objectType":"Currency","objectId":"2ea69820-1f8b-46c8-80c1-7e61e2f983fa","objectVersion":0,"createdAt":"2023-03-02T15:44:37.690+01:00","createdBy":"system","updatedAt":"2023-03-02T15:44:37.690+01:00","updatedBy":"system","deleted":false,"deletedAt":null,"deletedBy":null,"deactivated":false,"deactivatedAt":null,"id":0,"name":"Dollar","iso2":null,"iso3":"USD","symbol":"$","alignment":"RIGHT","exchangeRatio":1.00}

Getted Object by security on:

{"objectType":"Currency","objectId":"2ea69820-1f8b-46c8-80c1-7e61e2f983fa","objectVersion":null,"createdAt":"2023-03-02T15:44:37.690+01:00","createdBy":null,"updatedAt":"2023-03-02T15:44:37.690+01:00","updatedBy":null,"deleted":false,"deletedAt":null,"deletedBy":null,"deactivated":false,"deactivatedAt":null,"id":null,"name":null,"iso2":null,"iso3":null,"symbol":null,"alignment":"RIGHT","exchangeRatio":null}

EDIT 5: I debug the request and found a difference. I hope that help in any way. If the Rest Request work, without the Annotaion @PreAuthorize("hasAnyRole('api','frontend')"), the right abstract class come into the MethodParameter, so the parameter names could found. When I add the annotation, the wrong extendet class is in the MethodParameter also with final, and the names couldn't found.

package org.springframework.web.method.annotation;

public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { // WORK without Security  parameter.executable = public org.springframework.data.domain.Page com.example.framework.AbstractRestService.listAll(int,int)
                                                                          // NOT WORK with Security parameter.executable = public final org.springframework.data.domain.Page com.example.framework.BusinessRoleRestService$$SpringCGLIB$$0.listAll(int,int)
        NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
        if (namedValueInfo == null) {
            namedValueInfo = createNamedValueInfo(parameter);
            namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
            this.namedValueInfoCache.put(parameter, namedValueInfo);
        }
        return namedValueInfo;
    }
}

EDIT 5:

I was able to isolate the problem. But do not know how to solve it. I noticed that this $$SpringCGLIB$$ proxy class is only created for the rest of the service classes where another method is inserted.

This work without Problems:

@RestController
@RequestMapping("/api/businessrole")
@Tag(name = "businessrole")
@Extension
@PreAuthorize("hasAnyRole('api','frontend')")
public class BusinessRoleRestService extends AbstractRestService<BusinessRoleController, Long, BusinessRole> {

    public BusinessRoleRestService(BusinessRoleController businessRoleController) {
        super(businessRoleController);
    }
}

Here the $$SpringCGLIB$$ proxy class created, and I get the Error Message.

@RestController
@RequestMapping("/api/businessrole")
@Tag(name = "businessrole")
@Extension
@PreAuthorize("hasAnyRole('api','frontend')")
public class BusinessRoleRestService extends AbstractRestService<BusinessRoleController, Long, BusinessRole> {

    public BusinessRoleRestService(BusinessRoleController businessRoleController) {
        super(businessRoleController);
    }

    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                         description = "Success")
    })
    @Operation(summary = "",
               description = "")
    @GetMapping("/types")
    public List<String> listAllSubclasses() {
        return this.controller.findAllSubclasses();
    }
}

Solution

  • I found a solution. With ClassUtils.getUserClass(clazz); I get the right class and could register manualy the methods with an RequestMappingHandlerMapping. Now all work. I still thing it is a Bug. But I don't know where.