The issues I encountered when migrating Spring Boot from 2.x to 3.x including Spring Security

Hugo
4 min readJan 24, 2023

--

In this article I share my exprience in migration of Spring boot 3.x. My application contains login/social login, basic auth and hibernate.

  1. Upgrade to Java 17. It’s the minimum requirement.
  2. Update Spring boot to the newest 2.x version

as mentioend in the official guide

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide

Replace WebSecurityConfigurerAdapter

Create Beans for SecurityFilterChain is not a big issue at this step yet.

Short reference:

https://www.baeldung.com/spring-deprecated-websecurityconfigureradapter

Long:

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

My biggest issue here is to understand the difference between Global AuthenticationManager and Local AuthenticationManager.

To create an AuthenticationManager that is available to the entire application you register the AuthenticationManager as a @Bean.

Local AuthenticationManagermeans the default AuthenticationManager is overwritten for a specific SecurityFilterChain.

In my usecase, some endpoints are authenticated by Basic auth and the credentials are set via environment variables. The other endpoints are authenticated by email and password stored in the database. Thus,

At the end, these are the examlpes for Local and Global AuthenticationManager

BasicAuthSecurityConfig.kt

@Configuration
class BasicAuthSecurityConfig(

@Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser(client.clientId)
.password("{bcrypt}${passwordEncoder.encode(client.clientSecret)}")
.roles(*client.clientRoles!!.map { it.name }.toTypedArray())

}
}

UserRegistrationSecurityConfig.kt

@Configuration
class UserRegistrationSecurityConfig {

@Bean
fun authManager(http: HttpSecurity): AuthenticationManager {
val authenticationManagerBuilder = http.getSharedObject(
AuthenticationManagerBuilder::class.java
)
.userDetailsService(userDetailsService)
.passwordEncoder(encoder)
return authenticationManagerBuilder.build()
}
}

After replacing, I started to test social login and login, and basic auth to make sure this part is fine and then move on.

I must admit that seeing what’s working doesn’t really match the theory about Local/Global AuthenticationManager, but I do see the issue comes from AuthenticationManager, when your basic auth works and the other doesn’t and the other way around.

My approach probably configures the Global AuthenticationManager to work both for basic auth and user login.

Upgrade to 3.x

Simply change it in the package mamangement tool. Then you start to see some red flags. That’s ok. Lets deal with them one by one

Update SecurityFilterChain

This @Order(1) @Order(2) … no longer work for my application. I start to use securityMatcher. I like the explaination in the offical guide very much.

The idea is each SecurityFilterChain is registered for certain paths by “securityMatcher()”

BasicAuthSecurityConfig.kt

   
@Bean
fun filterChainBasicAuth(http: HttpSecurity): SecurityFilterChain {
http.securityMatcher(
"/admin/**,
)
authorizeHttpRequests {
authz ->
authz
.requestMatchers(
"/admin/users/**",
).hasRole("admin).anyRequest().authenticated()
....

UserRegistrationSecurityConfig.kt

@Bean
fun filterChainBasicAuth(http: HttpSecurity): SecurityFilterChain {
http.securityMatcher(
"/user/**,
)
authorizeHttpRequests {
authz ->
authz
.requestMatchers(
"/user/**",
)....

For sure in your code you will want to replace “/admin” with a const and replace “/user” with a const as well

This step takes me some time to figure out and verify the configuration. Take your time and be patient.

Replace import javax with import jakarta

There are a lot import javax has to be replaced by import jakarta. Use IDE to help this task.

Error: Schema-validation: missing sequence

This is the other issue that needs to read documentation. I recommend to read

Spring Boot 2 uses hibernate 5 and Spring Boot 3 uses hibernate 6. For auto-generated id like the following:

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long = 0

Hibernatet 5 relies on one sequence for all tables, and Hibernate 6 requires each table has its own index. You can find this behaviour in your database.

It will be cubersome to handle the sequence issue now, so I decide to set the following in application.properties

spring.jpa.properties.hibernate.id.db_structure_naming_strategy=single

Such that the legacy behaviour is kept.

Swagger does not show up

You can remove all the libs from SpringDoc(My app had three) and only use

implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2")

Official guide: https://springdoc.org/v2/#migrating-from-springdoc-v1

Keep the format of the error response body

My application use both @ControllerAdvice and throw ResponseStatusException.

For those managed by @ControllerAdvice, the format doesn’t change. For those error response body generated by ResponseStatusException, the format changes.

The following class adds the missing fields

// Backward-compatible with error response body of Spring Boot 2
@ControllerAdvice
class BackwardCompatibleResponseEntityExceptionHandler : ResponseEntityExceptionHandler() {
override fun createResponseEntity(
@Nullable body: Any?,
headers: HttpHeaders,
statusCode: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
if (body is ProblemDetail) {
val problemDetail = body
problemDetail.setProperty("error", problemDetail.title!!)
problemDetail.setProperty("timestamp", Date())

if (request is ServletWebRequest) {
problemDetail.setProperty(
"path", request.request
.requestURI
)
}
}
return ResponseEntity(body, headers, statusCode)
}
}

Ref: https://stackoverflow.com/questions/75029947/error-response-body-changed-after-boot-3-upgrade

The error response body is broken when using @CotrollerAdvice and ResponseStatusException together.

Example when throwing ResponseStatusException(404, “Resource not found”)`

without @ControllerAdvice :

{
"timestamp": "2023-01-01T01::10.111+00:00",
"status": 404,
"error": "Not Found",
"message": "Resource not found",
"path": "/api/v1/hi"
}

with @ControllerAdvice :

{
"type": "about:blank",
"title": "problemDetail.title.org.springframework.web.server.ResponseStatusException",
"status": 404,
"detail": "problemDetail.org.springframework.web.server.ResponseStatusException",
"instance": "/api/v1/apps/token"
}

@ControllerAdvice forces the error response body of ResponseStatusException to follow the RFC standard introduced in Spring Boot 3.

The source code [ResponseEntityExceptionHandler] doesn’t set ex.exception (ResponseStatusException) to title or detail, and only pick the java class type.

 @Nullable
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {

if (request instanceof ServletWebRequest servletWebRequest) {
HttpServletResponse response = servletWebRequest.getResponse();
if (response != null && response.isCommitted()) {
if (logger.isWarnEnabled()) {
logger.warn("Response already committed. Ignoring: " + ex);
}
return null;
}
}

if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}

// See here, ex(ResponseStatusException) is not used. "body" doesn't know anything about ex
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale());
}

return createResponseEntity(body, headers, statusCode, request);
}

In the future: I would recommend to only use @ControllerAdvice instead of ResponseStatusException. For migration phase, I recommend to wrap ResponseStatusException and let it return a cusom ErrorResponse and not replace all places that are using ResponseStatusException.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Responses (1)

Write a response