最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring Security6 最新版配置及實(shí)現(xiàn)動(dòng)態(tài)權(quán)限管理

 更新時(shí)間:2024年03月05日 14:34:31   作者:Young丶  
Spring Security 在最近幾個(gè)版本中配置的寫法都有一些變化,很多常見(jiàn)的方法都廢棄了,并且將在未來(lái)的 Spring Security7 中移除,因此又補(bǔ)充了一些新的內(nèi)容,重新發(fā)一下,供各位使用 Spring Security 的小伙伴們參考,需要的朋友可以參考下

Spring Security 在最近幾個(gè)版本中配置的寫法都有一些變化,很多常見(jiàn)的方法都廢棄了,并且將在未來(lái)的 Spring Security7 中移除,因此又補(bǔ)充了一些新的內(nèi)容,重新發(fā)一下,供各位使用 Spring Security 的小伙伴們參考。

接下來(lái),我把從 Spring Security5.7 開(kāi)始(對(duì)應(yīng) Spring Boot2.7 開(kāi)始),各種已知的變化都來(lái)和小伙伴們梳理一下。

1. WebSecurityConfigurerAdapter

首先第一點(diǎn),就是各位小伙伴最容易發(fā)現(xiàn)的 WebSecurityConfigurerAdapter 過(guò)期了,在目前最新的 Spring Security6.1 中,這個(gè)類已經(jīng)完全被移除了,想湊合著用都不行了。

準(zhǔn)確來(lái)說(shuō),Spring Security 是在 5.7.0-M2 這個(gè)版本中將 WebSecurityConfigurerAdapter 過(guò)期的,過(guò)期的原因是因?yàn)楣俜较胍膭?lì)各位開(kāi)發(fā)者使用基于組件的安全配置。

那么什么是基于組件的安全配置呢?我們來(lái)舉幾個(gè)例子:

以前我們配置 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();
    }
}

如果懂之前的寫法的話,下面這個(gè)代碼其實(shí)是很好理解的,我就不做過(guò)多解釋了,不過(guò)還不懂 Spring Security 基本用法的小伙伴

以前我們配置 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");
    }
}

另外還有一個(gè)就是關(guān)于 AuthenticationManager 的獲取,以前可以通過(guò)重寫父類的方法來(lái)獲取這個(gè) Bean,類似下面這樣:

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

以后就只能自己創(chuàng)建這個(gè) 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;
    }
}

當(dāng)然,也可以從 HttpSecurity 中提取出來(lái) 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();
    }
}

這也是一種辦法。

我們來(lái)看一個(gè)具體的例子。

首先我們新建一個(gè) Spring Boot 工程,引入 Web 和 Spring Security 依賴,注意 Spring Boot 選擇最新版。

接下來(lái)我們提供一個(gè)簡(jiǎn)單的測(cè)試接口,如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello 江南一點(diǎn)雨!";
    }
}

小伙伴們知道,在 Spring Security 中,默認(rèn)情況下,只要添加了依賴,我們項(xiàng)目的所有接口就已經(jīng)被統(tǒng)統(tǒng)保護(hù)起來(lái)了,現(xiàn)在啟動(dòng)項(xiàng)目,訪問(wèn) /hello 接口,就需要登錄之后才可以訪問(wèn),登錄的用戶名是 user,密碼則是隨機(jī)生成的,在項(xiàng)目的啟動(dòng)日志中。

現(xiàn)在我們的第一個(gè)需求是使用自定義的用戶,而不是系統(tǒng)默認(rèn)提供的,這個(gè)簡(jiǎn)單,我們只需要向 Spring 容器中注冊(cè)一個(gè) UserDetailsService 的實(shí)例即可,像下面這樣:

@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("江南一點(diǎn)雨").password("{noop}123").roles("admin").build());
        return users;
    }
}

這就可以了。

