В Spring Security6 появился новый способ написания — большое изменение!
В Spring Security6 появился новый способ написания — большое изменение!

В последних версиях произошли некоторые изменения в методе записи конфигурации Spring Security. Многие распространенные методы были заброшены и будут удалены в будущем Spring Security7. Поэтому, основываясь на старой прошлогодней статье, Сонгге добавил некоторый новый контент. рекомендации друзей, которые используют Spring Security.

Далее я вместе с друзьями разберу все известные изменения, начиная с Spring Security 5.7 (соответствует Spring Boot 2.7).

1. WebSecurityConfigurerAdapter

Прежде всего, первый момент заключается в том, что срок действия WebSecurityConfigurerAdapter, который легче всего обнаружить друзьям, истек. В последней версии Spring Security6.1 этот класс полностью удален, и обойтись им больше невозможно.

Если быть точным, срок действия Spring Security истек. WebSecurityConfigurerAdapter в версии 5.7.0-M2. Причина истечения срока действия заключается в том, что чиновник хочет побудить разработчиков использовать конфигурацию безопасности на основе компонентов.

Так что же такое конфигурация безопасности на основе компонентов? Приведем несколько примеров:

Раньше мы настраивали SecurityFilterChain следующим образом:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

Затем оно будет изменено на следующее:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfiguration {

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

}

Если вы понимаете предыдущий метод написания, следующий код на самом деле легко понять, поэтому я не буду его объяснять слишком подробно.

Ранее мы настроили WebSecurity следующим образом:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

В дальнейшем его придется изменить на следующее:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfiguration {

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

}

Другой вопрос касается получения AuthenticationManager. Раньше вы могли получить этот компонент, переопределив метод родительского класса, аналогично следующему:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

В дальнейшем вы сможете создать этот Bean только самостоятельно, аналогично следующему:

Язык кода:javascript
копировать
@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

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

Конечно, AuthenticationManager также можно извлечь из HttpSecurity следующим образом:

Язык кода:javascript
копировать
@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();
    }

}

Это тоже способ.

Давайте рассмотрим конкретный пример.

Сначала мы создаем новый проект Spring Boot, представляем зависимости Web и Spring Security и обращаем внимание на выбор последней версии Spring Boot.

Далее мы предоставляем простой тестовый интерфейс, а именно:

Язык кода:javascript
копировать
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello Небольшой дождь в Цзяннань!";
    }
}

Друзья знают, что в Spring Security По умолчанию, пока зависимости добавлены, все интерфейсы нашего проекта защищены. Теперь запустите проект и получите доступ. /hello интерфейс, вам необходимо войти в систему, прежде чем вы сможете получить к нему доступ. Имя пользователя для входа: user, пароль генерируется случайным образом и находится в журнале запуска проекта.

Теперь наше первое требование — использовать пользовательского пользователя вместо пользователя по умолчанию, предоставленного системой. Нам нужно только зарегистрировать экземпляр UserDetailsService в контейнере Spring следующим образом:

Язык кода:javascript
копировать
@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("Небольшой дождь в Цзяннань").password("{noop}123").roles("admin").build());
        return users;
    }

}

Вот и все.

Конечно, мои текущие пользователи хранятся в памяти. Если ваши пользователи хранятся в базе данных, вам нужно только предоставить класс реализации интерфейса UserDetailsService и внедрить его в контейнер Spring. Это много раз упоминалось в vhr. видео раньше (в ответе 666 общедоступной учетной записи Backend есть видео-знакомство), поэтому я не буду здесь вдаваться в подробности.

Но если я скажу, что надеюсь /hello Доступ к этому интерфейсу можно получить анонимно, и я надеюсь, что этот анонимный доступ не пройдет Spring Security Цепочка фильтров, если она была раньше, можем переписать configure(WebSecurity) Способ настройки, но теперь вам нужно изменить способ игры:

Язык кода:javascript
копировать
@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("Небольшой дождь в Цзяннань").password("{noop}123").roles("admin").build());
        return users;
    }

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

}

ранее располагавшийся в configure(WebSecurity) Содержимое метода теперь находится в WebSecurityCustomizer Bean , просто напишите здесь настройки.

Что делать, если я также хочу настроить страницу входа, параметры и т. д.? Читайте дальше:

Язык кода:javascript
копировать
@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("Небольшой дождь в Цзяннань").password("{noop}123").roles("admin").build());
        return users;
    }

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

}

