随手记录第十话 -- 升级SpringBoot3.0 + JDK17的踩坑记录

2023-10-19 10:44

本文主要是介绍随手记录第十话 -- 升级SpringBoot3.0 + JDK17的踩坑记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

随着有些jar包的升级,JDK1.8已经不是最稳定的版本了。

前段时间接触到Web3相关,jar包的编译最低要JDK13了,碰巧另一个使用Kotlin写的jar包依赖需要17了,那就直接上17吧,同时Springboot也上到3.0。

1. 框架说明

Springboot3.0 + SpringSecurity + Swagger,数据库Jpa和mybatis都可,缓存使用redis。
先看主pom.xml依赖

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.0.0</version><relativePath/>
</parent>

依赖项

<!--Spring boot 安全框架--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<!-- Swagger UI 相关 2.0.4版本存在问题 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

服务之间调用采用openfeign,<openfeign.version>12.2</openfeign.version>

<!--Spring boot openfeign-->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-core</artifactId><version>${openfeign.version}</version>
</dependency>
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId><version>${openfeign.version}</version>
</dependency>
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-jackson</artifactId><version>${openfeign.version}</version>
</dependency>
<!--spring契约-->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-spring4</artifactId><version>${openfeign.version}</version>
</dependency>

至于其他的依赖JWT,Redis,Jpa,Mybatis就不贴了

2. SpringSecurity配置

和2.x版本的变化的还是挺大的,3.0采用的是是流式写法。

2.1 验证规则

public class SpringSecurityConf {@AutowiredApplicationContext applicationContext;@Beanpublic PasswordEncoder passwordEncoder() {// 密码加密方式return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic JwtTokenOncePerRequestFilter authenticationJwtTokenFilter() {return new JwtTokenOncePerRequestFilter();}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 获取匿名标记Map<String, Set<String>> anonymousUrls = this.getAnonymousUrl();http//禁言basic明文.httpBasic().disable()//前后端分离不用卡跨域.csrf().disable()//禁用默认登录页.formLogin().disable()//授权异常.exceptionHandling(exceptions -> {//403处理类 无法访问  401处理类 身份信息验证错误exceptions.authenticationEntryPoint(getAuthenticationEntryPoint()).accessDeniedHandler(getAccessDeniedHandler());})//http请求.authorizeHttpRequests(registry -> {registry.requestMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll().requestMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll().requestMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll().requestMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll().requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();//添加白名单的this.addWhiteListAuthorize(registry);//添加自定义的this.addCustomAuthorize(registry);//默认全部放行
//                    registry.requestMatchers("/**").permitAll();// 所有请求都需要认证registry.anyRequest().authenticated();})//验证service类
//                .authenticationProvider().addFilterBefore(this.getDefaultFilter(), UsernamePasswordAuthenticationFilter.class);return http.build();}//可重写该public void addWhiteListAuthorize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {String[] PASS_PATH_GROUP = new String[]{"/*.html","/webSocket/**","/swagger-ui/*","/*/api-docs/*","/*/api-docs*","/actuator/*","/error",};for (String str : PASS_PATH_GROUP) {registry.requestMatchers(str).permitAll();}}//例如system有不同的处理 可重写该方法public void addCustomAuthorize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {//hasRole 需要在权限面前加上ROLE_ hasAuthority/则不需要registry.requestMatchers("/api/**").hasAuthority(SecurityConstant.AUTH_USER).requestMatchers("/admin/**").hasAuthority(SecurityConstant.AUTH_MANAGER);}public AccessDeniedHandler getAccessDeniedHandler() {return (request, response, e) -> {log.error("403 Forbidden,URI:{}", request.getRequestURI());//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());};}public AuthenticationEntryPoint getAuthenticationEntryPoint() {return (request, response, e) -> {log.error("401 无凭据,{},URI:{}", request.getMethod(), request.getRequestURI());// 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e == null ? "Unauthorized" : e.getMessage());};}//默认的jwt校验器public JwtTokenOncePerRequestFilter getDefaultFilter() throws Exception {return new JwtTokenOncePerRequestFilter();}//自定义授权开放的注解 例如AnonymousGetMapping 注解了的方法放开权限public Map<String, Set<String>> getAnonymousUrl() {// 搜寻匿名标记 url: @AnonymousAccessRequestMappingHandlerMapping requestMappingHandlerMapping =(RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();// 获取匿名标记Map<String, Set<String>> anonymousUrls = new HashMap<>(6);Set<String> get = new HashSet<>();Set<String> post = new HashSet<>();Set<String> put = new HashSet<>();Set<String> patch = new HashSet<>();Set<String> delete = new HashSet<>();Set<String> all = new HashSet<>();for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {HandlerMethod handlerMethod = infoEntry.getValue();AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);if (null != anonymousAccess) {List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());Set<PathPattern> pathPatterns = infoEntry.getKey().getPathPatternsCondition().getPatterns();for (PathPattern pattern : pathPatterns) {switch (Objects.requireNonNull(request)) {case GET:get.add(pattern.getPatternString());break;case POST:post.add(pattern.getPatternString());break;case PUT:put.add(pattern.getPatternString());break;case PATCH:patch.add(pattern.getPatternString());break;case DELETE:delete.add(pattern.getPatternString());break;default:all.add(pattern.getPatternString());break;}}}}anonymousUrls.put(RequestMethodEnum.GET.getType(), get);anonymousUrls.put(RequestMethodEnum.POST.getType(), post);anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);return anonymousUrls;}
}

2.2 拦截器

每个请求在满足权限之后都会走的入口类

public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {RedisConf redisConf;public JwtTokenOncePerRequestFilter() {}//如果有自定义拦截请求处理 //则重写验证规则中的getDefaultFilter方法 传入自定义拦截器public JwtTokenOncePerRequestFilter(RedisConf redisConf) {this.redisConf = redisConf;}@Overrideprotected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,FilterChain chain) throws ServletException, IOException {String token = JwtUtils.getToken(req);if (token == null) {//可能会存在前端session缓存问题SecurityContextHolder.getContext().setAuthentication(null);chain.doFilter(req, res);return;}try {//验证token 并取出对应信息Authentication authentication = JwtUtils.getAuthentication(token);SecurityContextHolder.getContext().setAuthentication(authentication);//子类自定义操作 例如 存储,redis过期/续期this.handler(token, authentication);chain.doFilter(req, res);} catch (Exception e) {GlobalExceptionEnum ex = GlobalExceptionEnum.TOKEN_IS_ERROR;logger.error(e.getMessage());res.setContentType("application/json; charset=utf-8");res.setStatus(HttpStatus.UNAUTHORIZED.value());res.getWriter().write("{\"code\": " + HttpStatus.UNAUTHORIZED.value() + ", \"message\": \"" + ex.getText() + "\"}");res.getWriter().close();}}public void handler(String token, Authentication authentication) {if (redisConf != null && JwtUtils.containsRole(SecurityConstant.AUTH_USER)) {//仅针对用户服务做拦截 减少redis访问 对于 Token 为空的不需要去查Redisif (StringUtils.isNotEmptyObj(token)) {Long uid = JwtUtils.getLoginUid();String oldToken = redisConf.get(String.format(RedisKey.MEMBER_UID_TOKEN, uid));if (oldToken != null && !token.equals(oldToken)) {logger.error("token不一致:token:" + token + ",oldToken:" + oldToken);throw new BadRequestException(GlobalExceptionEnum.TOKEN_IS_ERROR.getText());}}}}
}

2.3 验证器

从验证规则来看,上面的验证器我是注释掉了的,因为登录注册是由自己实现,不是走的SpringSecurity的密码验证。这里也贴一下代码

//用账户密码登录的 会传入的明文密码 根据验证规则里面设置的加密算法进行加密
UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);