當(dāng)然我現(xiàn)在的用戶是存在內(nèi)存中的,如果你的用戶是存在數(shù)據(jù)庫(kù)中,那么只需要提供 UserDetailsService 接口的實(shí)現(xiàn)類并注入 Spring 容器即可,這個(gè)之前在 vhr 視頻中講過(guò)多次了(公號(hào)后臺(tái)回復(fù) 666 有視頻介紹),這里就不再贅述了。

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

@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("江南一點(diǎn)雨").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) 方法中的內(nèi)容,現(xiàn)在位于 WebSecurityCustomizer Bean 中,該配置的東西寫在這里就可以了。

那如果我還希望對(duì)登錄頁(yè)面,參數(shù)等,進(jìn)行定制呢?繼續(xù)往下看:

@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("江南一點(diǎn)雨").password("{noop}123").roles("admin").build());
        return users;
    }
    @Bean
    SecurityFilterChain securityFilterChain() {
        List<Filter> filters = new ArrayList<>();
        return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"), filters);
    }
}

Spring Security 的底層實(shí)際上就是一堆過(guò)濾器,所以我們之前在 configure(HttpSecurity) 方法中的配置,實(shí)際上就是配置過(guò)濾器鏈?,F(xiàn)在過(guò)濾器鏈的配置,我們通過(guò)提供一個(gè) SecurityFilterChain Bean 來(lái)配置過(guò)濾器鏈,SecurityFilterChain 是一個(gè)接口,這個(gè)接口只有一個(gè)實(shí)現(xiàn)類 DefaultSecurityFilterChain,構(gòu)建 DefaultSecurityFilterChain 的第一個(gè)參數(shù)是攔截規(guī)則,也就是哪些路徑需要攔截,第二個(gè)參數(shù)則是過(guò)濾器鏈,這里我給了一個(gè)空集合,也就是我們的 Spring Security 會(huì)攔截下所有的請(qǐng)求,然后在一個(gè)空集合中走一圈就結(jié)束了,相當(dāng)于不攔截任何請(qǐng)求。

此時(shí)重啟項(xiàng)目,你會(huì)發(fā)現(xiàn) /hello 也是可以直接訪問(wèn)的,就是因?yàn)檫@個(gè)路徑不經(jīng)過(guò)任何過(guò)濾器。

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

有小伙伴會(huì)說(shuō),這寫法跟我以前寫的也不一樣呀!這么配置,我也不知道 Spring Security 中有哪些過(guò)濾器,其實(shí),換一個(gè)寫法,我們就可以將這個(gè)配置成以前那種樣子:

@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("江南一點(diǎn)雨").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();
    }
}

這么寫,就跟以前的寫法其實(shí)沒(méi)啥大的差別了。

2. 使用 Lambda

在最新版中,小伙伴們發(fā)現(xiàn),很多常見(jiàn)的方法廢棄了,如下圖:

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

也就是說(shuō),你以后見(jiàn)不到類似下面這樣的配置了:

@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 方法的注釋中小伙伴們可以看到,官方現(xiàn)在是在推動(dòng)基于 Lambda 的配置來(lái)代替?zhèn)鹘y(tǒng)的鏈?zhǔn)脚渲?,所以以后我們的寫法就得改成下面這樣啦:

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

其實(shí),這里的幾個(gè)方法倒不是啥新方法,只不過(guò)有的小伙伴可能之前不太習(xí)慣用上面這幾個(gè)方法進(jìn)行配置,習(xí)慣于鏈?zhǔn)脚渲???墒峭螅偷寐?xí)慣上面這種按照 Lambda 的方式來(lái)配置了,配置的內(nèi)容倒很好理解,我覺(jué)得沒(méi)啥好解釋的。

3. 自定義 JSON 登錄

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

3.1 自定義 JSON 登錄

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

3.1.1 自定義登錄過(guò)濾器

Spring Security 默認(rèn)處理登錄數(shù)據(jù)的過(guò)濾器是 UsernamePasswordAuthenticationFilter,在這個(gè)過(guò)濾器中,系統(tǒng)會(huì)通過(guò) request.getParameter(this.passwordParameter) 的方式將用戶名和密碼讀取出來(lái),很明顯這就要求前端傳遞參數(shù)的形式是 key-value。

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

