精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Spring Security6 全新寫法,大變樣!

開發 前端
小伙伴們看到,在登錄成功之后,開發者自己手動將數據存入到 HttpSession 中,這樣就能確保下個請求到達的時候,能夠從 HttpSession 中讀取到有效的數據存入到 SecurityContextHolder 中了。

Spring Security 在最近幾個版本中配置的寫法都有一些變化,很多常見的方法都廢棄了,并且將在未來的 Spring Security7 中移除,因此松哥在去年舊文的基礎之上,又補充了一些新的內容,重新發一下,供各位使用 Spring Security 的小伙伴們參考。

接下來,我把從 Spring Security5.7 開始(對應 Spring Boot2.7 開始),各種已知的變化都來和小伙伴們梳理一下。

1. WebSecurityConfigurerAdapter

圖片圖片

首先第一點,就是各位小伙伴最容易發現的 WebSecurityConfigurerAdapter 過期了,在目前最新的 Spring Security6.1 中,這個類已經完全被移除了,想湊合著用都不行了。

準確來說,Spring Security 是在 5.7.0-M2 這個版本中將 WebSecurityConfigurerAdapter 過期的,過期的原因是因為官方想要鼓勵各位開發者使用基于組件的安全配置。

那么什么是基于組件的安全配置呢?我們來舉幾個例子:

以前我們配置 SecurityFilterChain 的方式是下面這樣:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

那么以后就要改為下面這樣了:

@Configuration
public class SecurityConfiguration {

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

}

如果懂之前的寫法的話,下面這個代碼其實是很好理解的,我就不做過多解釋了,不過還不懂 Spring Security 基本用法的小伙伴,可以在公眾號后臺回復 ss,有松哥寫的教程。

以前我們配置 WebSecurity 是這樣:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

以后就得改成下面這樣了:

@Configuration
public class SecurityConfiguration {

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

}

另外還有一個就是關于 AuthenticationManager  的獲取,以前可以通過重寫父類的方法來獲取這個 Bean,類似下面這樣:

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

以后就只能自己創建這個 Bean 了,類似下面這樣:

@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

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

當然,也可以從 HttpSecurity 中提取出來 AuthenticationManager,如下:

@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 選擇最新版。

接下來我們提供一個簡單的測試接口,如下:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello 江南一點雨!";
    }
}

小伙伴們知道,在 Spring Security 中,默認情況下,只要添加了依賴,我們項目的所有接口就已經被統統保護起來了,現在啟動項目,訪問 /hello 接口,就需要登錄之后才可以訪問,登錄的用戶名是 user,密碼則是隨機生成的,在項目的啟動日志中。

現在我們的第一個需求是使用自定義的用戶,而不是系統默認提供的,這個簡單,我們只需要向 Spring 容器中注冊一個 UserDetailsService 的實例即可,像下面這樣:

@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 有視頻介紹),這里就不再贅述了。

但是假如說我希望 /hello 這個接口能夠匿名訪問,并且我希望這個匿名訪問還不經過 Spring Security 過濾器鏈,要是在以前,我們可以重寫 configure(WebSecurity) 方法進行配置,但是現在,得換一種玩法:

@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 中,該配置的東西寫在這里就可以了。

那如果我還希望對登錄頁面,參數等,進行定制呢?繼續往下看:

@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 Bean 來配置過濾器鏈,SecurityFilterChain 是一個接口,這個接口只有一個實現類 DefaultSecurityFilterChain,構建 DefaultSecurityFilterChain 的第一個參數是攔截規則,也就是哪些路徑需要攔截,第二個參數則是過濾器鏈,這里我給了一個空集合,也就是我們的 Spring Security 會攔截下所有的請求,然后在一個空集合中走一圈就結束了,相當于不攔截任何請求。

此時重啟項目,你會發現 /hello 也是可以直接訪問的,就是因為這個路徑不經過任何過濾器。

其實我覺得目前這中新寫法比以前老的寫法更直觀,更容易讓大家理解到 Spring Security 底層的過濾器鏈工作機制。

有小伙伴會說,這寫法跟我以前寫的也不一樣呀!這么配置,我也不知道 Spring Security 中有哪些過濾器,其實,換一個寫法,我們就可以將這個配置成以前那種樣子:

@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. 使用 Lambda

在最新版中,小伙伴們發現,很多常見的方法廢棄了,如下圖:

包括大家熟悉的用來連接各個配置項的 and() 方法現在也廢棄了,并且按照官方的說法,將在 Spring Security7 中徹底移除該方法。