经过框架内部加密密码后,会回调到UserDetailsService#loadUserByUsername方法,该方法需要自己实现

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {private final UserService userService;private final RoleService roleService;private final DataService dataService;private final LoginProperties loginProperties;@Overridepublic JwtUserDto loadUserByUsername(String username) {boolean searchDb = true;JwtUserDto jwtUserDto = null;if (searchDb) {UserDto user;try {user = userService.findByName(username);} catch (EntityNotFoundException e) {// SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsExceptionthrow new UsernameNotFoundException("", e);}if (user == null) {throw new UsernameNotFoundException("");} else {if (!user.getEnabled()) {throw new BadRequestException("账号未激活!");}jwtUserDto = new JwtUserDto(user,dataService.getDeptIds(user),roleService.mapToGrantedAuthorities(user));userDtoCache.put(username, jwtUserDto);}}return jwtUserDto;}
}

在这里面验证状态等,同时补齐权限,然后返回对应的dto,框架会根据dto里面的password来校验密码是否正确。

3. Swagger变化

在Springboot3中,原来的swagger-ui包已经不支持了,需要更改为springdoc

3.1 依赖

在最开始使用的2.0.4版本中,会出现@RequestParam(required = false)在swagger界面中不生效的问题。

<!-- Swagger UI 相关 2.0.4版本存在问题 -->
<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3.2 配置

和2版本相比,如果想添加Authorization,需要在类上添加如下注解

@Configuration
@SecurityScheme(name = "Authorization", type = SecuritySchemeType.HTTP, scheme = "bearer", in = SecuritySchemeIn.HEADER)
public abstract class SwaggerConf {@Beanpublic OpenAPI createOpenApi() {return new OpenAPI().info(apiInfo())//设置授权信息.security(List.of(new SecurityRequirement().addList("Authorization")))//引入其他文档
//                .externalDocs(new ExternalDocumentation().description("百度一下")
//                        .url("http://www.baido.com"));}public Info apiInfo() {return new Info().title("一款 web3.0 业务接口文档").description("web3业务处理").version("1.0");}//分组@Beanpublic GroupedOpenApi publicApi() {return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build();}@Beanpublic GroupedOpenApi adminApi() {return GroupedOpenApi.builder().group("admin").pathsToMatch("/admin/**").build();}
}

其他的配置大同小异。

3.3 注解的使用

原来的版本的注解我就不贴了,先看类上和方式上的注解

@Tag(name = "APP:FTMO数据相关接口")
@RestController
@RequestMapping("/api/app")
public class CmsController {//get参数的注解自行查询@Operation(summary = "test测试", description = "test接口说明")@AnonymousGetMapping("/test")public UserInfo test(@RequestBody UserInfoVO vo) {}
}

在实体类上的注解

@Schema(title = "用户数据VO")
@Data
public class UserInfoVO extends UserInfo {@Schema(title = "页码查询")Integer page;@Schema(title = "条数")Integer pageSize;
}

需要注意的是在同一规范返回的实体类上不用注解

@NoArgsConstructor
@Data
//加了这个注解回导致无法显示泛型的实体类
//@Schema(name = "ApiResult", description = "REST接口标准返回值 View Model")
public class ApiResultVO<T> implements Serializable {/*** 返回码*/@Schema(title ="REST接口返回码")private Integer code;/*** 返回描述*/@Schema(title ="REST接口返回消息")private String message;/*** 返回数据*///这个DataVO类上不影响private DataVO<T, Object> content;
}

3.4 效果图

在这里插入图片描述
传入参数说明
在这里插入图片描述
返回参数实体类说明
在这里插入图片描述

4.其他

配置文件redis,移动多了一层data目录

  data:redis:database: 0#数据库索引host: 192.168.0.100port: 6379password:#连接超时时间timeout: 5000

过去有一段时间了,今天才抽时间记录一下,还有其他问题会继续更新。
以上就是本文的全部内容了!

上一篇:随手记录第九话 – Java框架整合篇
下一篇:随手记录第十一话 – xxx

非学无以广才,非志无以成学。

这篇关于随手记录第十话 -- 升级SpringBoot3.0 + JDK17的踩坑记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/239245

相关文章

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件

macOS升级后SVN升级

问题 svn: error: The subversion command line tools are no longer provided by Xcode. 解决 sudo chown -R $(whoami) /usr/local/Cellar brew install svn

记录每次更新到仓库 —— Git 学习笔记 10

记录每次更新到仓库 文章目录 文件的状态三个区域检查当前文件状态跟踪新文件取消跟踪(un-tracking)文件重新跟踪(re-tracking)文件暂存已修改文件忽略某些文件查看已暂存和未暂存的修改提交更新跳过暂存区删除文件移动文件参考资料 咱们接着很多天以前的 取得Git仓库 这篇文章继续说。 文件的状态 不管是通过哪种方法,现在我们已经有了一个仓库,并从这个仓

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

perl的学习记录——仿真regression

1 记录的背景 之前只知道有这个强大语言的存在,但一直侥幸自己应该不会用到它,所以一直没有开始学习。然而人生这么长,怎就确定自己不会用到呢? 这次要搭建一个可以自动跑完所有case并且打印每个case的pass信息到指定的文件中。从而减轻手动跑仿真,手动查看log信息的重复无效低质量的操作。下面简单记录下自己的思路并贴出自己的代码,方便自己以后使用和修正。 2 思路整理 作为一个IC d

Golang支持平滑升级的HTTP服务

前段时间用Golang在做一个HTTP的接口,因编译型语言的特性,修改了代码需要重新编译可执行文件,关闭正在运行的老程序,并启动新程序。对于访问量较大的面向用户的产品,关闭、重启的过程中势必会出现无法访问的情况,从而影响用户体验。 使用Golang的系统包开发HTTP服务,是无法支持平滑升级(优雅重启)的,本文将探讨如何解决该问题。 一、平滑升级(优雅重启)的一般思路 一般情况下,要实现平滑

SSM项目使用AOP技术进行日志记录

本步骤只记录完成切面所需的必要代码 本人开发中遇到的问题: 切面一直切不进去,最后发现需要在springMVC的核心配置文件中中开启注解驱动才可以,只在spring的核心配置文件中开启是不会在web项目中生效的。 之后按照下面的代码进行配置,然后前端在访问controller层中的路径时即可观察到日志已经被正常记录到数据库,代码中有部分注释,看不懂的可以参照注释。接下来进入正题 1、导入m

JeecgBoot 升级springboot版本到2.6.0

1. 环境描述 Jeecgboot 3.0,他所依赖的springboot版本为2.3.5Release,将springboot版本升级为2.6.0。过程全纪录,从2开始描述。 2. 修改springboot版本号 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-pare

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位 一、背景二、定位问题三、解决方法 一、背景 flume系列之:定位flume没有关闭某个时间点生成的tmp文件的原因,并制定解决方案在博主上面这篇文章的基础上,在机器内存、cpu资源、flume agent资源都足够的情况下,flume agent又出现了tmp文件无法关闭的情况 二、

欧拉系统 kernel 升级、降级

系统版本  cat  /etc/os-release  NAME="openEuler"VERSION="22.03 (LTS-SP1)"ID="openEuler"VERSION_ID="22.03"PRETTY_NAME="openEuler 22.03 (LTS-SP1)"ANSI_COLOR="0;31" 系统初始 kernel 版本 5.10.0-136.12.0.