public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //獲取請(qǐng)求頭,據(jù)此判斷請(qǐng)求參數(shù)類型
        String contentType = request.getContentType();
        if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {
            //說(shuō)明請(qǐng)求參數(shù)是 JSON
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            String username = null;
            String password = null;
            try {
                //解析請(qǐng)求體中的 JSON 參數(shù)
                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);
            }
            //構(gòu)建登錄令牌
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                    password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //執(zhí)行真正的登錄操作
            Authentication auth = this.getAuthenticationManager().authenticate(authRequest);
            return auth;
        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}

看過(guò)之前的 Spring Security 系列文章的小伙伴,這段代碼應(yīng)該都是非常熟悉了。

  • 首先我們獲取請(qǐng)求頭,根據(jù)請(qǐng)求頭的類型來(lái)判斷請(qǐng)求參數(shù)的格式。
  • 如果是 JSON 格式的參數(shù),就在 if 中進(jìn)行處理,否則說(shuō)明是 key-value 形式的參數(shù),那么我們就調(diào)用父類的方法進(jìn)行處理即可。
  • JSON 格式的參數(shù)的處理邏輯和 key-value 的處理邏輯是一致的,唯一不同的是參數(shù)的提取方式不同而已。

最后,我們還需要對(duì)這個(gè)過(guò)濾器進(jìn)行配置:

@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();
            //獲取當(dāng)前登錄成功的用戶對(duì)象
            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("用戶名或者密碼輸入錯(cuò)誤,登錄失敗");
            } else if (e instanceof DisabledException) {
                respBean.setMessage("賬戶被禁用,登錄失敗");
            } else if (e instanceof CredentialsExpiredException) {
                respBean.setMessage("密碼過(guò)期,登錄失敗");
            } else if (e instanceof AccountExpiredException) {
                respBean.setMessage("賬戶過(guò)期,登錄失敗");
            } 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 {
        //開(kāi)啟過(guò)濾器的配置
        http.authorizeHttpRequests()
                //任意請(qǐng)求,都要認(rèn)證之后才能訪問(wèn)
                .anyRequest().authenticated()
                .and()
                //開(kāi)啟表單登錄,開(kāi)啟之后,就會(huì)自動(dòng)配置登錄頁(yè)面、登錄接口等信息
                .formLogin()
                //和登錄相關(guān)的 URL 地址都放行
                .permitAll()
                .and()
                //關(guān)閉 csrf 保護(hù)機(jī)制,本質(zhì)上就是從 Spring Security 過(guò)濾器鏈中移除了 CsrfFilter
                .csrf().disable();
        http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

這里就是配置一個(gè) JsonLoginFilter 的 Bean,并將之添加到 Spring Security 過(guò)濾器鏈中即可。

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

但是從 Spring Boot3 開(kāi)始,這段代碼有點(diǎn)瑕疵了,直接用已經(jīng)無(wú)法實(shí)現(xiàn) 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();
        }
    }
}

這里直接自定義登錄接口,請(qǐng)求參數(shù)通過(guò) JSON 的形式來(lái)傳遞。拿到用戶名密碼之后,調(diào)用 AuthenticationManager#authenticate 方法進(jìn)行認(rèn)證即可。認(rèn)證成功之后,將認(rèn)證后的用戶信息存入到 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 這個(gè)地址可以不用登錄直接訪問(wèn)
                .requestMatchers("/doLogin").permitAll()
                .anyRequest().authenticated().and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();
    }
}

這也算是一種使用 JSON 格式參數(shù)的方案。在 Spring Boot3 之前(Spring Security6 之前),上面這個(gè)方案也是沒(méi)有任何問(wèn)題的。

從 Spring Boot3(Spring Security6) 開(kāi)始,上面這兩種方案都出現(xiàn)了一些瑕疵。