也就是說,你以后見不到類似下面這樣的配置了:

@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 的配置來代替傳統的鏈式配置,所以以后我們的寫法就得改成下面這樣啦:

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

其實,這里的幾個方法倒不是啥新方法,只不過有的小伙伴可能之前不太習慣用上面這幾個方法進行配置,習慣于鏈式配置。可是往后,就得慢慢習慣上面這種按照 Lambda 的方式來配置了,配置的內容倒很好理解,我覺得沒啥好解釋的。

3. 自定義 JSON 登錄

自定義 JSON 登錄也和之前舊版不太一樣了。

3.1 自定義 JSON 登錄

小伙伴們知道,Spring Security 中默認的登錄接口數據格式是 key-value 的形式,如果我們想使用 JSON 格式來登錄,那么就必須自定義過濾器或者自定義登錄接口,下面松哥先來和小伙伴們展示一下這兩種不同的登錄形式。

3.1.1 自定義登錄過濾器

Spring Security 默認處理登錄數據的過濾器是 UsernamePasswordAuthenticationFilter,在這個過濾器中,系統會通過 request.getParameter(this.passwordParameter) 的方式將用戶名和密碼讀取出來,很明顯這就要求前端傳遞參數的形式是 key-value。

如果想要使用 JSON 格式的參數登錄,那么就需要從這個地方做文章了,我們自定義的過濾器如下:

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 中進行處理,否則說明是 key-value 形式的參數,那么我們就調用父類的方法進行處理即可。
  3. JSON 格式的參數的處理邏輯和 key-value 的處理邏輯是一致的,唯一不同的是參數的提取方式不同而已。

最后,我們還需要對這個過濾器進行配置:

@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 的 Bean,并將之添加到 Spring Security 過濾器鏈中即可。

在 Spring Boot3 之前(Spring Security6 之前),上面這段代碼就可以實現 JSON 登錄了。

但是從 Spring Boot3 開始,這段代碼有點瑕疵了,直接用已經無法實現 JSON 登錄了,具體原因松哥下文分析。

3.1.2 自定義登錄接口

另外一種自定義 JSON 登錄的方式是直接自定義登錄接口,如下:

@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 中。

最后再配一下登錄接口就行了:

@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 Security 過濾器鏈中移除了,取而代之的是一個名為 SecurityContextHolderFilter 的過濾器。

在第一小節和小伙伴們介紹的兩種 JSON 登錄方案在 Spring Boot2.x 中可以運行在 Spring Boot3.x 中無法運行,就是因為這個過濾器的變化導致的。

所以接下來我們就來分析一下這兩個過濾器到底有哪些區別。

先來看 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());
 }
}

我這里只貼出來了一些關鍵的核心代碼:

  1. 首先,這個過濾器位于整個 Spring Security 過濾器鏈的第三個,是非常靠前的。
  2. 當登錄請求經過這個過濾器的時候,首先會嘗試從 SecurityContextRepository(上文中的 this.repo)中讀取到 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 過濾器的一個關鍵邏輯:

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);
 }
}

小伙伴們看到,前面的邏輯基本上還是一樣的,不一樣的是 finally 中的代碼,finally 中少了一步向 HttpSession 保存 SecurityContext 的操作。

這下就明白了,用戶登錄成功之后,用戶信息沒有保存到 HttpSession,導致下一次請求到達的時候,無法從 HttpSession 中讀取到 SecurityContext 存到 SecurityContextHolder 中,在后續的執行過程中,Spring Security 就會認為當前用戶沒有登錄。

這就是問題的原因!

找到原因,那么問題就好解決了。

3.3 問題解決

首先問題出在了過濾器上,直接改過濾器倒也不是不可以,但是,既然 Spring Security 在升級的過程中拋棄了之前舊的方案,我們又費勁的把之前舊的方案寫回來,好像也不合理。

其實,Spring Security 提供了另外一個修改的入口,在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication 方法中,源碼如下:

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 屬性的值就可以了,如下:

@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 過濾器中保存了。

那么對于自定義登錄接口的問題,解決思路也是類似的:

@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 新舊版本交替中,一個小小的問題,希望小伙伴們能夠有所收獲。

責任編輯:武曉燕 來源: 江南一點雨
相關推薦

2021-09-29 09:03:20

Windows 11操作系統微軟

2021-06-16 18:05:17

Windows 10Windows操作系統

2012-03-08 22:26:08

Windows Pho

2021-01-26 09:01:03

MozillaFirefoxProton

2017-05-16 14:16:39

Firefox瀏覽器Mozilla

