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

創(chuàng)新互聯(lián)長期為上千多家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為南崗企業(yè)提供專業(yè)的成都網(wǎng)站建設(shè)、網(wǎng)站制作,南崗網(wǎng)站改版等技術(shù)服務(wù)。擁有十余年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
接下來,我把從 Spring Security5.7 開始(對應(yīng) Spring Boot2.7 開始),各種已知的變化都來和小伙伴們梳理一下。
1. WebSecurityConfigurerAdapter
圖片
首先第一點(diǎn),就是各位小伙伴最容易發(fā)現(xiàn)的 WebSecurityConfigurerAdapter 過期了,在目前最新的 Spring Security6.1 中,這個類已經(jīng)完全被移除了,想湊合著用都不行了。
準(zhǔn)確來說,Spring Security 是在 5.7.0-M2 這個版本中將 WebSecurityConfigurerAdapter 過期的,過期的原因是因?yàn)楣俜较胍膭罡魑婚_發(fā)者使用基于組件的安全配置。
那么什么是基于組件的安全配置呢?我們來舉幾個例子:
以前我們配置 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();
}
}如果懂之前的寫法的話,下面這個代碼其實(shí)是很好理解的,我就不做過多解釋了,不過還不懂 Spring Security 基本用法的小伙伴,可以在公眾號后臺回復(fù) 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");
}
}另外還有一個就是關(guān)于 AuthenticationManager 的獲取,以前可以通過重寫父類的方法來獲取這個 Bean,類似下面這樣:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}以后就只能自己創(chuàng)建這個 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 中提取出來 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 江南一點(diǎn)雨!";
}
}小伙伴們知道,在 Spring Security 中,默認(rèn)情況下,只要添加了依賴,我們項(xiàng)目的所有接口就已經(jīng)被統(tǒng)統(tǒng)保護(hù)起來了,現(xiàn)在啟動項(xiàng)目,訪問 /hello 接口,就需要登錄之后才可以訪問,登錄的用戶名是 user,密碼則是隨機(jī)生成的,在項(xiàng)目的啟動日志中。
現(xiàn)在我們的第一個需求是使用自定義的用戶,而不是系統(tǒng)默認(rèn)提供的,這個簡單,我們只需要向 Spring 容器中注冊一個 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ù)庫中,那么只需要提供 UserDetailsService 接口的實(shí)現(xiàn)類并注入 Spring 容器即可,這個之前在 vhr 視頻中講過多次了(公號后臺回復(fù) 666 有視頻介紹),這里就不再贅述了。
但是假如說我希望 /hello 這個接口能夠匿名訪問,并且我希望這個匿名訪問還不經(jīng)過 Spring Security 過濾器鏈,要是在以前,我們可以重寫 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 中,該配置的東西寫在這里就可以了。
那如果我還希望對登錄頁面,參數(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 filters = new ArrayList<>();
return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"), filters);
}
} Spring Security 的底層實(shí)際上就是一堆過濾器,所以我們之前在 configure(HttpSecurity) 方法中的配置,實(shí)際上就是配置過濾器鏈。現(xiàn)在過濾器鏈的配置,我們通過提供一個 SecurityFilterChain Bean 來配置過濾器鏈,SecurityFilterChain 是一個接口,這個接口只有一個實(shí)現(xiàn)類 DefaultSecurityFilterChain,構(gòu)建 DefaultSecurityFilterChain 的第一個參數(shù)是攔截規(guī)則,也就是哪些路徑需要攔截,第二個參數(shù)則是過濾器鏈,這里我給了一個空集合,也就是我們的 Spring Security 會攔截下所有的請求,然后在一個空集合中走一圈就結(jié)束了,相當(dāng)于不攔截任何請求。
此時重啟項(xiàng)目,你會發(fā)現(xiàn) /hello 也是可以直接訪問的,就是因?yàn)檫@個路徑不經(jīng)過任何過濾器。
其實(shí)我覺得目前這中新寫法比以前老的寫法更直觀,更容易讓大家理解到 Spring Security 底層的過濾器鏈工作機(jī)制。
有小伙伴會說,這寫法跟我以前寫的也不一樣呀!這么配置,我也不知道 Spring Security 中有哪些過濾器,其實(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;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}這么寫,就跟以前的寫法其實(shí)沒啥大的差別了。
2. 使用 Lambda
在最新版中,小伙伴們發(fā)現(xiàn),很多常見的方法廢棄了,如下圖:
包括大家熟悉的用來連接各個配置項(xiàng)的 and() 方法現(xiàn)在也廢棄了,并且按照官方的說法,將在 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() 方法將被移除!
其實(shí),松哥覺得移除 and 方法是個好事,對于很多初學(xué)者來說,光是理解 and 這個方法就要好久。
從上面 and 方法的注釋中小伙伴們可以看到,官方現(xiàn)在是在推動基于 Lambda 的配置來代替?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í),這里的幾個方法倒不是啥新方法,只不過有的小伙伴可能之前不太習(xí)慣用上面這幾個方法進(jìn)行配置,習(xí)慣于鏈?zhǔn)脚渲???墒峭螅偷寐?xí)慣上面這種按照 Lambda 的方式來配置了,配置的內(nèi)容倒很好理解,我覺得沒啥好解釋的。
3. 自定義 JSON 登錄
自定義 JSON 登錄也和之前舊版不太一樣了。
3.1 自定義 JSON 登錄
小伙伴們知道,Spring Security 中默認(rèn)的登錄接口數(shù)據(jù)格式是 key-value 的形式,如果我們想使用 JSON 格式來登錄,那么就必須自定義過濾器或者自定義登錄接口,下面松哥先來和小伙伴們展示一下這兩種不同的登錄形式。
3.1.1 自定義登錄過濾器
Spring Security 默認(rèn)處理登錄數(shù)據(jù)的過濾器是 UsernamePasswordAuthenticationFilter,在這個過濾器中,系統(tǒng)會通過 request.getParameter(this.passwordParameter) 的方式將用戶名和密碼讀取出來,很明顯這就要求前端傳遞參數(shù)的形式是 key-value。
如果想要使用 JSON 格式的參數(shù)登錄,那么就需要從這個地方做文章了,我們自定義的過濾器如下:
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//獲取請求頭,據(jù)此判斷請求參數(shù)類型
String contentType = request.getContentType();
if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {
//說明請求參數(shù)是 JSON
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = null;
String password = null;
try {
//解析請求體中的 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);
}
}
}看過松哥之前的 Spring Security 系列文章的小伙伴,這段代碼應(yīng)該都是非常熟悉了。
- 首先我們獲取請求頭,根據(jù)請求頭的類型來判斷請求參數(shù)的格式。
- 如果是 JSON 格式的參數(shù),就在 if 中進(jìn)行處理,否則說明是 key-value 形式的參數(shù),那么我們就調(diào)用父類的方法進(jìn)行處理即可。
- JSON 格式的參數(shù)的處理邏輯和 key-value 的處理邏輯是一致的,唯一不同的是參數(shù)的提取方式不同而已。
最后,我們還需要對這個過濾器進(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)前登錄成功的用戶對象
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()
//任意請求,都要認(rèn)證之后才能訪問
.anyRequest().authenticated()
.and()
//開啟表單登錄,開啟之后,就會自動配置登錄頁面、登錄接口等信息
.formLogin()
//和登錄相關(guān)的 URL 地址都放行
.permitAll()
.and()
//關(guān)閉 csrf 保護(hù)機(jī)制,本質(zhì)上就是從 Spring Security 過濾器鏈中移除了 CsrfFilter
.csrf().disable();
http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}這里就是配置一個 JsonLoginFilter 的 Bean,并將之添加到 Spring Security 過濾器鏈中即可。
在 Spring Boot3 之前(Spring Security6 之前),上面這段代碼就可以實(shí)現(xiàn) JSON 登錄了。
但是從 Spring Boot3 開始,這段代碼有點(diǎn)瑕疵了,直接用已經(jīng)無法實(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();
}
}
}這里直接自定義登錄接口,請求參數(shù)通過 JSON 的形式來傳遞。拿到用戶名密碼之后,調(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 這個地址可以不用登錄直接訪問
.requestMatchers("/doLogin").permitAll()
.anyRequest().authenticated().and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}這也算是一種使用 JSON 格式參數(shù)的方案。在 Spring Boot3 之前(Spring Security6 之前),上面這個方案也是沒有任何問題的。
從 Spring Boot3(Spring Security6) 開始,上面這兩種方案都出現(xiàn)了一些瑕疵。
具體表現(xiàn)就是:當(dāng)你調(diào)用登錄接口登錄成功之后,再去訪問系統(tǒng)中的其他頁面,又會跳轉(zhuǎn)回登錄頁面,說明訪問登錄之外的其他接口時,系統(tǒng)不知道你已經(jīng)登錄過了。
3.2 原因分析
產(chǎn)生上面問題的原因,主要在于 Spring Security 過濾器鏈中有一個過濾器發(fā)生變化了:
在 Spring Boot3 之前,Spring Security 過濾器鏈中有一個名為 SecurityContextPersistenceFilter 的過濾器,這個過濾器在 Spring Boot2.7.x 中廢棄了,但是還在使用,在 Spring Boot3 中則被從 Spring Security 過濾器鏈中移除了,取而代之的是一個名為 SecurityContextHolderFilter 的過濾器。
在第一小節(jié)和小伙伴們介紹的兩種 JSON 登錄方案在 Spring Boot2.x 中可以運(yùn)行在 Spring Boot3.x 中無法運(yùn)行,就是因?yàn)檫@個過濾器的變化導(dǎo)致的。
所以接下來我們就來分析一下這兩個過濾器到底有哪些區(qū)別。
先來看 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());
}
}我這里只貼出來了一些關(guān)鍵的核心代碼:
- 首先,這個過濾器位于整個 Spring Security 過濾器鏈的第三個,是非??壳暗摹?/li>
- 當(dāng)?shù)卿浾埱蠼?jīng)過這個過濾器的時候,首先會嘗試從 SecurityContextRepository(上文中的 this.repo)中讀取到 SecurityContext 對象,這個對象中保存了當(dāng)前用戶的信息,第一次登錄的時候,這里實(shí)際上讀取不到任何用戶信息。
- 將讀取到的 SecurityContext 存入到 SecurityContextHolder 中,默認(rèn)情況下,SecurityContextHolder 中通過 ThreadLocal 來保存 SecurityContext 對象,也就是當(dāng)前請求在后續(xù)的處理流程中,只要在同一個線程里,都可以直接從 SecurityContextHolder 中提取到當(dāng)前登錄用戶信息。
- 請求繼續(xù)向后執(zhí)行。
- 在 finally 代碼塊中,當(dāng)前請求已經(jīng)結(jié)束了,此時再次獲取到 SecurityContext,并清空 SecurityContextHolder 防止內(nèi)存泄漏,然后調(diào)用 this.repo.saveContext 方法保存當(dāng)前登錄用戶對象(實(shí)際上是保存到 HttpSession 中)。
- 以后其他請求到達(dá)的時候,執(zhí)行前面第 2 步的時候,就讀取到當(dāng)前用戶的信息了,在請求后續(xù)的處理過程中,Spring Security 需要知道當(dāng)前用戶的時候,會自動去 SecurityContextHolder 中讀取當(dāng)前用戶信息。
這就是 Spring Security 認(rèn)證的一個大致流程。
然而,到了 Spring Boot3 之后,這個過濾器被 SecurityContextHolderFilter 取代了,我們來看下 SecurityContextHolderFilter 過濾器的一個關(guān)鍵邏輯:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Supplier 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,導(dǎo)致下一次請求到達(dá)的時候,無法從 HttpSession 中讀取到 SecurityContext 存到 SecurityContextHolder 中,在后續(xù)的執(zhí)行過程中,Spring Security 就會認(rèn)為當(dāng)前用戶沒有登錄。
這就是問題的原因!
找到原因,那么問題就好解決了。
3.3 問題解決
首先問題出在了過濾器上,直接改過濾器倒也不是不可以,但是,既然 Spring Security 在升級的過程中拋棄了之前舊的方案,我們又費(fèi)勁的把之前舊的方案寫回來,好像也不合理。
其實(shí),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);
}這個方法是當(dāng)前用戶登錄成功之后的回調(diào)方法,小伙伴們看到,在這個回調(diào)方法中,有一句 this.securityContextRepository.saveContext(context, request, response);,這就表示將當(dāng)前登錄成功的用戶信息存入到 HttpSession 中。
在當(dāng)前過濾器中,securityContextRepository 的類型是 RequestAttributeSecurityContextRepository,這個表示將 SecurityContext 存入到當(dāng)前請求的屬性中,那很明顯,在當(dāng)前請求結(jié)束之后,這個數(shù)據(jù)就沒了。在 Spring Security 的自動化配置類中,將 securityContextRepository 屬性指向了 DelegatingSecurityContextRepository,這是一個代理的存儲器,代理的對象是 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository,所以在默認(rèn)的情況下,用戶登錄成功之后,在這里就把登錄用戶數(shù)據(jù)存入到 HttpSessionSecurityContextRepository 中了。
當(dāng)我們自定義了登錄過濾器之后,就破壞了自動化配置里的方案了,這里使用的 securityContextRepository 對象就真的是 RequestAttributeSecurityContextRepository 了,所以就導(dǎo)致用戶后續(xù)訪問時系統(tǒng)以為用戶未登錄。
那么解決方案很簡單,我們只需要為自定義的過濾器指定 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)前登錄成功的用戶對象
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;
}小伙伴們看到,最后調(diào)用 setSecurityContextRepository 方法設(shè)置一下就行。
Spring Boot3.x 之前之所以不用設(shè)置這個屬性,是因?yàn)檫@里雖然沒保存最后還是在 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();
}
}
}小伙伴們看到,在登錄成功之后,開發(fā)者自己手動將數(shù)據(jù)存入到 HttpSession 中,這樣就能確保下個請求到達(dá)的時候,能夠從 HttpSession 中讀取到有效的數(shù)據(jù)存入到 SecurityContextHolder 中了。
好啦,Spring Boot 新舊版本交替中,一個小小的問題,希望小伙伴們能夠有所收獲。
新聞名稱:SpringSecurity6全新寫法,大變樣!
網(wǎng)頁網(wǎng)址:http://www.dlmjj.cn/article/cddisgi.html


咨詢
建站咨詢