具體表現(xiàn)就是:當(dāng)你調(diào)用登錄接口登錄成功之后,再去訪問(wèn)系統(tǒng)中的其他頁(yè)面,又會(huì)跳轉(zhuǎn)回登錄頁(yè)面,說(shuō)明訪問(wèn)登錄之外的其他接口時(shí),系統(tǒng)不知道你已經(jīng)登錄過(guò)了。

3.2 原因分析

產(chǎn)生上面問(wèn)題的原因,主要在于 Spring Security 過(guò)濾器鏈中有一個(gè)過(guò)濾器發(fā)生變化了:

在 Spring Boot3 之前,Spring Security 過(guò)濾器鏈中有一個(gè)名為 SecurityContextPersistenceFilter 的過(guò)濾器,這個(gè)過(guò)濾器在 Spring Boot2.7.x 中廢棄了,但是還在使用,在 Spring Boot3 中則被從 Spring Security 過(guò)濾器鏈中移除了,取而代之的是一個(gè)名為 SecurityContextHolderFilter 的過(guò)濾器。

在第一小節(jié)和小伙伴們介紹的兩種 JSON 登錄方案在 Spring Boot2.x 中可以運(yùn)行在 Spring Boot3.x 中無(wú)法運(yùn)行,就是因?yàn)檫@個(gè)過(guò)濾器的變化導(dǎo)致的。

所以接下來(lái)我們就來(lái)分析一下這兩個(gè)過(guò)濾器到底有哪些區(qū)別。

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

我這里只貼出來(lái)了一些關(guān)鍵的核心代碼:

  • 首先,這個(gè)過(guò)濾器位于整個(gè) Spring Security 過(guò)濾器鏈的第三個(gè),是非??壳暗?。
  • 當(dāng)?shù)卿浾?qǐng)求經(jīng)過(guò)這個(gè)過(guò)濾器的時(shí)候,首先會(huì)嘗試從 SecurityContextRepository(上文中的 this.repo)中讀取到 SecurityContext 對(duì)象,這個(gè)對(duì)象中保存了當(dāng)前用戶的信息,第一次登錄的時(shí)候,這里實(shí)際上讀取不到任何用戶信息。
  • 將讀取到的 SecurityContext 存入到 SecurityContextHolder 中,默認(rèn)情況下,SecurityContextHolder 中通過(guò) ThreadLocal 來(lái)保存 SecurityContext 對(duì)象,也就是當(dāng)前請(qǐng)求在后續(xù)的處理流程中,只要在同一個(gè)線程里,都可以直接從 SecurityContextHolder 中提取到當(dāng)前登錄用戶信息。
  • 請(qǐng)求繼續(xù)向后執(zhí)行。
  • 在 finally 代碼塊中,當(dāng)前請(qǐng)求已經(jīng)結(jié)束了,此時(shí)再次獲取到 SecurityContext,并清空 SecurityContextHolder 防止內(nèi)存泄漏,然后調(diào)用 this.repo.saveContext 方法保存當(dāng)前登錄用戶對(duì)象(實(shí)際上是保存到 HttpSession 中)。
  • 以后其他請(qǐng)求到達(dá)的時(shí)候,執(zhí)行前面第 2 步的時(shí)候,就讀取到當(dāng)前用戶的信息了,在請(qǐng)求后續(xù)的處理過(guò)程中,Spring Security 需要知道當(dāng)前用戶的時(shí)候,會(huì)自動(dòng)去 SecurityContextHolder 中讀取當(dāng)前用戶信息。

這就是 Spring Security 認(rèn)證的一個(gè)大致流程。

然而,到了 Spring Boot3 之后,這個(gè)過(guò)濾器被 SecurityContextHolderFilter 取代了,我們來(lái)看下 SecurityContextHolderFilter 過(guò)濾器的一個(gè)關(guān)鍵邏輯:

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 的操作。

