Spring?Boot?2.x升3.x的那些事
序言
手頭上有個項目,準備從Spring Boot 2.x升級到3.x,升級后發(fā)現(xiàn)編譯器報了一堆錯誤。一般來說大版本升級,肯定會有諸多問題,對于程序開發(fā)來說能不升就不升。但是對于系統(tǒng)架構來說,能用最新的肯定是用最新的,實在不行再降回去嘛??墒悄?,不知道是發(fā)布沒多久,還是我搜索技巧的問題,很多問題在網(wǎng)上找不到答案。沒辦法,還是得自己研究,所以呢這次我們就一起來研究一下Spring Boot 3.x究竟有什么改變。
一、關于Spring Session
一般來說,如果一個Spring Boot 2.x項目一開始只需要單實例部署,用不上redis共享會話的話,會在application.properties里加上這個參數(shù)。
spring.session.store-type=none
當需要改為多實例部署,需要redis共享會話的時候,只需要改為這樣就行了。
spring.session.store-type=redis
但是在Spring Boot 3.x項目里,這個參數(shù)就不復存在了。查了Spring Session的官方文檔也沒有收獲。于是去翻Spring Boot的官方文檔,在2.x的參考文檔中有這么一條提示“You can disable Spring Session by setting the store-type to none.”。而在3.x的文檔中,這個提示被刪掉了。好家伙,原來store-type=none是直接禁用整個Spring Session,而不是Api文檔中所說的"No session data-store."

