Spring Security6 has a new way of writing, a big change!

There have been some changes in the writing method of Spring Security configuration in recent versions. Many common methods have been abandoned and will be removed in the future Spring Security7. Therefore, Song Ge based on last year’s old article , and added some new content, and reposted it for reference by friends who use Spring Security.

Next, I will sort out all the known changes starting from Spring Security 5.7 (corresponding to Spring Boot 2.7) with my friends.

1. WebSecurityConfigurerAdapter

First of all, the first point is that the WebSecurityConfigurerAdapter, which is most easily discovered by friends, has expired. In the latest Spring Security6.1, this class has been completely removed, and it is no longer possible to make do with it.

To be precise, Spring Security expired the WebSecurityConfigurerAdapter in version 5.7.0-M2. The reason for the expiration is that the official wants to encourage developers to use component-based security configuration.

So what is component-based security configuration? Let’s give a few examples:

In the past, the way we configured SecurityFilterChain was as follows:

@Configuration 
public  class  SecurityConfiguration  extends  WebSecurityConfigurerAdapter {

    @Override 
    protected  void  configure (HttpSecurity http)  throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}

Then it will be changed to the following:

@Configuration 
public  class  SecurityConfiguration {

    @Bean 
    public SecurityFilterChain filterChain (HttpSecurity http)  throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

If you understand the previous writing method, the following code is actually easy to understand. I won’t explain it too much. However, friends who still don’t understand the basic usage of Spring Security can reply to ss in the background of the official account. It was written by Brother Song. Tutorial.

Previously we configured WebSecurity like this:

@Configuration 
public  class  SecurityConfiguration  extends  WebSecurityConfigurerAdapter {

    @Override 
    public  void  configure (WebSecurity web) {
        web.ignoring().antMatchers( "/ignore1" , "/ignore2" );
    }

}

In the future, it will have to be changed to the following:

@Configuration 
public  class  SecurityConfiguration {

    @Bean 
    public WebSecurityCustomizer webSecurityCustomizer () {
         return (web) -> web.ignoring().antMatchers( "/ignore1" , "/ignore2" );
    }

}

Another one is about obtaining AuthenticationManager. In the past, you could obtain this Bean by overriding the parent class method, similar to the following:

@Configuration 
public  class  SecurityConfig  extends  WebSecurityConfigurerAdapter {
     @Override 
    @Bean 
    public AuthenticationManager authenticationManagerBean ()  throws Exception {
         return  super .authenticationManagerBean();
    }
}

In the future, you can only create this Bean yourself, similar to the following:

@Configuration 
public  class  SecurityConfig {

    @Autowired
    UserService userService;

    @Bean 
    AuthenticationManager authenticationManager () {
         DaoAuthenticationProvider  daoAuthenticationProvider  =  new  DaoAuthenticationProvider ();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager  pm  =  new  ProviderManager (daoAuthenticationProvider);
         return pm;
    }
}

Of course, the AuthenticationManager can also be extracted from HttpSecurity, as follows:

@Configuration 
public  class  SpringSecurityConfiguration {

    AuthenticationManager authenticationManager;

    @Autowired
    UserDetailsService userDetailsService;

    @Bean 
    public SecurityFilterChain filterChain (HttpSecurity http)  throws Exception {

        AuthenticationManagerBuilder  authenticationManagerBuilder  = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService);
        authenticationManager = authenticationManagerBuilder.build();

        http.csrf().disable().cors().disable().authorizeHttpRequests().antMatchers( "/api/v1/account/register" , "/api/v1/account/auth" ).permitAll()
            .anyRequest().authenticated()
            .and()
            .authenticationManager(authenticationManager)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}

This is also a way.

Let’s look at a specific example.

First, we create a new Spring Boot project, introduce Web and Spring Security dependencies, and pay attention to select the latest version of Spring Boot.

Next we provide a simple test interface, as follows:

@RestController 
public  class  HelloController {

    @GetMapping("/hello") 
    public String hello () {
         return  "hello Jiangnan a little rain!" ;
    }
}

Friends know that in Spring Security, by default, as long as dependencies are added, all interfaces of our project have been protected. Now to start the project and access /hellothe interface, you need to log in before you can access it. The logged-in user The name is user, and the password is randomly generated in the project’s startup log.

Now our first requirement is to use a custom user instead of the default one provided by the system. This is simple. We only need to register an instance of UserDetailsService with the Spring container, like the following:

@Configuration 
public  class  SecurityConfig {