Нижний уровень Spring Security на самом деле представляет собой набор фильтров, поэтому наша предыдущая конфигурация в методе configure(HttpSecurity) фактически настраивает цепочку фильтров. Теперь что касается настройки цепочки фильтров, мы настраиваем цепочку фильтров, предоставляя компонент SecurityFilterChain. SecurityFilterChain — это интерфейс. Этот интерфейс имеет только один класс реализации, DefaultSecurityFilterChain. Первый параметр для построения DefaultSecurityFilterChain — это правило перехвата. пути нужно перехватывать. Второй параметр — цепочка фильтров. Здесь я дал пустую коллекцию, которая является нашей Spring Security. Все запросы будут перехвачены, а затем завершатся после прохождения пустой коллекции, что эквивалентно отказу от перехвата каких-либо запросов.

Перезапустите проект в это время, и вы обнаружите /hello Доступ к нему также возможен напрямую, поскольку этот путь не проходит через какой-либо фильтр.

На самом деле, я думаю, что новый способ написания более интуитивен, чем старый способ написания, и каждому легче понять основной рабочий механизм цепочки фильтров Spring Security.

Некоторые друзья скажут, что этот способ письма отличается от того, что я писал раньше! При такой конфигурации я не знаю, какие фильтры есть в Spring Security. На самом деле, если мы изменим метод записи, мы сможем настроить это как раньше:

Язык кода:javascript
копировать
@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("Небольшой дождь в Цзяннань").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();
    }

}

Написание этого способа на самом деле мало чем отличается от предыдущего способа написания.

2. Используйте Лямбда

В последней версии друзья обнаружили, что от многих распространенных методов отказались, как показано ниже:

В том числе привычный метод and(), используемый для соединения различных элементов конфигурации, теперь заброшен, и согласно официальному заявлению, этот метод будет полностью удален в Spring Security7.

Другими словами, вы больше не увидите такие конфигурации, как следующие:

Язык кода:javascript
копировать
@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() будет удален!

На самом деле, брат Сон считает, что удаление метода and — это хорошо. Многим новичкам требуется много времени, чтобы просто понять метод and.

Как вы можете видеть из комментариев к методу and выше, чиновник сейчас продвигает конфигурацию на основе Lambda для замены традиционной конфигурации цепочки, поэтому в будущем наш метод записи придется изменить на следующий:

Язык кода:javascript
копировать
@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();
    }
}

На самом деле, методы здесь не являются новыми, но некоторые друзья, возможно, раньше не использовали вышеуказанные методы для настройки и используют их для цепочки конфигураций. Но в дальнейшем к приведенной выше конфигурации по методу Лямбда придется потихоньку привыкать. Содержание конфигурации легко понять, объяснять, думаю, нечего.

3. Пользовательский логин в формате JSON.

Пользовательский вход в формате JSON также отличается от предыдущей версии.

3.1 Пользовательский вход в формате JSON

Друзья знают, что формат данных интерфейса входа в систему по умолчанию в Spring Security имеет форму «ключ-значение». Если мы хотим использовать формат JSON для входа в систему, нам необходимо настроить фильтр или интерфейс входа в систему. Далее Brother Song будет сначала. поговорите с ребятами из Сяо, покажите мне эти две разные формы входа.

3.1.1 Пользовательский фильтр входа в систему

Spring Security Фильтр по умолчанию для обработки данных входа: UsernamePasswordAuthenticationFilter, в этом фильтре система будет проходить request.getParameter(this.passwordParameter) Имя пользователя и пароль считываются. Очевидно, что для этого интерфейс должен передать параметры в форму. key-value。

Если вы хотите использовать параметры формата JSON для входа в систему, вам нужно повозиться с этим местом. Наши пользовательские фильтры следующие:

Язык кода:javascript
копировать
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //Получаем заголовок запроса и на его основе определяем тип параметра запроса
        String contentType = request.getContentType();
        if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {
            //Объясняем, что параметры запроса JSON
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            String username = null;
            String password = null;
            try {
                //Разбираем тело запроса JSON параметр
                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);
            }
            //Создаем токен Авторизации
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                    password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //Выполняем настоящую операцию из Авторизоваться
            Authentication auth = this.getAuthenticationManager().authenticate(authRequest);
            return auth;
        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}

Друзья, которые читали предыдущую серию статей Сонг Гэ по Spring Security, должны быть хорошо знакомы с этим кодом.

  1. Сначала мы получаем заголовок запроса и определяем формат параметров запроса на основе типа заголовка запроса.
  2. Если это параметр в формате JSON, он будет обработан в if. В противном случае это означает, что это параметр в формате ключ-значение, тогда мы можем просто вызвать метод родительского класса для обработки.
  3. Логика обработки параметров в формате JSON такая же, как и у пары «ключ-значение», с той лишь разницей, что параметры извлекаются разными способами.

