Spring?Boot?2.x升3.x的那些事
序言
手頭上有個(gè)項(xiàng)目,準(zhǔn)備從Spring Boot 2.x升級(jí)到3.x,升級(jí)后發(fā)現(xiàn)編譯器報(bào)了一堆錯(cuò)誤。一般來(lái)說(shuō)大版本升級(jí),肯定會(huì)有諸多問(wèn)題,對(duì)于程序開(kāi)發(fā)來(lái)說(shuō)能不升就不升。但是對(duì)于系統(tǒng)架構(gòu)來(lái)說(shuō),能用最新的肯定是用最新的,實(shí)在不行再降回去嘛??墒悄兀恢朗前l(fā)布沒(méi)多久,還是我搜索技巧的問(wèn)題,很多問(wèn)題在網(wǎng)上找不到答案。沒(méi)辦法,還是得自己研究,所以呢這次我們就一起來(lái)研究一下Spring Boot 3.x究竟有什么改變。
一、關(guān)于Spring Session
一般來(lái)說(shuō),如果一個(gè)Spring Boot 2.x項(xiàng)目一開(kāi)始只需要單實(shí)例部署,用不上redis共享會(huì)話的話,會(huì)在application.properties里加上這個(gè)參數(shù)。
spring.session.store-type=none
當(dāng)需要改為多實(shí)例部署,需要redis共享會(huì)話的時(shí)候,只需要改為這樣就行了。
spring.session.store-type=redis
但是在Spring Boot 3.x項(xiàng)目里,這個(gè)參數(shù)就不復(fù)存在了。查了Spring Session的官方文檔也沒(méi)有收獲。于是去翻Spring Boot的官方文檔,在2.x的參考文檔中有這么一條提示“You can disable Spring Session by setting the store-type to none.”。而在3.x的文檔中,這個(gè)提示被刪掉了。好家伙,原來(lái)store-type=none是直接禁用整個(gè)Spring Session,而不是Api文檔中所說(shuō)的"No session data-store."
那么解決辦法就很簡(jiǎn)單了,單實(shí)例部署,不需要用redis的時(shí)候,刪掉pom.xml里org.springframework.session的依賴(lài)就好。需要redis共享會(huì)話的時(shí)候就要把依賴(lài)加回去了,就是沒(méi)有原來(lái)修改配置文件來(lái)得方便而已。
二、關(guān)于redis
在application.properties里關(guān)于redis的配置也有所變化。如果你是這么配置redis的:
spring.redis.host=127.0.0.1 spring.redis.port=6379
這時(shí)編譯器就會(huì)警告你:“Property ‘spring.redis.host’ is Deprecated: Use ‘spring.data.redis.host’ instead.”、“Property ‘spring.redis.password’ is Deprecated: Use ‘spring.data.redis.password’ instead.”按照警告所說(shuō)的,把“spring.redis”替換成“spring.data.redis”即可。
spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379
三、關(guān)于servlet
由于tomcat 10包名的更換,如果你的程序是這么寫(xiě)的:
import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; ...
那么編譯器就會(huì)報(bào)"The import javax.servlet cannot be resolved"錯(cuò)誤。原因是包名從javax.servlet 調(diào)整為了jakarta.servlet 。解決辦法很簡(jiǎn)單,把javax.servlet 替換為 jakarta.servlet 即可。
import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; ...
四、關(guān)于thymeleaf模板
當(dāng)你使用片段表達(dá)式(fragment expression)而沒(méi)有使用“~{}”時(shí),會(huì)獲得運(yùn)行警告。例如你模板里這么寫(xiě):
<footer th:replace="footer::copy"></footer>
則會(huì)得到這樣的運(yùn)行警告:“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中,未封裝的片段表達(dá)式不再被推薦。解決方法也很簡(jiǎn)單,按照警告所說(shuō)的改為完整版的片段表達(dá)式,即加上“~{}”即可。
<footer th:replace="~{footer::copy}"></footer>
五、關(guān)于Spring Security
重點(diǎn)來(lái)了,隨著Spring Boot升級(jí)到3.x,Spring Security也升級(jí)到了6.x。話不多說(shuō),先來(lái)看看代碼,在6.x之前,如果你想要實(shí)現(xiàn)動(dòng)態(tài)權(quán)限,你的代碼可能會(huì)是這樣的:
@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里運(yùn)行,那么你需要這么寫(xiě):
@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(); } }
我們來(lái)逐個(gè)講解一下。
1.關(guān)于WebSecurityConfigurerAdapter
在Spring Security 6.x之前,我們通常是寫(xiě)一個(gè)配置類(lèi),繼承WebSecurityConfigurerAdapter 然后重寫(xiě)(@Override)對(duì)應(yīng)的方法來(lái)完成Security的配置的。而在Spring Security 6.x里WebSecurityConfigurerAdapter 已經(jīng)被棄用了,現(xiàn)在推薦使用的是基于組件的編碼方式,只要在配置類(lèi)里注冊(cè)對(duì)應(yīng)的組件(@Bean)即可。另外,使用組件配置時(shí)and()方法已經(jīng)不再推薦使用,官方建議使用lambda DSL。
2.關(guān)于UserDetailsService
按上面所說(shuō)的,下面這段代碼。
@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; }
但實(shí)際上只要你的用戶服務(wù)(MyUserService)實(shí)現(xiàn)了UserDetailsService接口,并且注冊(cè)到了Spring容器中(加了@Service或者@Component注解),Spring Security 6.x就會(huì)自動(dòng)綁定用戶服務(wù),只需注冊(cè)密碼加密組件即可。所以上面的代碼直接改成下面的就可以了。
@Bean protected PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
(由于篇幅關(guān)系,這里就不貼MyUserService的代碼了,自己按實(shí)際情況實(shí)現(xiàn)對(duì)應(yīng)接口功能就好)
3.關(guān)于WebSecurity
WebSecurity可以控制哪些地址不進(jìn)入Security過(guò)濾器鏈。原來(lái)的代碼是這么寫(xiě)的。
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/static/**"); }
現(xiàn)在,除了需要改為基于組件的寫(xiě)法外,antMatchers()方法也改成了requestMatchers()方法。
@Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().requestMatchers("/static/**"); }
4.關(guān)于HttpSecurity
原來(lái)在HttpSecurity中實(shí)現(xiàn)動(dòng)態(tài)權(quán)限,是先要寫(xiě)一個(gè)訪問(wèn)地址過(guò)濾器(MyUrlFilter),來(lái)判斷當(dāng)前訪問(wèn)地址需要什么權(quán)限,然后將所需權(quán)限送給決策管理器(MyDecisionManager)進(jìn)行判斷是否有權(quán)限。
@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(); //若當(dāng)前頁(yè)面是登錄頁(yè)面,則直接放行,否則會(huì)進(jìn)入死循環(huán),最終報(bào)重定向次數(shù)過(guò)多的錯(cuò)誤 if(antPathMatcher.match("/login/**", requestUrl)) { //權(quán)限數(shù)量為0時(shí),不會(huì)調(diào)用AccessDecisionManager.decide()方法,無(wú)需登錄,直接放行 return SecurityConfig.createList(new String[0]); } //基于數(shù)據(jù)庫(kù)的動(dòng)態(tài)權(quán)限,獲取整個(gè)系統(tǒng)的訪問(wèn)路徑權(quán)限配置(建議緩存起來(lái)) List<AccessPermit> accessPermits = accessPermitService.list(); //遍歷訪問(wèn)路徑權(quán)限配置列表,判斷當(dāng)前請(qǐng)求url和哪個(gè)訪問(wèn)路徑配置匹配 for (AccessPermit accessPermit : accessPermits) { //如果匹配上了,獲取這個(gè)訪問(wèn)路徑的角色 if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){ String roles = accessPermit.getRoles(); //如果沒(méi)有設(shè)置角色,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限,設(shè)置一個(gè)默認(rèn)權(quán)限給該訪問(wèn)地址;否則根據(jù)逗號(hào)切分,返回對(duì)應(yīng)的權(quán)限 if(roles.equals("")) { return SecurityConfig.createList("login_required"); } else{ return SecurityConfig.createList(roles.split(",")); } } } //沒(méi)有匹配上,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限,設(shè)置一個(gè)默認(rèn)權(quán)限給該訪問(wèn)地址 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) { //需要登錄但不需要權(quán)限時(shí),myUrlFilter過(guò)濾器默認(rèn)返回一個(gè)默認(rèn)權(quán)限,需要進(jìn)行特殊處理 if(configAttribute.getAttribute().equals("login_required")) { //如果沒(méi)有登錄,則返回登錄頁(yè)面;否則用戶已登錄,直接放行 if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("沒(méi)有登錄,請(qǐng)登錄!"); } else { return; } } //需要權(quán)限的情況 for (GrantedAuthority authority : authorities) { //判斷當(dāng)前用戶是否有對(duì)應(yīng)權(quán)限,有則放行 if(configAttribute.getAttribute().equals(authority.getAuthority())){ return; } } } //沒(méi)有權(quán)限則不放行 throw new AccessDeniedException("權(quán)限不足,無(wú)法訪問(wèn)!"); } @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ù)庫(kù)的動(dòng)態(tài)權(quán)限,獲取整個(gè)系統(tǒng)的訪問(wèn)路徑權(quán)限配置(建議緩存起來(lái)) List<AccessPermit> accessPermits = accessPermitService.list(); //遍歷訪問(wèn)路徑權(quán)限配置列表,判斷當(dāng)前請(qǐng)求url和哪個(gè)訪問(wèn)路徑配置匹配 for (AccessPermit accessPermit : accessPermits) { //如果匹配上了,獲取這個(gè)訪問(wèn)路徑的角色 if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){ String roles = accessPermit.getRoles(); //如果沒(méi)有設(shè)置角色,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限;否則根據(jù)逗號(hào)切分,返回對(duì)應(yīng)的權(quán)限 if(roles.equals("")) { break; } else{ for(String role : roles.split(",")) { for (GrantedAuthority authority : authorities) { //判斷當(dāng)前用戶是否有對(duì)應(yīng)權(quán)限,有則放行 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); } }
這里改動(dòng)挺多的,一是使用lambda DSL的格式去寫(xiě)相關(guān)代碼。二是現(xiàn)在只需要使用authorizeHttpRequests()方法配置一個(gè)自定義的授權(quán)管理器(MyAuthorizationManager)就可以了??梢岳斫鉃?strong>這個(gè)授權(quán)管理器(MyAuthorizationManager)取代了原來(lái)的訪問(wèn)地址過(guò)濾器(MyUrlFilter)和決策管理器(MyDecisionManager)。三是現(xiàn)在的過(guò)濾器鏈?zhǔn)窍冉?jīng)過(guò)HttpSecurity 的過(guò)濾器再到授權(quán)管理器(MyAuthorizationManager)的,之前給登錄頁(yè)面放行的相關(guān)邏輯也不用自己實(shí)現(xiàn)了,但是formLogin和logout都要設(shè)置.permitAll()。四是logoutUrl(“/logout/page”)無(wú)需自行實(shí)現(xiàn)了,這個(gè)頁(yè)面與loginProcessingUrl(“/login/process”)一樣,已經(jīng)交由Security 托管了,自行實(shí)現(xiàn)也不會(huì)執(zhí)行。五是現(xiàn)在sessionRegistry會(huì)自動(dòng)銷(xiāo)毀登出的會(huì)話了,也無(wú)需自行實(shí)現(xiàn)了。
(由于篇幅關(guān)系,這里就不貼AccessPermitService 的相關(guān)代碼了,大家按自己實(shí)際情況去實(shí)現(xiàn)即可)
總結(jié)
從Spring Boot 2.x升級(jí)到3.x肯定還有很多改動(dòng),是我這里沒(méi)列舉的,雖然改動(dòng)挺多的,但還是建議能用最新的版本就用最新的版本。特別是Spring Security 升級(jí)到了6.x之后,代碼邏輯清晰了許多,不會(huì)像之前那樣繞到云里霧里,僅這點(diǎn)就值得了。
參考資料
Spring Session #2.7.15-SNAPSHOT
Spring Session #3.0.10-SNAPSHOT
Spring Security without the WebSecurityConfigurerAdapter
到此這篇關(guān)于Spring Boot 2.x升3.x的那些事的文章就介紹到這了,更多相關(guān)Spring Boot 2.x升3.x的那些事內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解springboot + profile(不同環(huán)境讀取不同配置)
本篇文章主要介紹了springboot + profile(不同環(huán)境讀取不同配置),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Java中StringBuffer和StringBuilder區(qū)別
這篇文章主要介紹了Java中StringBuffer和StringBuilder區(qū)別,本文只介紹了它們之間的核心區(qū)別,需要的朋友可以參考下2015-06-06SpringMVC 通過(guò)commons-fileupload實(shí)現(xiàn)文件上傳功能
這篇文章主要介紹了SpringMVC 通過(guò)commons-fileupload實(shí)現(xiàn)文件上傳,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Java selenium截圖操作的實(shí)現(xiàn)
這篇文章主要介紹了Java selenium截圖操作的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Java替換(新增)JSON串里面的某個(gè)節(jié)點(diǎn)操作
這篇文章主要介紹了Java替換(新增)JSON串里面的某個(gè)節(jié)點(diǎn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11SpringMVC請(qǐng)求、響應(yīng)和攔截器的使用實(shí)例詳解
攔截器(Interceptor) 它是一個(gè)Spring組件,并由Spring容器管理,并不依賴(lài)Tomcat等容器,是可以單獨(dú)使用的,這篇文章給大家介紹SpringMVC請(qǐng)求、響應(yīng)和攔截器的使用,感興趣的朋友一起看看吧2024-03-03