前言:
为了提高服务器的性能,学习一下新特性,做降本增效,以及解决老版本中security的一些bug等....,但更多的是兴趣使然和大势所趋,故进行此项目。
虽然网上有了很多升级资料(在讲解过程中提供链接供参考),但由于项目复杂度以及技术栈的不同,也会在落地时要走不少坑,做下总结以及踩坑点供后人使用,如果有帮助到你那是我的荣幸。
主要是针对核心坑点做一下描述哦,如果有详细问题可以在沟通~
核心技术栈
springboot+springsecurity+dubbo+redis+mybatis+swagger
1. 本次升级带来的相关调整
原始版本 | 目标版本 | 备注 |
jdk1.8 | jdk17 | |
springboot 2.1.8 | springboot 3.3.1 | |
springsecurity 5.1.6 | springsecurity 6.3.1 | 配合boot升级,需要重写,踩坑大头 |
dubbo 2.7.3 | dubbo 3.2.16 | |
redis:jedis | redis:Lettuce | 配合boot升级,更改客户端使用框架默认的,且Lettuce并发场景下更适用 |
mybatis 2.x | mybatis3.0.3 | 非plus |
swagger 2.4.0 | knife 4.5.0 | 基于swagger的再次封装,个人认为更加好看&好用,微踩坑 |
logback 1.2.3 | logback 1.5.6 | |
lombok 1.18.12 | lombok 1.18.20 | |
nacos 2.0.0 | nacos 2.5.0 | 本地工具升级 |
2.针对技术栈坑点给予一些参考
一些简单的包依赖路径变更就不赘述了
可参考
springboot2.x升级到3.x实战经验总结_springboot2升级到3-优快云博客
核心讲一下 springsecurity , swagger的升级踩坑点
springSecurity
security经历了一次大版本迭代,其中实现方式与之前大相径庭但是基本概念还是如出一辙,什么handle filter的概念依旧存在,这些该集成实现的方法是没有改动的,不做更多赘述。
业务背景主要是用户账号密码登录以及某些场景下登陆设备数的校验,没有用到security中的鉴权概念。
旧版本:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthProperties authProperties;
@Autowired
private UsernamePasswordAuthSecurityConfig usernamePasswordAuthSecurityConfig;
@Autowired
@Qualifier(value = "defaultLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private DefaultExpiredSessionStrategy defaultExpiredSessionStrategy;
@Autowired
private SessionRegistry sessionRegistry;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage(authProperties.getAuthRequireUrl()).and().apply(usernamePasswordAuthSecurityConfig).and().authorizeRequests()
.antMatchers(authProperties.getIgnoreAuthUrlsArray()).permitAll().anyRequest().authenticated().and()
.sessionManagement().sessionFixation().migrateSession().maximumSessions(-1).maxSessionsPreventsLogin(false)
.expiredSessionStrategy(defaultExpiredSessionStrategy).sessionRegistry(sessionRegistry).and()
.sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()).and().logout()
.logoutUrl(authProperties.getLogoutUrl()).logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("你的cookie")
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringAntMatchers(authProperties.getIgnoreCsrfUrlsArray());
}
}
新版本:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@Slf4j
public class WebSecurityConfigV6 {
@Autowired
private AuthProperties authProperties;
@Autowired
private MyLoginSuccessHandler myLoginSuccessHandler;
@Autowired
private MyLoginFailHandler myLoginFailHandler;
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Autowired
private ObjectMapper objectMapper;
/**
* 注入AuthenticationManagerBuilder,用于配置认证提供者
*/
@Autowired
private AuthenticationManagerBuilder authenticationManagerBuilder;
/**
* 注入UsernamePasswordAuthenticationProvider,用于提供用户密码认证
*/
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
// @Bean
// public ChargeValidateHelper chargeValidateHelper() {
// return new ChargeValidateHelper();
// };
@Autowired
private MyExpiredSessionStrategy myExpiredSessionStrategy;
@Autowired
@Qualifier("userDefineSessionAuthenticationStrategy")
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
@Autowired
private AuthenticationBeanConfig authenticationConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
MyAuthenticationFilter myAuthenticationFilter, AuthenticationManager authenticationManager) throws Exception {
httpSecurity.authorizeHttpRequests(registry -> registry
// 放行的请求
.requestMatchers(authProperties.getIgnoreAuthUrlsArray()).permitAll().anyRequest().authenticated())
// 设置会话过期策略
// 将我们自己的usernamePasswordAuthenticationFilter添加到SpringSecurity的过滤器链中
.addFilterBefore(myAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 自定义的认证管理器
.authenticationManager(authenticationManager)
// 禁用表单登录
// .formLogin(AbstractHttpConfigurer::disable)
// 禁用httpBasic登录
// .httpBasic(AbstractHttpConfigurer::disable)
// 禁用rememberMe
// .rememberMe(AbstractHttpConfigurer::disable)
// 开启csrf验证
.csrf(csrf -> {
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers(authProperties.getIgnoreCsrfUrlsArray());
});
httpSecurity.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// .sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy())
.maximumSessions(-1).maxSessionsPreventsLogin(false).expiredSessionStrategy(myExpiredSessionStrategy));
httpSecurity.logout(body -> body.logoutUrl(authProperties.getLogoutUrl())
.logoutSuccessHandler(myLogoutSuccessHandler).deleteCookies("你的cookie").deleteCookies("cookie"));
httpSecurity.exceptionHandling(
body -> body.authenticationEntryPoint(new LoginUrlAuthenticationCustomEntryPoint(objectMapper)));
// httpSecurity.rememberMe((rememberMe) -> rememberMe.rememberMeServices(rememberMeServices()));
return httpSecurity.build();
}
/**
* 配置认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration,
BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
// 为安全管理器配置认证提供者,加入自定义的UsernamePasswordAuthenticationProvider,他使用list保存provider的所以可以添加多个provider
myAuthenticationProvider.setBCryptPasswordEncoder(bCryptPasswordEncoder);
myAuthenticationProvider.setSessionRegistry(authenticationConfig.sessionRegistry());
myAuthenticationProvider.setStringRedisTemplate(stringRedisTemplate);
authenticationManagerBuilder.authenticationProvider(myAuthenticationProvider);
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public String url() {
return authProperties.getPasswordLoginUrl();
}
/**
* 配置认证过滤器
*/
@Bean
public MyAuthenticationFilter myAuthenticationFilter(AuthenticationManager authenticationManager, String url) {
MyAuthenticationFilter authenticationFilter = new MyAuthenticationFilter(url);
// 设置认证管理器
authenticationFilter.setAuthenticationManager(authenticationManager);
// 设置登录成功和失败的处理器,分别对应AuthenticationSuccessHandler和AuthenticationFailureHandler,自行实现里面的方法就行,认证成功或失败时AbstractAuthenticationProcessingFilter会帮我们调用这两个处理器
authenticationFilter.setAuthenticationSuccessHandler(myLoginSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(myLoginFailHandler);
authenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
authenticationFilter.setSecurityContextRepository(securityContextRepository());
// authenticationFilter.setSecurityContextHolderStrategy();
// 必须是post请求
authenticationFilter.setPostOnly(true);
return authenticationFilter;
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository() {
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
// 自定义保存逻辑,例如 Redis
super.saveContext(context, request, response);
}
};
}
}
其中在做改造过程中主要面临三个问题
1. 如何框架识别自己重写的登录
答:网上很多都是在使用 formLogin 但是实际上在更多的场景下其实我们都是不会用官方默认的表单方式而都是需要自己实现的所以这个时候重写 AbstractAuthenticationProcessingFilter 其更好的便于我们扩展参数在框架中 通过@Bean的方式 把我们重写好的filter进行注入,其中注意因为securityContext 一定要自行@Bean进行注入否则会导致无法登录。
2. 如何实现登陆设备数量的限制
补充代码
@Configuration
public class AuthenticationBeanConfig {
@Autowired
private FindByIndexNameSessionRepository sessionRepository;
@Autowired
private ChargeValidateHelper chargeValidateHelper;
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(this.sessionRepository);
}
/**
* https://www.cnblogs.com/gocode/p/14814711.html
* https://www.cnblogs.com/LQBlog/p/14241787.html
* @return
*/
@Bean(name = "userDefineSessionAuthenticationStrategy")
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
MyConcurrentSessionControlAuthenticationStrategy controlAuthenticationStrategy =
new MyConcurrentSessionControlAuthenticationStrategy(sessionRegistry(), chargeValidateHelper);
controlAuthenticationStrategy.setExceptionIfMaximumExceeded(false);
// 要注意这3个实例在集合中的顺序
return new CompositeSessionAuthenticationStrategy(Arrays.asList(controlAuthenticationStrategy,
new ChangeSessionIdAuthenticationStrategy(), new RegisterSessionAuthenticationStrategy(sessionRegistry())));
}
}
3. 如何保证迁移之后用户的登录态不受影响
这块看了不少资料,官方也给出就是不可以兼容(后续的一些新版本会兼容小版本的),但让所有客户重新登陆一下是不友好的(可能我们眼里是个小问题,但他人不会),于是用了一些骚操作。
核心思路要确保在新版本框架下仍要使用旧版本的序列化id,否则无法使用旧版的sessionContext
看起来不错的方案(我没用):记spring-security升级,引发的redis反序列化不一致问题 - 程序员CRUD - 博客园
以下是我的方案:
3.1 保证重写了 AbstractAuthenticationToken 的类路径和命名跟老的一致
3.1 重写几个核心用到序列化id的类 (也可以通过在启动类中使用反射去修改)
SimpleGrantedAuthority,SecurityContextImpl,SpringSecurityCoreVersion,WebAuthenticationDetails,DefaultSavedRequest,SavedCookie,RedisSessionMapper
如图所示,通过这种操作来让框架读取类对应的序列号旧版ID,自己更改自己的老版本id即可,可能不同版本对应的类会有所差异可以根据序列化相关报错来补充。
swagger
这里简单描述一下就是,不要想着兼容老注解,不要想着兼容老注解,不要想着兼容老注解,我就为了图省事找了很多兼容方案结果访问 http://localhost:8080/doc.html 一直无法正常展示接口文档,最后一咬牙决定那就全都换成新的,其实全文替换也是简单的,可以参考文档。
Springboot2.x升级到3.x的经验分享 - 盗梦笔记 - 博客园
总结
主要是描述一些自己纠结很久的踩坑点,一些小的报错基本参考我提供的文档都可以解决掉,首次撰写此类总结,若存在表述偏差或认知局限,还望各位同仁不吝赐教。
相较于业务开发中产品逻辑的拆解与落地,技术项目对我而言更像是一场技术认知的突破之旅。从架构设计的全局视角到细节实现的深度打磨,每一个技术难点的攻克都带来强烈的成就感。这种通过技术创新推动系统演进的过程,正是我持续深耕的动力源泉。未来也将继续保持对技术的敬畏之心,在架构优化的道路上笃行致远。与君共勉!