這下就明白了,用戶登錄成功之后,用戶信息沒(méi)有保存到 HttpSession,導(dǎo)致下一次請(qǐng)求到達(dá)的時(shí)候,無(wú)法從 HttpSession 中讀取到 SecurityContext 存到 SecurityContextHolder 中,在后續(xù)的執(zhí)行過(guò)程中,Spring Security 就會(huì)認(rèn)為當(dāng)前用戶沒(méi)有登錄。

這就是問(wèn)題的原因!

找到原因,那么問(wèn)題就好解決了。

3.3 問(wèn)題解決

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

其實(shí),Spring Security 提供了另外一個(gè)修改的入口,在 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);
}

這個(gè)方法是當(dāng)前用戶登錄成功之后的回調(diào)方法,小伙伴們看到,在這個(gè)回調(diào)方法中,有一句 this.securityContextRepository.saveContext(context, request, response);,這就表示將當(dāng)前登錄成功的用戶信息存入到 HttpSession 中。

在當(dāng)前過(guò)濾器中,securityContextRepository 的類型是 RequestAttributeSecurityContextRepository,這個(gè)表示將 SecurityContext 存入到當(dāng)前請(qǐng)求的屬性中,那很明顯,在當(dāng)前請(qǐng)求結(jié)束之后,這個(gè)數(shù)據(jù)就沒(méi)了。在 Spring Security 的自動(dòng)化配置類中,將 securityContextRepository 屬性指向了 DelegatingSecurityContextRepository,這是一個(gè)代理的存儲(chǔ)器,代理的對(duì)象是 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository,所以在默認(rèn)的情況下,用戶登錄成功之后,在這里就把登錄用戶數(shù)據(jù)存入到 HttpSessionSecurityContextRepository 中了。

當(dāng)我們自定義了登錄過(guò)濾器之后,就破壞了自動(dòng)化配置里的方案了,這里使用的 securityContextRepository 對(duì)象就真的是 RequestAttributeSecurityContextRepository 了,所以就導(dǎo)致用戶后續(xù)訪問(wèn)時(shí)系統(tǒng)以為用戶未登錄。

那么解決方案很簡(jiǎn)單,我們只需要為自定義的過(guò)濾器指定 securityContextRepository 屬性的值就可以了,如下:

@Bean
JsonLoginFilter jsonLoginFilter() {
    JsonLoginFilter filter = new JsonLoginFilter();
    filter.setAuthenticationSuccessHandler((req,resp,auth)->{
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        //獲取當(dāng)前登錄成功的用戶對(duì)象
        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("用戶名或者密碼輸入錯(cuò)誤,登錄失敗");
        } else if (e instanceof DisabledException) {
            respBean.setMessage("賬戶被禁用,登錄失敗");
        } else if (e instanceof CredentialsExpiredException) {
            respBean.setMessage("密碼過(guò)期,登錄失敗");
        } else if (e instanceof AccountExpiredException) {
            respBean.setMessage("賬戶過(guò)期,登錄失敗");
        } 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;
}

小伙伴們看到,最后調(diào)用 setSecurityContextRepository 方法設(shè)置一下就行。

Spring Boot3.x 之前之所以不用設(shè)置這個(gè)屬性,是因?yàn)檫@里雖然沒(méi)保存最后還是在 SecurityContextPersistenceFilter 過(guò)濾器中保存了。

那么對(duì)于自定義登錄接口的問(wèn)題,解決思路也是類似的:

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

小伙伴們看到,在登錄成功之后,開(kāi)發(fā)者自己手動(dòng)將數(shù)據(jù)存入到 HttpSession 中,這樣就能確保下個(gè)請(qǐng)求到達(dá)的時(shí)候,能夠從 HttpSession 中讀取到有效的數(shù)據(jù)存入到 SecurityContextHolder 中了。

4. 該如何實(shí)現(xiàn)動(dòng)態(tài)權(quán)限管理

4.1. 權(quán)限開(kāi)發(fā)思路