那么解決辦法就很簡單了,單實例部署,不需要用redis的時候,刪掉pom.xml里org.springframework.session的依賴就好。需要redis共享會話的時候就要把依賴加回去了,就是沒有原來修改配置文件來得方便而已。
二、關于redis
在application.properties里關于redis的配置也有所變化。如果你是這么配置redis的:
spring.redis.host=127.0.0.1 spring.redis.port=6379
這時編譯器就會警告你:“Property ‘spring.redis.host’ is Deprecated: Use ‘spring.data.redis.host’ instead.”、“Property ‘spring.redis.password’ is Deprecated: Use ‘spring.data.redis.password’ instead.”按照警告所說的,把“spring.redis”替換成“spring.data.redis”即可。
spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379
三、關于servlet
由于tomcat 10包名的更換,如果你的程序是這么寫的:
import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; ...
那么編譯器就會報"The import javax.servlet cannot be resolved"錯誤。原因是包名從javax.servlet 調整為了jakarta.servlet 。解決辦法很簡單,把javax.servlet 替換為 jakarta.servlet 即可。
import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; ...
四、關于thymeleaf模板
當你使用片段表達式(fragment expression)而沒有使用“~{}”時,會獲得運行警告。例如你模板里這么寫:
<footer th:replace="footer::copy"></footer>
則會得到這樣的運行警告:“Deprecated unwrapped fragment expression “footer::copy” found in template index, line 7, col 9. Please use the complete syntax of fragment expressions instead (“~{footer::copy}”). The old, unwrapped syntax for fragment expressions will be removed in future versions of Thymeleaf.”
原因是在thymeleaf 3.1中,未封裝的片段表達式不再被推薦。解決方法也很簡單,按照警告所說的改為完整版的片段表達式,即加上“~{}”即可。
<footer th:replace="~{footer::copy}"></footer>
五、關于Spring Security
重點來了,隨著Spring Boot升級到3.x,Spring Security也升級到了6.x。話不多說,先來看看代碼,在6.x之前,如果你想要實現(xiàn)動態(tài)權限,你的代碼可能會是這樣的:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserService myUserService;
@Autowired
MyUrlFilter myUrlFilter;
@Autowired
MyDecisionManager myDecisionManager;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**");
}
@Override
public void configure(HttpSecurity http) throws Exception{
http.apply(new UrlAuthorizationConfigurer<>(http.getSharedObject(ApplicationContext.class)))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myDecisionManager);
o.setSecurityMetadataSource(myUrlFilter);
return o;
}
})
.and().formLogin().loginProcessingUrl("/login/process").loginPage("/login/page")
.and().logout().logoutUrl("/logout/page")
.and().sessionManagement().maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry())
.and().and().csrf().disable();
}
}
如果要把上面的代碼改成可以在Spring Security 6.x里運行,那么你需要這么寫:
@Configuration
public class MySecurityConfig {
@Autowired
MyAuthorizationManager myAuthorizationManager;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authz -> authz.anyRequest().access(myAuthorizationManager))
.formLogin(login -> login.loginProcessingUrl("/login/process").loginPage("/login/page").permitAll())
.logout(logout -> logout.logoutUrl("/logout/page").permitAll())
.sessionManagement(session -> session.maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry()))
.csrf(csrf -> csrf.disable());
return http.build();
}
}
我們來逐個講解一下。
1.關于WebSecurityConfigurerAdapter
在Spring Security 6.x之前,我們通常是寫一個配置類,繼承WebSecurityConfigurerAdapter 然后重寫(@Override)對應的方法來完成Security的配置的。而在Spring Security 6.x里WebSecurityConfigurerAdapter 已經(jīng)被棄用了,現(xiàn)在推薦使用的是基于組件的編碼方式,只要在配置類里注冊對應的組件(@Bean)即可。另外,使用組件配置時and()方法已經(jīng)不再推薦使用,官方建議使用lambda DSL。
2.關于UserDetailsService
按上面所說的,下面這段代碼。
@Autowired
MyUserService myUserService;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserService);
}
理論上是要改成這樣的。
@Autowired
MyUserService myUserService;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(myUserService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
但實際上只要你的用戶服務(MyUserService)實現(xiàn)了UserDetailsService接口,并且注冊到了Spring容器中(加了@Service或者@Component注解),Spring Security 6.x就會自動綁定用戶服務,只需注冊密碼加密組件即可。所以上面的代碼直接改成下面的就可以了。
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
(由于篇幅關系,這里就不貼MyUserService的代碼了,自己按實際情況實現(xiàn)對應接口功能就好)
3.關于WebSecurity
WebSecurity可以控制哪些地址不進入Security過濾器鏈。原來的代碼是這么寫的。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**");
}
現(xiàn)在,除了需要改為基于組件的寫法外,antMatchers()方法也改成了requestMatchers()方法。
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**");
}
4.關于HttpSecurity
原來在HttpSecurity中實現(xiàn)動態(tài)權限,是先要寫一個訪問地址過濾器(MyUrlFilter),來判斷當前訪問地址需要什么權限,然后將所需權限送給決策管理器(MyDecisionManager)進行判斷是否有權限。
@Autowired
MyUrlFilter myUrlFilter;
@Autowired
MyDecisionManager myDecisionManager;
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
public void configure(HttpSecurity http) throws Exception{
http.apply(new UrlAuthorizationConfigurer<>(http.getSharedObject(ApplicationContext.class)))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myDecisionManager);
o.setSecurityMetadataSource(myUrlFilter);
return o;
}
})
.and().formLogin().loginProcessingUrl("/login/process").loginPage("/login/page")
.and().logout().logoutUrl("/logout/page")
.and().sessionManagement().maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry())
.and().and().csrf().disable();
}
MyUrlFilter.java
@Component
public class MyUrlFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
AccessPermitService accessPermitService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//若當前頁面是登錄頁面,則直接放行,否則會進入死循環(huán),最終報重定向次數(shù)過多的錯誤
if(antPathMatcher.match("/login/**", requestUrl)) {
//權限數(shù)量為0時,不會調用AccessDecisionManager.decide()方法,無需登錄,直接放行
return SecurityConfig.createList(new String[0]);
}
//基于數(shù)據(jù)庫的動態(tài)權限,獲取整個系統(tǒng)的訪問路徑權限配置(建議緩存起來)
List<AccessPermit> accessPermits = accessPermitService.list();
//遍歷訪問路徑權限配置列表,判斷當前請求url和哪個訪問路徑配置匹配
for (AccessPermit accessPermit : accessPermits) {
//如果匹配上了,獲取這個訪問路徑的角色
if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){
String roles = accessPermit.getRoles();
//如果沒有設置角色,則視為需要登錄但不需要對應權限,設置一個默認權限給該訪問地址;否則根據(jù)逗號切分,返回對應的權限
if(roles.equals("")) {
return SecurityConfig.createList("login_required");
}
else{
return SecurityConfig.createList(roles.split(","));
}
}
}
//沒有匹配上,則視為需要登錄但不需要對應權限,設置一個默認權限給該訪問地址
return SecurityConfig.createList("login_required");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MyDecisionManager.java
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
//需要登錄但不需要權限時,myUrlFilter過濾器默認返回一個默認權限,需要進行特殊處理
if(configAttribute.getAttribute().equals("login_required")) {
//如果沒有登錄,則返回登錄頁面;否則用戶已登錄,直接放行
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("沒有登錄,請登錄!");
}
else {
return;
}
}
//需要權限的情況
for (GrantedAuthority authority : authorities) {
//判斷當前用戶是否有對應權限,有則放行
if(configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
//沒有權限則不放行
throw new AccessDeniedException("權限不足,無法訪問!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
LogoutController.java
@Controller
@RequestMapping("/logout")
public class LogoutController {
@Autowired
SessionRegistry sessionRegistry;
@RequestMapping("/page")
public String page(HttpSession session) {
SessionInformation sessionInformation = sessionRegistry.getSessionInformation(session.getId());
sessionInformation.expireNow();
return "redirect:login?logout";
}
}
現(xiàn)在改成這樣:
@Autowired
MyAuthorizationManager myAuthorizationManager;
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authz -> authz.anyRequest().access(myAuthorizationManager))
.formLogin(login -> login.loginProcessingUrl("/login/process").loginPage("/login/page").permitAll())
.logout(logout -> logout.logoutUrl("/logout/page").permitAll())
.sessionManagement(session -> session.maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry()))
.csrf(csrf -> csrf.disable());
return http.build();
}
MyAuthorizationManager.java
@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Autowired
AccessPermitService accessPermitService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
String requestUrl = context.getRequest().getServletPath();
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
//基于數(shù)據(jù)庫的動態(tài)權限,獲取整個系統(tǒng)的訪問路徑權限配置(建議緩存起來)
List<AccessPermit> accessPermits = accessPermitService.list();
//遍歷訪問路徑權限配置列表,判斷當前請求url和哪個訪問路徑配置匹配
for (AccessPermit accessPermit : accessPermits) {
//如果匹配上了,獲取這個訪問路徑的角色
if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){
String roles = accessPermit.getRoles();
//如果沒有設置角色,則視為需要登錄但不需要對應權限;否則根據(jù)逗號切分,返回對應的權限
if(roles.equals("")) {
break;
}
else{
for(String role : roles.split(",")) {
for (GrantedAuthority authority : authorities) {
//判斷當前用戶是否有對應權限,有則放行
if(role.equals(authority.getAuthority())){
return new AuthorizationDecision(true);
}
}
}
return new AuthorizationDecision(false);
}
}
}
if (authentication.get() instanceof AnonymousAuthenticationToken) {
return new AuthorizationDecision(false);
}
else return new AuthorizationDecision(true);
}
}
這里改動挺多的,一是使用lambda DSL的格式去寫相關代碼。二是現(xiàn)在只需要使用authorizeHttpRequests()方法配置一個自定義的授權管理器(MyAuthorizationManager)就可以了??梢岳斫鉃?strong>這個授權管理器(MyAuthorizationManager)取代了原來的訪問地址過濾器(MyUrlFilter)和決策管理器(MyDecisionManager)。三是現(xiàn)在的過濾器鏈是先經(jīng)過HttpSecurity 的過濾器再到授權管理器(MyAuthorizationManager)的,之前給登錄頁面放行的相關邏輯也不用自己實現(xiàn)了,但是formLogin和logout都要設置.permitAll()。四是logoutUrl(“/logout/page”)無需自行實現(xiàn)了,這個頁面與loginProcessingUrl(“/login/process”)一樣,已經(jīng)交由Security 托管了,自行實現(xiàn)也不會執(zhí)行。五是現(xiàn)在sessionRegistry會自動銷毀登出的會話了,也無需自行實現(xiàn)了。
(由于篇幅關系,這里就不貼AccessPermitService 的相關代碼了,大家按自己實際情況去實現(xiàn)即可)
總結
從Spring Boot 2.x升級到3.x肯定還有很多改動,是我這里沒列舉的,雖然改動挺多的,但還是建議能用最新的版本就用最新的版本。特別是Spring Security 升級到了6.x之后,代碼邏輯清晰了許多,不會像之前那樣繞到云里霧里,僅這點就值得了。
參考資料
Spring Session #2.7.15-SNAPSHOT
Spring Session #3.0.10-SNAPSHOT
Spring Security without the WebSecurityConfigurerAdapter
到此這篇關于Spring Boot 2.x升3.x的那些事的文章就介紹到這了,更多相關Spring Boot 2.x升3.x的那些事內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Windows上為Java配置多個版本的環(huán)境變量的操作指南
本文介紹了在Windows系統(tǒng)上配置多個Java版本環(huán)境變量的方法,包括配置CLASSPATH和PATH,設置多個JAVA_HOME變量,以及如何切換和驗證Java版本,通過合理配置,實現(xiàn)不同Java版本的高效切換,需要的朋友可以參考下2026-04-04
idea創(chuàng)建Spring項目的方法步驟(圖文)
這篇文章主要介紹了idea創(chuàng)建Spring項目的方法步驟(圖文),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01
Java 刪除與重置 Excel 文件密碼的完整示例(文件級與工作表級示例)
本文詳細介紹了如何使用Java實現(xiàn)Excel文件密碼的刪除與重置,涵蓋了文件和工作表級別的保護機制,通過使用Spire.XLSforJava組件,可以高效地完成這些操作,確保數(shù)據(jù)的安全性和系統(tǒng)的穩(wěn)定性,感興趣的朋友跟隨小編一起看看吧2026-02-02
maven導入本地倉庫jar包,報:Could?not?find?artifact的解決
這篇文章主要介紹了maven導入本地倉庫jar包,報:Could?not?find?artifact的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Java數(shù)據(jù)庫連接池之proxool_動力節(jié)點Java學院整理
Proxool是一種Java數(shù)據(jù)庫連接池技術。方便易用,便于發(fā)現(xiàn)連接泄漏的情況2017-08-08