    @Bean 
    UserDetailsService userDetailsService () {
         InMemoryUserDetailsManager  users  =  new  InMemoryUserDetailsManager ();
        users.createUser(User.withUsername( "javaboy" ).password( "{noop}123" ).roles( "admin" ).build());
        users.createUser(User.withUsername( "Jiangnan Yidianyu" ).password( "{noop}123" ).roles( "admin" ).build());
         return users;
    }

}

That’s it.

Of course, my current users are stored in memory. If your users are stored in the database, then you only need to provide the implementation class of the UserDetailsService interface and inject it into the Spring container. This has been mentioned many times in the vhr video before (public account Backend reply 666 has a video introduction), so I won’t go into details here.

But if I want /hellothis interface to be accessible anonymously, and I want this anonymous access not to go through the Spring Security filter chain, in the past, we could rewrite configure(WebSecurity)the method for configuration, but now, we have to change the way:

@Configuration 
public  class  SecurityConfig {

    @Bean 
    UserDetailsService userDetailsService () {
         InMemoryUserDetailsManager  users  =  new  InMemoryUserDetailsManager ();
        users.createUser(User.withUsername( "javaboy" ).password( "{noop}123" ).roles( "admin" ).build());
        users.createUser(User.withUsername( "Jiangnan Yidianyu" ).password( "{noop}123" ).roles( "admin" ).build());
         return users;
    }

    @Bean 
    WebSecurityCustomizer webSecurityCustomizer () {
         return  new  WebSecurityCustomizer () {
             @Override 
            public  void  customize (WebSecurity web) {
                web.ignoring().antMatchers( "/hello" );
            }
        };
    }

}

The content that was previously located configure(WebSecurity)in the method is now located in the WebSecurityCustomizer Bean. The configuration can be written here.

What if I also want to customize the login page, parameters, etc.? Read on:

@Configuration 
public  class  SecurityConfig {

    @Bean 
    UserDetailsService userDetailsService () {
         InMemoryUserDetailsManager  users  =  new  InMemoryUserDetailsManager ();
        users.createUser(User.withUsername( "javaboy" ).password( "{noop}123" ).roles( "admin" ).build());
        users.createUser(User.withUsername( "Jiangnan Yidianyu" ).password( "{noop}123" ).roles( "admin" ).build());
         return users;
    }

    @Bean 
    SecurityFilterChain securityFilterChain () {
        List<Filter> filters = new  ArrayList <>();
         return  new  DefaultSecurityFilterChain ( new  AntPathRequestMatcher ( "/**" ), filters);
    }

}

The bottom layer of Spring Security is actually a bunch of filters, so our previous configuration in the configure(HttpSecurity) method is actually configuring the filter chain. Now for the configuration of the filter chain, we configure the filter chain by providing a SecurityFilterChain Bean. SecurityFilterChain is an interface. This interface has only one implementation class, DefaultSecurityFilterChain. The first parameter to build DefaultSecurityFilterChain is the interception rule, that is, which paths need to be intercepted. The second parameter is the filter chain. Here I gave an empty collection, that is, our Spring Security will intercept all requests, and then it will end after walking around in an empty collection, which is equivalent to not intercepting any requests. .

Restart the project at this time and you will find that /helloit can also be accessed directly because this path does not pass through any filter.

In fact, I think the current new way of writing is more intuitive than the old way of writing, and it is easier for everyone to understand the underlying filter chain working mechanism of Spring Security.

Some friends will say that this way of writing is different from what I have written before! With this configuration, I don’t know what filters there are in Spring Security. In fact, if we change the writing method, we can configure this as before:

@Configuration 
public  class  SecurityConfig {

    @Bean 
    UserDetailsService userDetailsService () {
         InMemoryUserDetailsManager  users  =  new  InMemoryUserDetailsManager ();
        users.createUser(User.withUsername( "javaboy" ).password( "{noop}123" ).roles( "admin" ).build());
        users.createUser(User.withUsername( "Jiangnan Yidianyu" ).password( "{noop}123" ).roles( "admin" ).build());
         return users;
    }