2021-07-13 05:19:51

Windows 11操作系統微軟

2017-11-22 07:25:02

Windows全局搜索功能

2021-10-25 05:34:15

Windows 11操作系統微軟

2021-01-06 07:54:17

Windows 10Windows操作系統

2019-08-26 13:25:50

Android 10安卓谷歌

2021-10-25 20:20:00

微軟Windows 11Windows

2021-03-20 07:21:19

Windows10 操作系統微軟

2021-10-27 06:03:02

macOS macOS Monte安全更新

2021-02-22 07:32:49

Windows10操作系統21H2

2017-10-31 09:59:15

互聯網商業數據

2024-02-02 09:08:32

2025-09-09 16:56:42

AI機器人特斯拉

2020-04-01 16:37:27

Windows 10Windows微軟

2018-12-07 10:55:37

微軟Windows操作系統

2021-04-21 16:34:32

社區團購電商新零售
點贊
收藏

51CTO技術棧公眾號

神马久久桃色视频| 台湾佬成人网| 136福利精品导航| 久久久久久久久久久黄色| 欧美乱大交xxxxx| 国产日韩成人内射视频| 青青操国产视频| 韩国理伦片久久电影网| 91丝袜国产在线播放| 久久久免费观看视频| 久久久精品视频国产| av在线免费一区| 日韩有码中文字幕在线| 一区二区日韩电影| 亚洲jizzjizz日本少妇| 日本激情视频一区二区三区| 精品欧美一区二区三区在线观看 | 综合天堂久久久久久久| 色婷婷一区二区三区四区| 国产九色精品| 在线免费观看毛片| 果冻天美麻豆一区二区国产| 一个色综合av| 免费看成人午夜电影| 国产微拍精品一区| 欧美一性一交| 色综合久久久久久久久| 伊人久久在线观看| 国产91久久久| 国产精品久久久亚洲一区| 日韩国产高清视频在线| 精品99在线视频| 国产三级在线| 久久爱www久久做| www.久久久久久.com| 香港日本韩国三级网站| 国产在线更新| 粉嫩欧美一区二区三区高清影视| 性日韩欧美在线视频| 国产激情视频网站| 少妇在线看www| 国产视频在线观看一区二区三区| 国产精品极品美女在线观看免费 | 亚洲欧美另类图片小说| 91探花福利精品国产自产在线| 翔田千里88av中文字幕| 国产亚洲成av人片在线观黄桃| 欧美精三区欧美精三区| 欧美人与动牲交xxxxbbbb| 欧美天堂在线视频| 日韩国产精品91| 久久不射电影网| www国产视频| 成人午夜在线| 亚洲成a人片在线不卡一二三区| 鲁丝一区鲁丝二区鲁丝三区| 少妇av一区二区| 青青草91视频| 久久久久久伊人| 亚洲午夜精品久久久久久高潮| 91麻豆精品一二三区在线| 午夜视频一区在线观看| 国产美女主播在线播放| 中文日本在线观看| 国产iv一区二区三区| 99久热re在线精品视频| 中文字幕视频免费观看| 伊人成人在线视频| 在线精品国产成人综合| 国产激情视频网站| 九九视频免费观看视频精品| 欧美一区二区黄色| 亚洲 中文字幕 日韩 无码| 少妇一区视频| 欧美日韩国产小视频| 欧美日韩二三区| h片在线播放| 欧美激情自拍偷拍| 久久亚洲免费| 岛国在线视频免费看| 国产精品区一区二区三| 美日韩精品免费| 国产中文字幕在线播放| eeuss国产一区二区三区| 国产在线拍偷自揄拍精品| 中文字幕黄色片| 国产一区二区三区自拍| 日韩三级成人av网| 九九九免费视频| 999国产精品| 亚洲人成网在线播放| 毛茸茸free性熟hd| 欧美视频精品全部免费观看| 欧美日韩一级二级| 亚洲精品中文字幕无码蜜桃| 99爱在线观看| 亚洲国产成人tv| 国产青草视频在线观看| 秋霞影院午夜丰满少妇在线视频| 久久久777精品电影网影网| 一区二区三区偷拍| 日本www在线观看| 精品久久久久久久久久ntr影视| 免费久久久久久| 免费在线午夜视频| 亚洲v日本v欧美v久久精品| 高清一区二区视频| 日韩一区二区三区免费视频| 色婷婷综合五月| 超碰中文字幕在线观看| 国产一区不卡| 色老头一区二区三区| 日韩伦人妻无码| 国内精品免费**视频| 91久热免费在线视频| 午夜小视频在线播放| 久久伊99综合婷婷久久伊| 国产在线精品一区二区三区》| 国产综合在线播放| 国产精品蜜臀av| 国产精品无码av在线播放| 成人国产精品久久| 日韩欧美一级在线播放| 亚洲日本久久久| 欧美成人激情| 国产精品久久久久久超碰 | 亚洲精品77777| 国产欧美日韩一级| 成人动漫视频在线观看完整版| 8888四色奇米在线观看| 欧美日韩亚洲国产一区| 精品www久久久久奶水| youjizzjizz亚洲| 日韩高清人体午夜| 日本在线视频免费观看| 国产盗摄一区二区三区| 国语精品免费视频| 久草福利在线| 欧美性开放视频| 久热精品在线播放| 永久免费精品视频| 美女av一区二区| 日本在线播放视频| 日本欧美大码aⅴ在线播放| 狼狼综合久久久久综合网| 毛片在线网站| 亚洲精品资源在线| 国产三级精品三级观看| 亚洲网站视频| 成人黄色片视频网站| 天堂av最新在线| 色噜噜久久综合| 在线观看视频在线观看| 亚洲电影在线一区二区三区| 国内偷自视频区视频综合| 午夜精品久久久久久久爽| 久久尤物电影视频在线观看| 午夜精品久久久久久久无码| 日韩av中文字幕一区| 欧美一区二区三区图| a级片免费视频| 91免费国产在线观看| 国产成人无码精品久久久性色| 全国精品免费看| 国产成人在线一区| 国产91免费在线观看| 婷婷中文字幕综合| 波多野结衣片子| 国产中文一区| 精品国产乱码久久久久久88av| 涩涩视频在线播放| 在线视频日韩精品| 天天操天天干视频| 久久天天做天天爱综合色| 久久久国产欧美| 久久久久久久久久久久久久久久久久| 亚洲一区二区三区在线免费观看| 久久青青色综合| 在线播放日韩导航| 天天躁夜夜躁狠狠是什么心态| 蜜臀久久99精品久久久久久9| 国内一区在线| 欧美不卡高清一区二区三区| 久久精视频免费在线久久完整在线看| 日韩精品一区二区亚洲av| 久久婷婷一区二区三区| 超碰人人草人人| 北条麻妃国产九九九精品小说| 5252色成人免费视频| jzzjzzjzz亚洲成熟少妇| 欧美一级一区二区| 精品人妻一区二区三区免费看 | 国产精品123区| 黄色片视频在线免费观看| www.豆豆成人网.com| 青青在线视频一区二区三区| 视频在线观看你懂的| 亚洲五月六月丁香激情| 手机看片国产精品| 亚洲国产精品久久久久蝴蝶传媒| 国产欧美日韩在线播放| 日韩欧乱色一区二区三区在线 | 日韩丰满少妇无码内射| 西西裸体人体做爰大胆久久久| 国产伦精品一区二区三区四区视频 | 人妻少妇精品无码专区| 欧美三日本三级三级在线播放| 久久久无码精品亚洲国产| 精东粉嫩av免费一区二区三区| www精品久久| 日韩精品dvd| 国产欧洲精品视频| 涩涩视频在线| 久久免费成人精品视频| 免费人成在线观看播放视频 | 99青草视频在线播放视| 亚洲国产婷婷香蕉久久久久久 | 色8久久精品久久久久久蜜| 中文字幕av久久爽av| 欧美高清在线一区| 亚洲免费av一区| 老司机精品视频网站| 亚洲精品中字| 日本久久伊人| 韩国v欧美v日本v亚洲| 成人在线播放免费观看| 在线中文字幕日韩| 精品三级久久久久久久电影聊斋| 亚洲高清福利视频| 亚洲图片在线视频| 一区二区成人在线| 男人与禽猛交狂配| 成人国产精品免费| 欧美日韩一区二区在线免费观看| 伊人久久亚洲热| 欧美黑人在线观看| 中文精品电影| 欧美精品久久96人妻无码| 粉嫩av一区二区| 91精品久久久久久蜜桃| 交100部在线观看| 伊人青青综合网站| 牛牛热在线视频| 91精品国产综合久久久蜜臀图片| 波多野结衣在线观看一区| 亚洲色图在线看| 国产麻豆xxxvideo实拍| 国产成人午夜视频| 不许穿内裤随时挨c调教h苏绵| 视频一区二区欧美| 草草草在线视频| 人禽交欧美网站| 日本高清久久久| 国产一区中文字幕| 国产成人a亚洲精v品无码| 国产日本精品| 超碰97人人射妻| 丝袜亚洲另类丝袜在线| 9久久婷婷国产综合精品性色| 蜜桃久久精品一区二区| 国产精品久久久久久久av福利| 亚洲在线视频| 成人黄色片视频| 免费亚洲电影在线| 免费网站在线观看黄| 高清免费成人av| 懂色av粉嫩av蜜乳av| 国产一区二区三区视频在线播放| 亚洲男人天堂2021| 日韩精品一二区| 午夜国产一区二区三区| 国产一区二区0| 日本一区二区在线免费观看| 久久久久久久精| 九九这里只有精品视频| 亚洲一二三四区| 亚洲国产av一区二区三区| 午夜精品爽啪视频| 精品人妻一区二区三区潮喷在线 | 日本天堂在线观看| 国产视频综合在线| 在线观看精品一区二区三区| 欧美成人免费在线视频| 日韩脚交footjobhd| 国产精品一区二区三区成人| 成人福利视频| 久久久噜久噜久久综合| 不卡一二三区| 成人午夜在线视频一区| 欧美日韩视频免费看| 高清视频一区二区三区| 亚洲黑人在线| 久久精品国产美女| 色先锋久久影院av| 一区二区三区偷拍| 午夜在线精品偷拍| 少妇性l交大片7724com| 91美女福利视频| 欧美极品aaaaabbbbb| 欧美在线你懂的| 中文无码av一区二区三区| 精品欧美一区二区在线观看 | 福利一区福利二区| 国产视频不卡在线| 亚洲国产精品高清| 国产亚洲色婷婷久久99精品| 欧美性受极品xxxx喷水| 国产情侣免费视频| 欧美日韩一区二区电影| 少妇一级淫片免费看| 久久成人亚洲精品| 欧美三级网址| 国偷自产av一区二区三区小尤奈| 希岛爱理一区二区三区| 十八禁视频网站在线观看| 国产91精品精华液一区二区三区 | 黄色另类av| 伊人成人222| 日本一区二区在线不卡| 国产三级av片| 亚洲成人精品视频| 怡红院av在线| 7777精品视频| 免费观看成人性生生活片| 国产精品v欧美精品∨日韩| 小处雏高清一区二区三区| 一级黄色香蕉视频| 国产亚洲精品bt天堂精选| 亚洲国产成人精品激情在线| 精品国产伦理网| 青青草观看免费视频在线| 伊人一区二区三区久久精品| 最近高清中文在线字幕在线观看1| 国产精品福利无圣光在线一区| 久久久久影视| 午夜一区二区三区| 欧美日本不卡| 免费在线激情视频| 99精品国产99久久久久久白柏| 精品人伦一区二区| 色偷偷88欧美精品久久久| 蜜桃免费在线| 国产成人精品视频| 欧美老女人另类| 国产精品一二三在线观看| 精品在线观看视频| 日韩精品一区二区亚洲av性色| 555夜色666亚洲国产免| 成人短视频在线观看| 91久久国产综合久久蜜月精品| 欧美激情四色| 久章草在线视频| 久久久.com| 伊人网视频在线| 久久亚洲精品网站| 欧美久久亚洲| 欧美精品久久久久久久久久久| 成人深夜在线观看| 国产视频123区| 欧美午夜电影在线| 玖玖综合伊人| 成人妇女淫片aaaa视频| 欧美成人日本| 成人网站免费观看| 欧美一a一片一级一片| 免费黄色网址在线观看| αv一区二区三区| 国产一区二区精品| 又色又爽的视频| 欧美刺激午夜性久久久久久久| 超碰97国产精品人人cao| 91老司机在线| 在线日韩欧美| 深夜做爰性大片蜜桃| 亚洲国产精品一区二区www| 日本人妖在线| 国产精品中文久久久久久久| 欧美国产专区| 性欧美13一14内谢| 51精品秘密在线观看| 国产在线美女| 亚洲福利av在线| 成人永久免费视频| 波多野结衣在线电影| 欧美日韩国产成人在线观看| 亚洲精品3区| 国产精品50p| 国产精品免费视频网站| 亚洲精品一区二区三区新线路| 久久久国产精品视频| 任你躁在线精品免费| 欧美一级xxxx| 欧美丝袜第一区| 91精品久久久久久粉嫩| 欧洲精品一区色| 先锋影音久久久| 亚洲成人生活片| 国产亚洲欧洲黄色| 成人国产精品一区二区免费麻豆| 国产一二三区在线播放|