Наконец, нам нужно настроить этот фильтр:

Язык кода:javascript
копировать
@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();
            //Получить текущий объект Авторизация успешно изпользователя
            User user = (User) auth.getPrincipal();
            user.setPassword(null);
            RespBean respBean = RespBean.ok("Авторизоватьсяуспех", 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("Авторизоватьсянеудача");            if (e instanceof BadCredentialsException) {
                respBean.setMessage("Имя пользователя или пароль были введены неверно, авторизоваться не удалось");
            } else if (e instanceof DisabledException) {
                respBean.setMessage("Аккаунт отключен, авторизация не удалась");
            } else if (e instanceof CredentialsExpiredException) {
                respBean.setMessage("Срок действия пароля истек, авторизоваться не удалось");
            } else if (e instanceof AccountExpiredException) {
                respBean.setMessage("Срок действия учетной записи истек, авторизоваться не удалось");
            } else if (e instanceof LockedException) {
                respBean.setMessage("Аккаунт заблокирован, авторизоваться не удалось");
            }
            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 {
        //Открываем фильтр из Конфигурация
        http.authorizeHttpRequests()
                //Любой запрос должен быть аутентифицирован перед доступом
                .anyRequest().authenticated()
                .and()
                //Открываем форму Авторизоваться. После открытия автоматически отобразится страница Конфигурация Авторизоваться, Авторизоватьсяинтерфейс и другая информация.
                .formLogin()
                //и Авторизоваться Связанныйиз URL Все адреса разрешены
                .permitAll()
                .and()
                //закрытие csrf Механизм защиты по существу от Spring Security Удален из цепочки фильтров CsrfFilter
                .csrf().disable();
        http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

}

Здесь нужно настроить компонент JsonLoginFilter и добавить его в цепочку фильтров Spring Security.

До Spring Boot3 (до Spring Security6) приведенный выше код мог реализовать вход в формате JSON.

Однако, начиная с Spring Boot 3, этот код имеет некоторые недостатки, и больше невозможно войти в систему напрямую с помощью JSON. Конкретные причины анализируются ниже Сонгом.

3.1.2 Пользовательский интерфейс входа в систему

Другой способ настроить вход в формате JSON — напрямую настроить интерфейс входа, как показано ниже:

Язык кода:javascript
копировать
@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();
        }
    }
}

Здесь напрямую настраивается интерфейс входа в систему, а параметры запроса передаются в виде JSON. После получения имени пользователя и пароля вызовите метод AuthenticationManager#authenticate для аутентификации. После успешной аутентификации информация о аутентифицированном пользователе сохраняется в SecurityContextHolder.

Наконец, просто настройте интерфейс входа в систему:

Язык кода:javascript
копировать
@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()
                //выражать /doLogin Доступ к этому адресу можно получить напрямую, не используя Авторизоваться.
                .requestMatchers("/doLogin").permitAll()
                .anyRequest().authenticated().and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();
    }
}

Это также можно рассматривать как решение с использованием параметров формата JSON. До Spring Boot3 (до Spring Security6) с вышеуказанным решением проблем не было.

Начиная с Spring Boot3 (Spring Security6), оба вышеперечисленных решения имеют некоторые недостатки.

Конкретная производительность:При звонке Авторизоваться Авторизоваться успешно,Затем посетите другие страницы в системе,Вы вернетесь на страницу Авторизоваться.,Инструкция по посещению Авторизоваться из другого интерфейса,Система не знает, что вы уже прошли Авторизацию.

3.2 Анализ причин

Причина вышеуказанной проблемы в основном заключается в том, что изменился один из фильтров в цепочке фильтров Spring Security:

До Spring Boot3 в цепочке фильтров Spring Security был фильтр SecurityContextPersistenceFilter. Этот фильтр был заброшен в Spring Boot2.7.x, но все еще используется. В Spring Boot3 он был удален из фильтра безопасности Spring. цепочку и заменен фильтром SecurityContextHolderFilter.

Два решения для входа в систему JSON, представленные моими друзьями в первом разделе, могут работать в Spring Boot 2.x, но не могут работать в Spring Boot 3.x, что вызвано изменением этого фильтра.

Итак, далее давайте проанализируем различия между этими двумя фильтрами.

Давайте сначала посмотрим на основную логику SecurityContextPersistenceFilter:

Язык кода:javascript
копировать
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());
 }
}