先來(lái)說(shuō)權(quán)限開(kāi)發(fā)的思路,當(dāng)我們?cè)O(shè)計(jì)好 RBAC 權(quán)限之后,具體到代碼層面,我們有兩種實(shí)現(xiàn)思路:

  • 直接在接口/Service 層方法上添加權(quán)限注解,這樣做的好處是實(shí)現(xiàn)簡(jiǎn)單,但是有一個(gè)問(wèn)題就是權(quán)限硬編碼,每一個(gè)方法需要什么權(quán)限都是代碼中配置好的,后期如果想通過(guò)管理頁(yè)面修改是不可能的,要修改某一個(gè)方法所需要的權(quán)限只能改代碼。
  • 將請(qǐng)求和權(quán)限的關(guān)系通過(guò)數(shù)據(jù)庫(kù)來(lái)描述,每一個(gè)請(qǐng)求需要什么權(quán)限都在數(shù)據(jù)庫(kù)中配置好,當(dāng)請(qǐng)求到達(dá)的時(shí)候,動(dòng)態(tài)查詢,然后判斷權(quán)限是否滿足,這樣做的好處是比較靈活,將來(lái)需要修改接口和權(quán)限之間的關(guān)系時(shí),可以通過(guò)管理頁(yè)面點(diǎn)擊幾下,問(wèn)題就解決了,不用修改代碼,松哥之前的 vhr 中就是這樣做的。

4.2. 具體實(shí)踐

4.2.1 舊方案回顧

第一個(gè)類是收集權(quán)限元數(shù)據(jù)的類:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //...
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

在 getAttributes 方法中,根據(jù)當(dāng)前請(qǐng)求的 URL 地址(從參數(shù) Object 中可提取出來(lái)),然后根據(jù)權(quán)限表中的配置,分析出來(lái)當(dāng)前請(qǐng)求需要哪些權(quán)限并返回。

另外我還重寫了一個(gè)決策器,其實(shí)決策器也可以不重寫,就看你自己的需求,如果 Spring Security 自帶的決策器無(wú)法滿足你的需求,那么可以自己寫一個(gè)決策器:

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //...
    }
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

decide 方法就是做決策的地方,第一個(gè)參數(shù)中可以提取出當(dāng)前用戶具備什么權(quán)限,第三個(gè)參數(shù)是當(dāng)前請(qǐng)求需要什么權(quán)限,比較一下就行了,如果當(dāng)前用戶不具備需要的權(quán)限,則直接拋出 AccessDeniedException 異常即可。

最后,通過(guò) Bean 的后置處理器 BeanPostProcessor,將這兩個(gè)配置類放到 Spring Security 的 FilterSecurityInterceptor 攔截器中:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setAccessDecisionManager(customUrlDecisionManager);
                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                    return object;
                }
            })
            .and()
            //...
}

大致上的邏輯就是如此

4.2.2 新方案

不過(guò)以上代碼在目前最新的 Spring Security6 中用不了了,不是因?yàn)轭愡^(guò)期了,而是因?yàn)轭惐灰瞥?!哪個(gè)類被移除了?FilterSecurityInterceptor。

FilterSecurityInterceptor 這個(gè)過(guò)濾器以前是做權(quán)限處理的,但是在新版的 Spring Security6 中,這個(gè)攔截器被 AuthorizationFilter 代替了。

老實(shí)說(shuō),新版的方案其實(shí)更合理一些,傳統(tǒng)的方案感覺(jué)帶有很多前后端不分的影子,現(xiàn)在就往更純粹的前后端分離奔去。

由于新版中連 FilterSecurityInterceptor 都不用了,所以舊版的方案顯然行不通了,新版的方案實(shí)際上更加簡(jiǎn)單。

雖然新舊寫法不同,但是核心思路是一模一樣。

