尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot3+Security6+JWT+Vben5.0企业级权限管理后台搭建实战

SpringBoot3+Security6+JWT+Vben5.0企业级权限管理后台搭建实战 SpringBoot3 发布之后我一直想把手里的老管理系统整体升级一遍。以前用的是 SpringBoot2 Vue2 的组合开发效率谈不上差但随着项目规模变大前后端分离的边界越来越模糊细粒度权限、多端登录、动态菜单这些东西全靠自己造轮子维护成本实在有点高。后来看到 Vben5.0 这个前端方案加上 SpringBoot3 对 Security6 和 OAuth2 的支持已经非常成熟我决定干脆从零搭一套企业级管理后台的样板工程。这篇文章就是我从零开始搭建的完整记录包含核心代码实现、踩坑记录和最终落地效果给同样想升级技术栈的朋友一个参考。这套东西解决的核心问题是一套开箱即用的前后端分离方案。后端用 SpringBoot3 统一提供 RESTful API集成 Security6 OAuth2 JWT 搞定认证授权前端基于 Vben5.0 快速搭建中后台框架把动态路由、按钮级权限、多标签页这些通用能力直接复用开发业务页面时只用关心表单和表格不用再从零啃前端工程化。如果你正准备上手 SpringBoot3或者对 Vben5.0 感兴趣但不知道怎么跟后端对接这篇文章应该能帮你省下不少时间。下面按照我的实际操作顺序逐个环节拆开讲。1. 项目背景与技术选型思路1.1 为什么选 SpringBoot3 而不是 SpringBoot2先说一个很多人纠结的问题项目还在用 SpringBoot2到底有没有必要升级到 SpringBoot3我的答案是新项目直接用 SpringBoot3老项目如果不是特别复杂的场景也应该尽早规划迁移。最核心的原因是 SpringBoot3 基于 Spring Framework6底层是 Jakarta EE9 规范把原来的javax.*包全部换成了jakarta.*。这意味着很多老代码里的javax.servlet、javax.validation导包在升级后会直接编译报错但反过来看这种破坏性升级反而逼着你梳理业务代码把历史包袱清一遍。另外SpringBoot3 最低要求 JDK17这带来的是 ZGC、虚拟线程这些实在的收益接口响应时间在压测下能肉眼可见地改善。我这次选的版本是 SpringBoot 3.2.x。之所以没有追求最新 3.3 或 3.4是因为企业级项目还是要看生态兼容性3.2 这一代对 Spring Security6、MyBatis-Plus、Sa-Token 等常用组件的支持已经非常稳定社区踩坑资料也足够多。1.2 为什么选 Vben5.0 作为前端基座前端选型这件事上我对比了好几个方案包括 RuoYi-Vue3、vue-element-plus-admin、还有原生 Vue3 Vite 手搭。最终选定 Vben5.0主要看中三个点。第一是工程化程度高。Vben5.0 基于 Vue3 Vite5 TypeScript Ant Design Vue内置了统一的代码规范、Git 提交校验、自动导入组件、多环境打包配置团队协作时不需要每个人各自配一套开发环境。第二是动态权限模型成熟。Vben5.0 提供了完整的动态路由方案后端可以根据用户角色返回菜单列表前端在路由守卫里动态注册页面组件。这套机制跟我要做的 RBAC 权限模型基本上是天然匹配的不需要做二次设计。第三是组件和 hooks 质量高。比如useTable、useForm这一层封装实际开发中能省掉大量重复的表格查询、分页、表单校验代码。一个标准 CRUD 页面从写接口到页面完成我实测下来半小时左右能搞定。1.3 整体架构与技术栈清单先画出我这套系统的整体分层结构不用图直接用文字描述前端Vben5.0Vue3 Vite Ant Design Vue Pinia Vue Router后端SpringBoot3Java17 Spring Web Spring Security6 OAuth2 Resource Server JWT数据访问MyBatis-Plus动态 SQL 方便兼容老项目习惯数据库MySQL 8.0后续可以平滑切换到 PostgreSQL缓存与 Token 黑名单RedisSpring Data Redis接口文档knife4j基于 OpenAPI3配合 SpringDoc整个请求链路是这样的前端 Vben5.0 发起登录请求后端校验用户名密码后颁发 JWT 令牌前端把令牌存到 Pinia 和 localStorage 里后续所有请求在 Axios 拦截器中自动携带后端通过 Spring Security6 的过滤器链校验 JWT 并解析出用户信息和权限集合每个接口通过PreAuthorize注解做方法级权限控制或者通过自定义权限校验器做细粒度数据权限控制。这套架构不算新但胜在稳定、边界清晰实际运行中没有出现框架层面的未知坑。2. 后端工程搭建与核心配置2.1 初始化 SpringBoot3 工程创建工程这一步有很多种方式Idea 自带的 Spring Initializr、Spring 官网的 start.spring.io或者直接在脚手架里选。这里我给出我实际使用的关键依赖配置用 Maven 的pom.xml来说明。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version jjwt.version0.12.5/jjwt.version springdoc.version2.5.0/springdoc.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-resource-server/artifactId /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-jose/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version${jjwt.version}/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version${jjwt.version}/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version${jjwt.version}/version scoperuntime/scope /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version${springdoc.version}/version /dependency /dependencies有两个点要特别提醒一是 MyBatis-Plus 从 3.5.3 之后已经专门提供了mybatis-plus-spring-boot3-starter如果你还在用老版本的mybatis-plus-boot-starter在 SpringBoot3 下会直接启动失败别被这个问题浪费一晚上二是 OAuth2 相关的依赖不是spring-boot-starter-oauth2-resource-server需要分别引入resource-server和jose两个模块缺少jose的话 JWT 解码器会报NoSuchAlgorithmException。2.2 整合 Spring Security6 OAuth2 资源服务器Spring Security6 的配置方式和 5.x 差别非常大最直观的就是WebSecurityConfigurerAdapter被彻底移除了现在必须用SecurityFilterChain的 Bean 方式配置。看代码Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { Bean Order(1) public SecurityFilterChain permitAllFilterChain(HttpSecurity http) throws Exception { http .securityMatcher(/auth/login, /auth/captcha, /doc.html, /webjars/**, /v3/api-docs/**) .authorizeHttpRequests(auth - auth.anyRequest().permitAll()) .csrf(AbstractHttpConfigurer::disable); return http.build(); } Bean Order(2) public SecurityFilterChain apiFilterChain(HttpSecurity http, JwtAuthConverter jwtAuthConverter) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt.jwtAuthenticationConverter(jwtAuthConverter)) ); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } }这里有一个比较重要的设计思路我把登录接口和其他公开接口单独剥离到一个permitAll过滤链里避免认证接口本身被资源服务器的 JWT 校验拦截。Order注解决定了过滤链的执行顺序第一条链匹配securityMatcher中的路径就结束匹配不会进入第二条链。实际上这种写法在处理验证码、登录、文档等公开接口时非常干净不用跟任何权限配置扯上关系。EnableMethodSecurity是 Spring Security6 里替代EnableGlobalMethodSecurity的新注解开启之后才能用PreAuthorize、Secured这些方法级权限控制。2.3 基于 RBAC 的权限模型设计企业级管理后台绕不开权限模型我这里选了经典的 RBACRole-Based Access Control基于角色的访问控制模型五张核心表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户表和角色表是一对多的关系通过用户角色关联表中间表连接角色表和菜单表是多对多的关系通过角色菜单关联表连接。这种设计的核心思想是用户不直接跟权限挂钩而是通过角色间接获得权限。好处是权限变更时只需要调整角色和菜单的关系不需要逐个用户去修改权限配置。建表的时候有两个细节值得说一下逻辑删除字段用户、角色这些业务数据不要物理删除加一个deleted字段用逻辑删除。企业系统里误删数据的成本非常高逻辑删除是底线。create_time / update_time 字段所有表统一加这两个字段由 MyBatis-Plus 的自动填充功能处理避免在每个 Service 里手动 set。菜单表需要支持树形结构字段设计上包含parent_id、menu_type目录、菜单、按钮、path、component、icon、sort_order等。Vben5.0 的动态路由就是根据这张表的数据生成的菜单表里menu_type为目录和菜单的记录会生成前端路由按钮类型的记录只用于控制按钮级权限不产生路由。2.4 统一响应体与全局异常处理前后端分离的项目里接口返回结构必须统一否则前端 Axios 拦截器没法做统一的错误处理。我封装的统一响应体结构如下public class ResultT { private int code; private String message; private T data; private long timestamp; public static T ResultT ok(T data) { return new Result(0, success, data, System.currentTimeMillis()); } public static T ResultT fail(int code, String message) { return new Result(code, message, null, System.currentTimeMillis()); } }这里把正常状态码定为 0业务错误码从 10001 开始递增HTTP 状态码仍然承担传输层语义。因为如果大量使用 2000 这种自定义状态码nginx 层容易把错误请求当成正常请求处理反而掩盖了问题。全局异常处理用RestControllerAdvice核心逻辑是拦截三类异常业务异常BusinessException主动抛出比如用户不存在密码错误返回具体的业务错误码参数校验异常MethodArgumentNotValidException把 Spring Validation 的字段错误信息拼装成可读信息返回兜底异常Exception统一记录 ERROR 日志并返回系统繁忙避免把堆栈信息直接暴露给前端在真实项目中这个全局异常处理层是整个后端稳定性的第一道防线一定要在项目初期就搭好否则后期每个接口都自己 try-catch代码冗余程度会非常可怕。3. 认证与授权链路从 Token 到接口权限3.1 登录认证流程拆解这一块的完整流程设计起来并不复杂但里面涉及的链路比较长。我把登录认证的整个时序拆解成下面这几步前端请求/auth/captcha获取图形验证码后端生成图片并返回captchaId同时把验证码答案存到 Redis有效期 120 秒前端提交登录表单用户名、密码、验证码、captchaId到/auth/login后端先检查 Redis 中的验证码不匹配直接返回业务异常通过AuthenticationManager执行认证内部调用自定义的UserDetailsService加载用户信息用PasswordEncoder校验密码认证成功后生成两个 TokenaccessToken有效期 2 小时和 refreshToken有效期 7 天返回 Token、用户基本信息、权限标识列表给前端这里为什么一定要加验证码环节因为登录接口如果不做任何防护非常容易被暴力破解工具扫。验证码用了 Redis 存储 一次性消费验证过一次就立即删除能有效阻断大部分自动化攻击。3.2 自定义 UserDetailsService 与认证管理器Spring Security 的AuthenticationManager本身只负责认证流程编排真正从数据库加载用户信息的是UserDetailsService实现类。我的实现大致是这样的Service RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final SysUserMapper userMapper; private final MenuMapper menuMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user userMapper.selectByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } if (user.getStatus() ! 1) { throw new DisabledException(账号已被禁用); } ListString permissions menuMapper.selectPermsByUserId(user.getId()); ListGrantedAuthority authorities permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new LoginUser(user, authorities); } }我这里返回的LoginUser是自定义的UserDetails实现类除了 Spring Security 需要的账号密码和权限列表之外还塞了用户 ID、部门 ID、昵称这些业务字段。这样后续业务代码里通过SecurityContextHolder拿当前登录人信息时可以直接强转成LoginUser不需要再去数据库查一遍。这个小优化在接口频繁开发时能省不少事。3.3 JWT 生成、解析与过滤器链JWT 本身是无状态的服务端不需要存储会话信息但代价是没法主动让 Token 失效。为了解决这个痛点我引入了 Redis Token 黑名单机制用户退出登录时把当前 Token 的 jti 加上剩余有效期存到 Redis后续每次请求校验 JWT 时先查一下黑名单命中则直接拒绝。Component RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final TokenService tokenService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null tokenService.validateToken(token)) { LoginUser loginUser tokenService.parseToken(token); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }Spring Security6 里这段过滤器需要注册到 SecurityFilterChain 上在http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)位置插入。整个过滤器链的顺序是先走到自定义的 JWT 过滤器解析出用户信息后设置到 SecurityContext后续的接口权限判断才能拿到完整的认证信息。3.4 动态权限控制与按钮级权限接口级权限我用的是PreAuthorize注解PreAuthorize(hasAuthority(system:user:add)) PostMapping(/admin/users) public ResultVoid addUser(Valid RequestBody SysUserDTO dto) { userService.addUser(dto); return Result.ok(null); }这里hasAuthority(system:user:add)中的权限标识就是上一节里UserDetailsService加载出来的权限字符串。登录用户拥有某个权限标识才能调用对应的接口。这种控制的优势在于权限判断粒度可以深入到单个按钮操作而不是只停留在路由层面。Vben5.0 前端也会根据接口返回的权限标识列表决定页面上某个按钮显示还是隐藏。前后端双重控制接口层面防绕过前端层面提升交互体验。4. 前端工程搭建与 Vben5.0 实战4.1 初始化 Vben5.0 工程Vben5.0 的官方文档提供了创建项目的脚手架命令我这里直接用了 pnpm 方式pnpm create vbenlatest my-admin --template vben5 cd my-admin pnpm install pnpm dev初始化完成后的项目结构很清晰核心目录是src下面有api接口定义、components通用组件、layouts布局、router路由配置、storesPinia 状态、views页面等。Vben5.0 开箱就带了不少好东西深色模式、多主题切换、国际化i18n、多标签页、面包屑、全屏等功能都是配置项直接开启就能用的。我实际用下来初始化工程的体积虽然不小但 Vite 的开发调试速度很给力热更新基本是秒级响应。4.2 接入后端 API 与请求封装Vben5.0 内置了基于 Axios 的请求封装提供了requestClient这个对象。我们开发时只需要在src/api目录下定义接口函数。下面是我封装登录接口的实际代码import { requestClient } from #/api/request; export namespace AuthApi { export interface LoginParams { username: string; password: string; captchaId: string; captchaCode: string; } export interface LoginResult { accessToken: string; refreshToken: string; userInfo: { userId: number; username: string; nickname: string; avatar: string; roles: string[]; permissions: string[]; }; } export async function login(data: LoginParams) { return requestClient.postLoginResult(/auth/login, data); } export async function getCaptcha() { return requestClient.get{ captchaId: string; captchaImg: string }(/auth/captcha); } export async function logout() { return requestClient.post(/auth/logout); } }请求封装的拦截器里做了几件重要的事请求拦截从 Pinia 中读取 accessToken通过 headers 的Authorization: Bearer token携带响应拦截遇到 HTTP 401 时自动跳转到登录页并清空本地用户信息响应拦截后端返回的code不为 0 时统一弹出 Ant Design Vue 的message.error提示4.3 登录页改造与 Token 存储Vben5.0 自带的登录页已经比较完整但我们后端用的是自定义验证码需要把默认的表单字段和验证码逻辑替换掉。我改造后的Login.vue核心逻辑是这样的const formState reactive({ username: , password: , captchaId: , captchaCode: , }); async function handleLogin() { await form.validate(); const loading message.loading(登录中..., 0); try { const result await AuthApi.login(formState); // 存储 token 到 Pinia 和 localStorage authStore.setToken(result.accessToken); authStore.setUserInfo(result.userInfo); // 加载动态菜单 await menuStore.generateMenus(result.userInfo.permissions); router.push(/); } finally { loading(); } }Token 存储这里要特别说明Vben5.0 默认会把 Token 存在 localStorage同时用 Pinia 做运行时状态管理。从安全性角度看localStorage 存在 XSS 风险但 SPA 应用纯前端无法完全避免解决方案是把敏感信息控制在最小范围并且加小程序级别的接口鉴权、操作日志审计作为兜底。4.4 动态菜单与路由守卫Vben5.0 里的动态路由机制是跟我后端权限模型衔接最紧密的部分。整体思路是前端在路由守卫中判断当前用户是否已登录未登录则跳转登录页已登录但尚未生成过动态路由时调用后端接口获取菜单列表将菜单列表转换为 Vben5.0 的路由配置通过router.addRoute动态注册注册完成后使用next({ ...to, replace: true })重新进入当前路由Vben5.0 对动态路由的组件映射做了统一约定后端菜单表里的component字段存的是views/system/user/index这种路径形式前端通过import.meta.glob批量注册 view 目录下的所有组件再根据路径动态解析。路由守卫的核心代码结构如下router.beforeEach(async (to) { const userStore useUserStore(); const menuStore useMenuStore(); if (!userStore.isLogin) { if (to.path /login) return true; return { path: /login, query: { redirect: to.fullPath } }; } if (userStore.isLogin !menuStore.isDynamicRouteLoaded) { await menuStore.generateDynamicRoutes(); return { path: to.fullPath, replace: true }; } return true; });这套设计的核心体验是刷新页面后前端会重新向后端请求菜单和权限保证用户权限变更是立即生效的不需要重新登录。4.5 页面与组件的落地实践Vben5.0 的useTable和useForm这两个 hooks 组合是开发效率提升最明显的地方。我以一个标准的用户管理页面为例核心代码雏形如下const [registerTable, { reload, getForm }] useTable({ title: 用户列表, api: fetchUserPage, columns: [ { title: 用户名, dataIndex: username }, { title: 昵称, dataIndex: nickname }, { title: 状态, dataIndex: status, slots: { customRender: status } }, { title: 创建时间, dataIndex: createTime }, { title: 操作, dataIndex: action, slots: { customRender: action } }, ], formConfig: { schemas: [ { field: keyword, label: 关键词, component: Input, labelWidth: 80 }, ], }, pagination: { current: 1, pageSize: 10, }, });useTable会在组件挂载时自动调用fetchUserPage这个入参函数并直接绑定后端分页接口的请求格式。翻页、搜索、刷新这些常规交互全部由组件内部接管不需要手写 axios 调用和 loading 状态管理开发体验非常顺畅。5. 核心功能模块实现5.1 用户管理模块用户管理模块是管理后台最基本也最绕不开的功能。我这里的用户管理支持分页查询、新增用户、编辑用户、重置密码、启停用、分配角色。前端页面上面放搜索表单下面放数据表格操作列放编辑、重置密码、删除按钮。后端的用户 Service 有几个关键点新增用户用户名唯一性校验密码通过BCryptPasswordEncoder加密后存储默认密码可以由管理员设置也可以走工号规则生成。编辑用户不直接更新密码字段密码修改走独立的resetPassword接口。分配角色先删除用户的旧角色关联再插入新角色关联放到一个事务里执行。分页查询用 MyBatis-Plus 的Page对象配合 LambdaQueryWrapper 做动态条件拼接。用 MyBatis-Plus 加分页的实现非常简洁完整查用户列表的服务方法大概长这样public PageSysUserVO pageUser(UserQueryDTO dto) { PageSysUser page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(dto.getKeyword()), SysUser::getUsername, dto.getKeyword()); wrapper.eq(dto.getStatus() ! null, SysUser::getStatus, dto.getStatus()); wrapper.orderByDesc(SysUser::getCreateTime); return userMapper.selectPage(page, wrapper).convert(user - { SysUserVO vo new SysUserVO(); BeanUtils.copyProperties(user, vo); vo.setRoleIds(userRoleMapper.selectRoleIdsByUserId(user.getId())); return vo; }); }5.2 角色权限管理模块角色管理是权限配置的核心一个角色的权限最终要落到菜单表和按钮权限标识上。角色管理的功能点包括角色列表、新增角色、编辑角色、分配菜单权限、删除角色。分配菜单权限的交互通常在 Vben5.0 里用一棵Tree组件实现左侧是角色信息右侧是权限树。勾选完成之后一次性提交所有选中的菜单 ID 到后端。后端接口接收的是一个菜单 ID 列表Transactional(rollbackFor Exception.class) public void assignMenus(Long roleId, ListLong menuIds) { roleMenuMapper.deleteByRoleId(roleId); if (CollectionUtils.isEmpty(menuIds)) { return; } ListSysRoleMenu list menuIds.stream() .map(menuId - new SysRoleMenu(roleId, menuId)) .collect(Collectors.toList()); roleMenuMapper.insertBatch(list); }注意这里加上了Transactional注解先删后插必须放在同一个事务里否则会出现角色权限分配一半失败导致数据不完整的问题。在实际实现中我还在删除角色时做了关联校验如果该角色已经被用户关联则提示该角色已分配给用户无法删除避免误操作导致用户权限丢失。5.3 操作日志记录企业级系统里操作日志几乎是硬性需求。审计要求、安全排查、问题追踪都需要操作日志。我实现的方案是 AOP 切面 自定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OperationLog { String module() default ; String action() default ; }然后在需要记录日志的接口方法上加上注解例如OperationLog(module 用户管理, action 新增用户) PreAuthorize(hasAuthority(system:user:add)) PostMapping(/admin/users) public ResultVoid addUser(Valid RequestBody SysUserDTO dto) { userService.addUser(dto); return Result.ok(null); }AOP 切面里统一获取当前登录用户、请求 IP、请求参数、方法执行耗时异步写入日志表。这里需要注意的是日志操作不应该影响主业务流程尽量用Async异步保存或者通过 MQ 队列削峰避免因为日志写入慢拖垮业务接口。5.4 文件上传与本地存储文件上传也是管理后台的高频功能无论是用户头像还是业务附件。我的做法是后端提供一个通用的上传接口接收 MultipartFile存储到服务器磁盘返回访问 URL。生产中建议挂 OSS 对象存储但小项目本地存储完全够用。PostMapping(/common/upload) public ResultUploadVO upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) suffix; String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String relativePath /uploads/ datePath / newFileName; File dest new File(uploadPath relativePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.ok(new UploadVO(relativePath)); }这里有两个需要特别注意的坑一是文件名一定不能直接用原始文件名要重命名为 UUID 加后缀否则容易产生重名覆盖或非法路径问题二是上传目录要在配置类里做静态资源映射否则浏览器无法直接访问上传后的图片。6. 部署与常见问题排查6.1 打包与部署方式后端打包直接用 Maven 的package命令生成可执行 Jar 包。我实际部署用的是 Docker Compose 方式容器编排文件里包含 MySQL、Redis、后端应用三个服务。# 后端 Dockerfile FROM eclipse-temurin:17-jre WORKDIR /app COPY target/admin-server.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]前端构建时要注意Vben5.0 默认使用环境变量管理 API 地址生产环境的请求代理路径要配置正确。构建命令是pnpm build产物是dist目录放到 Nginx 的 html 目录下即可。Nginx 还需要配置 SPA 路由的try_files否则刷新页面会 404。server { listen 80; server_name admin.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend-server:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }6.2 常见问题速查表我在实际开发和联调过程中遇到过不少问题挑几个高频的整理成表格希望能帮你少踩坑问题现象可能原因解决方案应用启动报ClassNotFoundException: javax.servlet.FilterJDK 版本低于 17 或引入了旧版 javax 依赖确认使用 JDK17检查 pom 依赖是否引入了 Servlet API 旧包统一使用jakarta.servlet登录后过一段时间请求 401accessToken 过期且前端未自动刷新在 Axios 响应拦截器中捕获 401调用 refreshToken 接口刷新 Token 后重新发起原请求前端动态菜单不显示后端菜单表 menuType 配错或 component 路径对不上前端文件路径检查菜单表中的 component 字段是否对应src/views下的真实组件路径例如system/user/indexPreAuthorize不生效未开启EnableMethodSecurity在 SecurityConfig 上添加EnableMethodSecurity注解接口返回 403 但用户已分配角色角色菜单关联表数据未同步权限标识没有加载进 UserDetails检查角色能否正常查到菜单权限重新分配菜单权限后再登录JWT 解析报Unknown algorithm生成 Token 的算法与校验算法不一致或缺少 jjwt-jackson 依赖统一 HS256 或 RS256确认引入了jjwt-impl和jjwt-jackson这些问题里面最坑的是 SpringBoot3 的 javax 包迁移问题因为javax.servlet.Filter这种错误会直接导致启动失败排查半天之后才发现是某个老依赖的传递性依赖引入了旧包。建议项目初期就统一检查所有第三方依赖是否支持 Jakarta 规范。另一个容易踩坑的地方是 Vben5.0 的登录校验逻辑它自带的表单校验规则比较严格比如密码策略、二次确认密码等。如果后端不需要这些校验要记得在表单配置里关闭或者修改否则前端会挡住登录请求。7. 跑通后的实际体验与几个小建议项目完整跑通之后我的整体感受是这套组合拳非常值得投入。SpringBoot3 Security6 OAuth2 JWT 在后端提供的是一套标准化程度很高的安全基础设施Vben5.0 在前端又帮我省掉了大量后台管理页面的重复工作。前后端加起来一个具备认证、授权、用户、角色、菜单、日志、上传这些通用能力的管理后端从零到跑通我大概用了一周左右而真正写业务代码比如某个模块的 CRUD 页面基本一两天就能出活。有几个我实际操作中的心得想单独分享一下。第一权限标识一定要尽早约定规范。比如统一用模块:子模块:操作这种三段式类似system:user:add、system:role:delete。一旦项目跑起来权限标识散落在各种注解和数据库里再想统一命名成本就很高了。第二JWT 过期时间设置要结合业务场景。accessToken 我设置的 2 小时refreshToken 7 天。如果你的系统对安全性要求特别高可以把 accessToken 缩短到 30 分钟同时把 refreshToken 的自动续期机制做完善Vben5.0 的请求封装里已经预留了刷新逻辑接入很快。第三跨域配置要提前想清楚。开发环境下 Vben5.0 默认端口和 SpringBoot 的 8080 端口不一样如果不配置 CORS前端请求会被浏览器拦截。我的做法是在 Security 配置里加 CorsConfigurationSource同时前端用 Vite 的 proxy 做开发代理生产环境走 Nginx 反向代理一套配置两边兼顾。第四数据权限和接口权限要分开设计。RBAC 解决的是能不能访问这个接口的问题而能访问哪些数据属于数据权限比如普通用户只能看自己部门的数据。这个在企业项目里很常见但在样板工程里很容易被忽略。建议在用户表设计时预留dept_id字段数据权限后续可以基于部门树做扩展。最后再分享一个小技巧Vben5.0 的代码生成器可以跟后端的 MyBatis-Plus 生成器配合起来用。后端根据数据库表生成 Entity、Mapper、Service、Controller前端根据接口文档快速生成 API 定义和页面模板。两条链路打通之后一个新模块的从零搭建时间可以压缩到原来的三分之一。这套方案现在在我这边已经稳定运行了几个长期项目没有再出现基础设施层面的返工。如果你也打算在新项目里上 SpringBoot3 Vben5.0建议直接照这个思路搭先把认证和权限打通再往里面填业务方向对了后面就不会乱。
返回列表