    @Bean 
    SecurityFilterChain securityFilterChain (HttpSecurity http)  throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();
    }

}

Writing it this way is actually not much different from the previous way of writing it.

2. Use Lambda

In the latest version, friends found that many common methods have been abandoned, as shown below:

Including the familiar and() method used to connect various configuration items is now abandoned, and according to the official statement, this method will be completely removed in Spring Security7.

In other words, you will no longer see configurations like the following:

@Override 
protected  void  configure (HttpSecurity http)  throws Exception {
     InMemoryUserDetailsManager  users  =  new  InMemoryUserDetailsManager ();
    users.createUser(User.withUsername( "javagirl" ).password( "{noop}123" ).roles( "admin" ).build());
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .userDetailsService(users);
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

and() method will be removed!

In fact, Brother Song thinks that removing the and method is a good thing. For many beginners, it takes a long time just to understand the and method.

As you can see from the comments of the and method above, the official is now promoting Lambda-based configuration to replace the traditional chain configuration, so in the future our writing method will have to be changed to the following:

@Configuration 
public  class  SecurityConfig {

    @Bean 
    SecurityFilterChain securityFilterChain (HttpSecurity http)  throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.requestMatchers( "/hello" ).hasAuthority( "user" ).anyRequest().authenticated())
                .formLogin(form -> form.loginProcessingUrl( "/login" ).usernameParameter( "name" ).passwordParameter( "passwd" ))
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.maximumSessions( 1 ).maxSessionsPreventsLogin( true ));
         return http.build();
    }
}

In fact, the methods here are not new methods, but some friends may not be used to using the above methods for configuration before, and are used to chain configuration. But in the future, you have to slowly get used to the above configuration according to the Lambda method. The configuration content is easy to understand, I think there is nothing to explain.

3. Customized JSON login

Custom JSON login is also different from the previous version.

3.1 Custom JSON login

Friends know that the default login interface data format in Spring Security is in the form of key-value. If we want to use JSON format to log in, then we must customize the filter or customize the login interface. Next, Brother Song will first talk to Xiao Guys, show me these two different login forms.

3.1.1 Custom login filter

Spring Security’s default filter for processing login data is UsernamePasswordAuthenticationFilter. In this filter, the system will request.getParameter(this.passwordParameter)read the username and password through . Obviously, this requires the front-end to pass parameters in the form of key-value.

If you want to use JSON format parameters to log in, then you need to make a fuss from this place. Our custom filters are as follows:

public  class  JsonLoginFilter  extends  UsernamePasswordAuthenticationFilter {
     @Override 
    public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response)  throws AuthenticationException {
         //Get the request header and judge the request parameter type based on it 
        String  contentType  = request.getContentType();
         if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase( contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {
             //Indicate that the request parameter is JSON 
            if (!request.getMethod().equals( "POST" )) {
                 throw  new  AuthenticationServiceException ( "Authentication method not supported: " + request.getMethod());
            }
            String  username  =  null ;
             String  password  =  null ;
             try {
                 //Parse the JSON parameters in the request body 
                User  user  =  new  ObjectMapper ().readValue(request.getInputStream(), User.class);
                username = user.getUsername();
                username = (username != null ) ? username.trim() : "" ;
                password = user.getPassword();
                password = (password != null ) ? password : "" ;
            } catch (IOException e) {
                 throw  new  RuntimeException (e);
            }
            //Build login token 
            UsernamePasswordAuthenticationToken  authRequest  = UsernamePasswordAuthenticationToken.unauthenticated(username,
                    password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //Perform the real login operation 
            Authentication  auth  =  this .getAuthenticationManager().authenticate(authRequest);
             return auth;
        } else {
             return  super .attemptAuthentication(request, response);
        }
    }
}

Friends who have read Song Ge’s previous Spring Security series of articles should be very familiar with this code.

  1. First, we obtain the request header and determine the format of the request parameters based on the type of the request header.
  2. If it is a parameter in JSON format, it is processed in the if. Otherwise, it is a parameter in the form of key-value, then we just call the method of the parent class for processing.
  3. The processing logic of parameters in JSON format is the same as that of key-value. The only difference is that the parameters are extracted in different ways.

Finally, we need to configure this filter:

@Configuration 
public  class  SecurityConfig {

    @Autowired
    UserService userService;

    @Bean 
    JsonLoginFilter jsonLoginFilter () {
         JsonLoginFilter  filter  =  new  JsonLoginFilter ();
        filter.setAuthenticationSuccessHandler((req,resp,auth)->{
            resp.setContentType( "application/json;charset=utf-8" );
             PrintWriter  out  = resp.getWriter();
             //Get the current user object that successfully logged in 
            User  user  = (User) auth.getPrincipal();
            user.setPassword( null );
             RespBean  respBean  = RespBean.ok( "Login successful" , user);
            out.write( new  ObjectMapper ().writeValueAsString(respBean));
        });
        filter.setAuthenticationFailureHandler((req,resp,e)->{
            resp.setContentType( "application/json;charset=utf-8" );
             PrintWriter  out  = resp.getWriter();
             RespBean  respBean  = RespBean.error( "Login failed" );
             if (e instanceof BadCredentialsException) {
                respBean.setMessage( "The username or password was entered incorrectly and login failed" );
            } else  if (e instanceof DisabledException) {
                respBean.setMessage( "Account disabled, login failed" );
            } else  if (e instanceof CredentialsExpiredException) {
                respBean.setMessage( "Password expired, login failed" );
            } else  if (e instanceof AccountExpiredException) {
                respBean.setMessage( "Account expired, login failed" );
            } else  if (e instanceof LockedException) {
                respBean.setMessage( "Account is locked, login failed" );
            }
            out.write( new  ObjectMapper ().writeValueAsString(respBean));
        });
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl( "/login" );
         return filter;
    }

    @Bean 
    AuthenticationManager authenticationManager () {
         DaoAuthenticationProvider  daoAuthenticationProvider  =  new  DaoAuthenticationProvider ();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager  pm  =  new  ProviderManager (daoAuthenticationProvider);
         return pm;
    }

    @Bean 
    SecurityFilterChain securityFilterChain (HttpSecurity http)  throws Exception {
         //Enable filter configuration
        http.authorizeHttpRequests()
                //Any request must be authenticated before access
                .anyRequest().authenticated()
                .and()
                //Enable form login. Once enabled, the login page, login interface and other information will be automatically configured.
                .formLogin()
                //All URL addresses related to login are allowed
                .permitAll()
                .and()
                //Turn off the csrf protection mechanism, which essentially removes the CsrfFilter from the Spring Security filter chain.
                .csrf().disable();
        http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

}

Here is to configure a JsonLoginFilter Bean and add it to the Spring Security filter chain.

Before Spring Boot3 (before Spring Security6), the above code could implement JSON login.

However, starting from Spring Boot 3, this code is a bit flawed, and it is no longer possible to log in with JSON directly. The specific reasons are analyzed below by Song Ge.

3.1.2 Custom login interface

Another way to customize JSON login is to directly customize the login interface, as follows:

@RestController 
public  class  LoginController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/doLogin") 
    public String doLogin ( @RequestBody User user) {
         UsernamePasswordAuthenticationToken  unauthenticated  = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
         try {
             Authentication  authenticate  = authenticationManager.authenticate(unauthenticated);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            return  "success" ;
        } catch (AuthenticationException e) {
             return  "error:" + e.getMessage();
        }
    }
}

The login interface is directly customized here, and the request parameters are passed in the form of JSON. After getting the username and password, call the AuthenticationManager#authenticate method for authentication. After successful authentication, the authenticated user information is stored in the SecurityContextHolder.

Finally, just configure the login interface:

@Configuration 
public  class  SecurityConfig {

    @Autowired
    UserService userService;

    @Bean 
    AuthenticationManager authenticationManager () {
         DaoAuthenticationProvider  provider  =  new  DaoAuthenticationProvider ();
        provider.setUserDetailsService(userService);
        ProviderManager  pm  =  new  ProviderManager (provider);
         return pm;
    }

    @Bean 
    SecurityFilterChain securityFilterChain (HttpSecurity http)  throws Exception {
        http.authorizeHttpRequests()
                //Indicates that the address /doLogin can be accessed directly without logging in.requestMatchers 
                ( "/doLogin" ).permitAll()
                .anyRequest().authenticated().and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();
    }
}

This is also a solution using JSON format parameters. Before Spring Boot3 (before Spring Security6), there was no problem with the above solution.

Starting from Spring Boot3 (Spring Security6), both of the above solutions have some flaws.

The specific manifestation is: after you successfully log in by calling the login interface, and then access other pages in the system, it will jump back to the login page, indicating that when accessing other interfaces other than login, the system does not know that you have already logged in.

3.2 Cause analysis

The cause of the above problem is mainly that one of the filters in the Spring Security filter chain has changed:

Before Spring Boot3, there was a filter named SecurityContextPersistenceFilter in the Spring Security filter chain. This filter was abandoned in Spring Boot2.7.x, but is still in use. In Spring Boot3, it was removed from the Spring Security filter Removed from the chain and replaced by a filter called SecurityContextHolderFilter.

The two JSON login solutions introduced with my friends in the first section can run in Spring Boot2.x but cannot run in Spring Boot3.x, which is caused by the change of this filter.

So next let’s analyze the differences between these two filters.

Let’s first look at the core logic of SecurityContextPersistenceFilter:

private  void  doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
        throws IOException, ServletException {
     HttpRequestResponseHolder  holder  =  new  HttpRequestResponseHolder (request, response);
     SecurityContext  contextBeforeChainExecution  =  this .repo.loadContext(holder);
     try {
        SecurityContextHolder.setContext(contextBeforeChainExecution);
        chain.doFilter(holder.getRequest(), holder.getResponse());
    }
    finally {
         SecurityContext  contextAfterChainExecution  = SecurityContextHolder.getContext();
        SecurityContextHolder.clearContext();
        this .repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
    }
}

I only posted some key core code here:

  1. First of all, this filter is located third in the entire Spring Security filter chain and is very early.
  2. When the login request passes through this filter, it will first try to read the SecurityContext object from the SecurityContextRepository (this.repo above). This object stores the current user’s information. When logging in for the first time, here is actually No user information could be read.
  3. Store the read SecurityContext into the SecurityContextHolder. By default, the SecurityContext object is saved in the SecurityContextHolder through ThreadLocal. That is, in the subsequent processing process, the current request can be directly extracted from the SecurityContextHolder as long as it is in the same thread. to the currently logged in user information.
  4. The request continues backward execution.
  5. In the finally code block, the current request has ended. At this time, the SecurityContext is obtained again, the SecurityContextHolder is cleared to prevent memory leaks, and then the this.repo.saveContextmethod is called to save the currently logged in user object (actually saved in the HttpSession).
  6. When other requests arrive in the future, the current user’s information will be read when performing the previous step 2. During the subsequent processing of the request, when Spring Security needs to know the current user, it will automatically read the current user’s information from the SecurityContextHolder. User Info.

This is the general process of Spring Security certification.

However, after Spring Boot3, this filter was replaced by SecurityContextHolderFilter. Let’s take a look at a key logic of the SecurityContextHolderFilter filter:

private  void  doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
        throws ServletException, IOException {
    Supplier<SecurityContext> deferredContext = this .securityContextRepository.loadDeferredContext(request);
     try {
         this .securityContextHolderStrategy.setDeferredContext(deferredContext);
        chain.doFilter(request, response);
    }
    finally {
         this .securityContextHolderStrategy.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

Friends, you can see that the previous logic is basically the same. The difference is the code in finally. There is one less step in finally to save the SecurityContext to HttpSession.

Now it becomes clear that after the user logs in successfully, the user information is not saved to the HttpSession. As a result, when the next request arrives, the SecurityContext cannot be read from the HttpSession and stored in the SecurityContextHolder. During subsequent execution, Spring Security will It is assumed that the current user is not logged in.

That’s the cause of the problem!

If you find the reason, the problem will be solved.

3.3 Problem solving

First of all, the problem lies in the filter. It is not impossible to change the filter directly. However, since Spring Security abandoned the old solution during the upgrade process, we have struggled to write back the old solution. It seems that it is not possible. unreasonable.

In fact, Spring Security provides another entry point for modification. In the org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication method, the source code is as follows:

protected  void  successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult)  throws IOException, ServletException {
     SecurityContext  context  =  this .securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authResult);
    this .securityContextHolderStrategy.setContext(context);
     this .securityContextRepository.saveContext(context, request, response);
     this .rememberMeServices.loginSuccess(request, response, authResult);
     if ( this .eventPublisher != null ) {
         this .eventPublisher.publishEvent ( new  InteractiveAuthenticationSuccessEvent (authResult, this .getClass()));
    }
    this .successHandler.onAuthenticationSuccess(request, response, authResult);
}

This method is the callback method after the current user successfully logs in. Friends, you can see that in this callback method, there is a sentence this.securityContextRepository.saveContext(context, request, response);, which means that the current user information that successfully logged in is stored in the HttpSession.

In the current filter, the type of securityContextRepository is RequestAttributeSecurityContextRepository, which means that the SecurityContext is stored in the attributes of the current request. Obviously, after the current request ends, this data is gone. In Spring Security’s automated configuration class, the securityContextRepository attribute points to DelegatingSecurityContextRepository, which is a proxy storage. The proxy objects are RequestAttributeSecurityContextRepository and HttpSessionSecurityContextRepository, so by default, after the user logs in successfully, the logged in user data is stored here. Stored in HttpSessionSecurityContextRepository.

When we customize the login filter, the solution in the automated configuration is destroyed. The securityContextRepository object used here is really RequestAttributeSecurityContextRepository, so the system will think that the user is not logged in when he or she visits later.

Then the solution is simple, we only need to specify the value of the securityContextRepository attribute for the custom filter, as follows:

@Bean 
JsonLoginFilter jsonLoginFilter () {
     JsonLoginFilter  filter  =  new  JsonLoginFilter ();
    filter.setAuthenticationSuccessHandler((req,resp,auth)->{
        resp.setContentType( "application/json;charset=utf-8" );
         PrintWriter  out  = resp.getWriter();
         //Get the current user object that successfully logged in 
        User  user  = (User) auth.getPrincipal();
          user.setPassword( null );
         RespBean  respBean  = RespBean.ok( "Login successful" , user);
        out.write( new  ObjectMapper ().writeValueAsString(respBean));
    });
    filter.setAuthenticationFailureHandler((req,resp,e)->{
        resp.setContentType( "application/json;charset=utf-8" );
         PrintWriter  out  = resp.getWriter();
         RespBean  respBean  = RespBean.error( "Login failed" );
         if (e instanceof BadCredentialsException) {
            respBean.setMessage( "The username or password was entered incorrectly and login failed" );
        } else  if (e instanceof DisabledException) {
            respBean.setMessage( "Account disabled, login failed" );
        } else  if (e instanceof CredentialsExpiredException) {
            respBean.setMessage( "Password expired, login failed" );
        } else  if (e instanceof AccountExpiredException) {
            respBean.setMessage( "Account expired, login failed" );
        } else  if (e instanceof LockedException) {
            respBean.setMessage( "Account is locked, login failed" );
        }
        out.write( new  ObjectMapper ().writeValueAsString(respBean));
    });
    filter.setAuthenticationManager(authenticationManager());
    filter.setFilterProcessesUrl( "/login" );
    filter.setSecurityContextRepository( new  HttpSessionSecurityContextRepository ());
     return filter;
}

Friends, you can see that you just need to call the setSecurityContextRepository method to set it up.

The reason why this property did not need to be set before Spring Boot3.x was because although it was not saved here, it was finally saved in the SecurityContextPersistenceFilter filter.

So for the problem of custom login interface, the solution is similar:

@RestController 
public  class  LoginController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/doLogin") 
    public String doLogin ( @RequestBody User user, HttpSession session) {
         UsernamePasswordAuthenticationToken  unauthenticated  = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
         try {
             Authentication  authenticate  = authenticationManager.authenticate( unauthenticated);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
            return  "success" ;
        } catch (AuthenticationException e) {
             return  "error:" + e.getMessage();
        }
    }
}

Friends have seen that after successful login, the developer manually stores the data into the HttpSession. This ensures that when the next request arrives, valid data can be read from the HttpSession and stored in the SecurityContextHolder. .

Okay, there is a small problem in the replacement of old and new versions of Spring Boot. I hope everyone can learn something.