我們來(lái)看下新版的配置:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(register -> register.anyRequest().access((authentication, object) -> {
                //表示請(qǐng)求的 URL 地址和數(shù)據(jù)庫(kù)的地址是否匹配上了
                boolean isMatch = false;
                //獲取當(dāng)前請(qǐng)求的 URL 地址
                String requestURI = object.getRequest().getRequestURI();
                List<MenuWithRoleVO> menuWithRole = menuService.getMenuWithRole();
                for (MenuWithRoleVO m : menuWithRole) {
                    if (antPathMatcher.match(m.getUrl(), requestURI)) {
                        isMatch = true;
                        //說(shuō)明找到了請(qǐng)求的地址了
                        //這就是當(dāng)前請(qǐng)求需要的角色
                        List<Role> roles = m.getRoles();
                        //獲取當(dāng)前登錄用戶的角色
                        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
                        for (GrantedAuthority authority : authorities) {
                            for (Role role : roles) {
                                if (authority.getAuthority().equals(role.getName())) {
                                    //說(shuō)明當(dāng)前登錄用戶具備當(dāng)前請(qǐng)求所需要的角色
                                    return new AuthorizationDecision(true);
                                }
                            }
                        }
                    }
                }
                if (!isMatch) {
                    //說(shuō)明請(qǐng)求的 URL 地址和數(shù)據(jù)庫(kù)的地址沒(méi)有匹配上,對(duì)于這種請(qǐng)求,統(tǒng)一只要登錄就能訪問(wèn)
                    if (authentication.get() instanceof AnonymousAuthenticationToken) {
                        return new AuthorizationDecision(false);
                    } else {
                        //說(shuō)明用戶已經(jīng)認(rèn)證了
                        return new AuthorizationDecision(true);
                    }
                }
                return new AuthorizationDecision(false);
            }))
            .formLogin(form -> 
            //...
            )
            .csrf(csrf -> 
            //...
            )
            .exceptionHandling(e -> 
            //...
            )
            .logout(logout ->
            //...
            );
    return http.build();
}

核心思路還是和之前一樣,只不過(guò)現(xiàn)在的工作都在 access 方法中完成。

access 方法的回調(diào)中有兩個(gè)參數(shù),第一個(gè)參數(shù)是 authentication,很明顯,這就是當(dāng)前登錄成功的用戶對(duì)象,從這里我們就可以提取出來(lái)當(dāng)前用戶所具備的權(quán)限。

第二個(gè)參數(shù) object 實(shí)際上是一個(gè) RequestAuthorizationContext,從這個(gè)里邊可以提取出來(lái)當(dāng)前請(qǐng)求對(duì)象 HttpServletRequest,進(jìn)而提取出來(lái)當(dāng)前請(qǐng)求的 URL 地址,然后依據(jù)權(quán)限表中的信息,判斷出當(dāng)前請(qǐng)求需要什么權(quán)限,再和 authentication 中提取出來(lái)的當(dāng)前用戶所具備的權(quán)限進(jìn)行對(duì)比即可。

如果當(dāng)前登錄用戶具備請(qǐng)求所需要的權(quán)限,則返回 new AuthorizationDecision(true);,否則返回 new AuthorizationDecision(false); 即可。

其實(shí)無(wú)論什么框架,只要能把其中一個(gè)版本掌握個(gè) 70%,以后無(wú)論它怎么升級(jí),你都能快速上手!