Я разместил здесь только некоторый ключевой код ядра:

  1. Во-первых, этот фильтр находится третьим во всей цепочке фильтров Spring Security и является очень ранним.
  2. Когда запрос Авторизоваться проходит через этот фильтр, он сначала пытается SecurityContextRepository (выше) читайте из этого репо) SecurityContext объект,Этот объект содержит информацию о текущем пользователе.,Первый раз Авторизоватьсяиз,На самом деле здесь нельзя прочитать никакую информацию о пользователе.
  3. будет прочитано SecurityContext внести депозит в SecurityContextHolder По умолчанию в SecurityContextHolder. проходить ThreadLocal сохранить SecurityContext объект, то есть при последующей обработке текущего запроса существовать, пока существует в том же потоке, можно напрямую от SecurityContextHolder Извлеките текущую информацию об авторизованном пользователе.
  4. Запрос продолжает обратное выполнение.
  5. существовать finally В блоке кода текущий запрос завершился, и SecurityContext и очистите его. SecurityContextHolder Чтобы предотвратить утечки памяти, вызовите this.repo.saveContext Метод держать текущий Авторизоваться пользовательский объект (фактически держать HttpSession середина).
  6. Когда в будущем поступят другие запросы, выполните предыдущий 2 При этом считывается текущая информация о пользователе во время последующей обработки запроса существования Spring. Security Когда вам понадобится узнать текущего пользователя, вы автоматически перейдете SecurityContextHolder Прочтите текущую информацию о пользователе.

Это общий процесс сертификации Spring Security.

Однако после Spring Boot3 этот фильтр был заменен на SecurityContextHolderFilter. Давайте посмотрим на ключевую логику фильтра SecurityContextHolderFilter:

Язык кода:javascript
копировать
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);
 }
}

Друзья, вы можете видеть, что предыдущая логика в основном такая же, разница в коде, наконец. Наконец, для сохранения SecurityContext в HttpSession стало на один шаг меньше.

Теперь становится ясно, что после успешного входа пользователя информация о пользователе не сохраняется в HttpSession. В результате при поступлении следующего запроса SecurityContext не может быть прочитан из HttpSession и сохранен в SecurityContextHolder. Spring Security будет Предполагается, что текущий пользователь не вошел в систему.

Вот в чем причина проблемы!

Если вы найдете причину, проблема будет решена.

3.3 Решение проблем

Прежде всего, проблема заключается в фильтре. Невозможно изменить фильтр напрямую. Однако, поскольку Spring Security отказалась от старого решения в процессе обновления, мы изо всех сил пытались записать старое решение. не возможно.

Фактически Spring Security предоставляет еще одну точку входа для модификации. В методе org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication исходный код выглядит следующим образом:

Язык кода:javascript
копировать
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.securityContextRepository.saveContext(context, request, response);,Это означает, что текущий Авторизация успешно из информации пользователя вносит депозит в HttpSession середина.

В текущем фильтре тип SecurityContextRepository — RequestAttributeSecurityContextRepository, что означает, что SecurityContext сохраняется в атрибутах текущего запроса. Очевидно, эти данные исчезают после завершения текущего запроса. В классе автоматической конфигурации Spring Security атрибут securityContextRepository указывает на DelegatingSecurityContextRepository, который является прокси-хранилищем. Прокси-объектами являются RequestAttributeSecurityContextRepository и HttpSessionSecurityContextRepository, поэтому по умолчанию после успешного входа пользователя в систему сохраняются здесь. HttpSessionSecurityContextRepository.

Когда мы настраиваем фильтр входа, решение в автоматической конфигурации уничтожается. Используемый здесь объект SecurityContextRepository на самом деле является RequestAttributeSecurityContextRepository, поэтому система будет думать, что пользователь не вошел в систему, когда пользователь посетит его позже.

Тогда решение простое, нам нужно только указать значение атрибута securityContextRepository для пользовательского фильтра следующим образом:

Язык кода:javascript
копировать
@Bean
JsonLoginFilter jsonLoginFilter() {
    JsonLoginFilter filter = new JsonLoginFilter();
    filter.setAuthenticationSuccessHandler((req,resp,auth)->{
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        //Получить текущий объект Авторизация успешно изпользователя
        User user = (User) auth.getPrincipal();
          user.setPassword(null);
        RespBean respBean = RespBean.ok("Авторизоватьсяуспех", 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("Авторизоватьсянеудача");        if (e instanceof BadCredentialsException) {
            respBean.setMessage("Имя пользователя или пароль были введены неверно, авторизоваться не удалось");
        } else if (e instanceof DisabledException) {
            respBean.setMessage("Аккаунт отключен, авторизация не удалась");
        } else if (e instanceof CredentialsExpiredException) {
            respBean.setMessage("Срок действия пароля истек, авторизоваться не удалось");
        } else if (e instanceof AccountExpiredException) {
            respBean.setMessage("Срок действия учетной записи истек, авторизоваться не удалось");
        } else if (e instanceof LockedException) {
            respBean.setMessage("Аккаунт заблокирован, авторизоваться не удалось");
        }
        out.write(new ObjectMapper().writeValueAsString(respBean));
    });
    filter.setAuthenticationManager(authenticationManager());
    filter.setFilterProcessesUrl("/login");
    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
    return filter;
}

Друзья, как видите, для настройки вам достаточно вызвать метод setSecurityContextRepository.

Причина, по которой это свойство не нужно было устанавливать до Spring Boot3.x, заключалась в том, что, хотя оно не было сохранено здесь, оно было сохранено в фильтре SecurityContextPersistenceFilter.

Итак, для проблемы пользовательского интерфейса входа решение аналогично:

Язык кода:javascript
копировать
@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();
        }
    }
}

Друзья видели, что после успешного входа в систему разработчик вручную сохраняет данные в HttpSession. Это гарантирует, что при поступлении следующего запроса действительные данные могут быть прочитаны из HttpSession и сохранены в SecurityContextHolder.

Ладно, есть небольшая проблема в замене старых и новых версий Spring Boot, надеюсь, каждый сможет чему-то научиться.

boy illustration
Междоменный доступ к конфигурации nginx не может вступить в силу. Междоменный доступ к странице_Page
boy illustration
Как написать текстовый контент на php
boy illustration
PHP добавляет текстовый водяной знак или водяной знак изображения к изображениям – метод инкапсуляции
boy illustration
Интерпретация быстрой таблицы (TLB)
boy illustration
Интерфейс WeChat API (полный) — оплата WeChat/красный конверт WeChat/купон WeChat/магазин WeChat/JSAPI
boy illustration
Преобразование Java-объекта в json string_complex json-строки в объект
boy illustration
Примените сегментацию слов jieba (версия Java) и предоставьте пакет jar
boy illustration
matinal: Самый подробный анализ управления разрешениями во всей сети SAP. Все управление разрешениями находится здесь.
boy illustration
Коротко расскажу обо всем процессе работы алгоритма сборки мусора G1 --- Теоретическая часть -- Часть 1
boy illustration
[Спецификация] Результаты и исключения возврата интерфейса SpringBoot обрабатываются единообразно, поэтому инкапсуляция является элегантной.
boy illustration
Интерпретация каталога веб-проекта Flask
boy illustration
Что такое подробное объяснение файла WSDL_wsdl
boy illustration
Как запустить большую модель ИИ локально
boy illustration
Подведение итогов десяти самых популярных веб-фреймворков для Go
boy illustration
5 рекомендуемых проектов CMS с открытым исходным кодом на базе .Net Core
boy illustration
Java использует httpclient для отправки запросов HttpPost (отправка формы, загрузка файлов и передача данных Json)
boy illustration
Руководство по развертыванию Nginx в Linux (Centos)
boy illustration
Интервью с Alibaba по Java: можно ли использовать @Transactional и @Async вместе?
boy illustration
Облачный шлюз Spring реализует примеры балансировки нагрузки и проверки входа в систему.
boy illustration
Используйте Nginx для решения междоменных проблем
boy illustration
Произошла ошибка, когда сервер веб-сайта установил соединение с базой данных. WordPress предложил решение проблемы с установкой соединения с базой данных... [Легко понять]
boy illustration
Новый адрес java-библиотеки_16 топовых Java-проектов с открытым исходным кодом, достойных вашего внимания! Обязательно к просмотру новичкам
boy illustration
Лучшие практики Kubernetes для устранения несоответствий часовых поясов внутри контейнеров
boy illustration
Введение в проект удаления водяных знаков из коротких видео на GitHub Douyin_TikTok_Download_API
boy illustration
Весенние аннотации: подробное объяснение @Service!
boy illustration
Пожалуйста, не используйте foreach для пакетной вставки в MyBatis. Для 5000 фрагментов данных потребовалось 14 минут. .
boy illustration
Как создать проект Node.js с помощью npm?
boy illustration
Mybatis-plus использует typeHandler для преобразования объединенных строк String в списки списков.
boy illustration
Не удалось установить программное обеспечение Mitsubishi. Возможно, возникла проблема с реестром.
boy illustration
Разрешение ошибок проекта SpringBoot 3 mybatis-plus: org.apache.ibatis.binding.BindingException: неверный оператор привязки