到此這篇關(guān)于Spring Security6 最新版配置及實(shí)現(xiàn)動(dòng)態(tài)權(quán)限管理的文章就介紹到這了,更多相關(guān)Spring Security6動(dòng)態(tài)權(quán)限管理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring中的AOP操作你了解嗎

    Spring中的AOP操作你了解嗎

    這篇文章主要為大家詳細(xì)介紹了Spring中的AOP操作,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2022-02-02
  • SpringCloud鏈路追蹤組件Sleuth配置方法解析

    SpringCloud鏈路追蹤組件Sleuth配置方法解析

    這篇文章主要介紹了SpringCloud鏈路追蹤組件Sleuth配置方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • Idea如何導(dǎo)入java mysql驅(qū)動(dòng)包

    Idea如何導(dǎo)入java mysql驅(qū)動(dòng)包

    本文介紹了如何在IntelliJ IDEA中配置MySQL數(shù)據(jù)庫(kù)連接,首先下載MySQL Connector/J驅(qū)動(dòng)并解壓,然后在Idea項(xiàng)目中創(chuàng)建lib文件夾并將.jar文件復(fù)制到該文件夾,接著,將.jar文件添加為項(xiàng)目庫(kù),通過(guò)這些步驟,可以成功配置MySQL數(shù)據(jù)庫(kù)連接
    2024-12-12
  • 解析JPA的視圖查詢問(wèn)題

    解析JPA的視圖查詢問(wèn)題

    這篇文章主要是對(duì)JPA的視圖查詢問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助
    2013-12-12
  • SpringBoot集成ClickHouse數(shù)據(jù)庫(kù)操作過(guò)程舉例

    SpringBoot集成ClickHouse數(shù)據(jù)庫(kù)操作過(guò)程舉例

    本文講解如何在SpringBoot中集成ClickHouse列式數(shù)據(jù)庫(kù),涵蓋依賴配置、實(shí)體映射、CRUD操作及復(fù)雜查詢實(shí)現(xiàn),結(jié)合實(shí)踐項(xiàng)目提升數(shù)據(jù)處理效率,本教程還將說(shuō)明ClickHouse的數(shù)據(jù)類型、并行處理能力、聚合函數(shù)等特性,并提供一個(gè)實(shí)踐項(xiàng)目以加深理解,需要的朋友一起看看吧
    2025-08-08
  • 在Java中輕松使用工廠設(shè)計(jì)模式介紹

    在Java中輕松使用工廠設(shè)計(jì)模式介紹

    這篇文章主要介紹了在Java中輕松使用工廠設(shè)計(jì)模式介紹,工廠設(shè)計(jì)模式或工廠方法設(shè)計(jì)模式是一種廣泛使用且易于理解的設(shè)計(jì)模式,文章通過(guò)圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,感興趣的朋友可以參考一下
    2022-09-09
  • Idea插件StopCoding的安裝使用教程

    Idea插件StopCoding的安裝使用教程

    這篇文章主要介紹了Idea插件StopCoding的安裝使用教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-01-01
  • Spring中的異步方法@Async失效的原因詳解

    Spring中的異步方法@Async失效的原因詳解

    這篇文章主要介紹了Spring中的異步方法@Async失效的原因詳解,@Async屬于異步注解,@Async放在方法上標(biāo)識(shí)該方法為異步方法,異步是指進(jìn)程不需要一直等待下去,而是繼續(xù)執(zhí)行下面的操作,不管其他進(jìn)程的狀態(tài),需要的朋友可以參考下
    2024-01-01
  • springboot操作靜態(tài)資源文件的方法

    springboot操作靜態(tài)資源文件的方法

    這篇文章主要介紹了springboot操作靜態(tài)資源文件的方法,本文給大家提到了兩種方法,小編在這里比較推薦第一種方法,具體內(nèi)容詳情大家跟隨腳本之家小編一起看看吧
    2018-07-07
  • 淺談@RequestParam(required = true)的誤區(qū)

    淺談@RequestParam(required = true)的誤區(qū)

    這篇文章主要介紹了@RequestParam(required = true)的誤區(qū),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11

最新評(píng)論

犍为县| 右玉县| 兴隆县| 长泰县| 克东县| 双柏县| 蕉岭县| 沽源县| 武隆县| 崇仁县| 曲阜市| 漳浦县| 化德县| 聊城市| 万山特区| 文成县| 淳化县| 东山县| 固阳县| 高安市| 万州区| 仪陇县| 井冈山市| 镇江市| 龙南县| 潜山县| 财经| 清河县| 灵丘县| 焉耆| 铜梁县| 集安市| 盖州市| 霸州市| 兰州市| 合川市| 奇台县| 普定县| 茌平县| 永平县| 安仁县|