资讯动态

Spring Boot 3 + Spring Security 6 权限管理后台系统实战拆解

发布时间:2026/10/15 11:31:18 来源:尧图企业网站定制
1. 权限管理系统到底在管什么很多刚接触企业级开发的朋友一听到“权限管理”四个字脑子里第一反应就是登录、注册、角色、菜单这一套。但真到了项目里你会发现事情远没有这么简单。一个能扛住实际业务的权限系统至少要回答四个问题你是谁、你能进哪些门、你进门之后能碰哪些东西、你碰东西的整个过程有没有留下痕迹。这四个问题分别对应认证、授权、数据权限和审计日志缺一个都不算完整。我这次要拆解的是一个基于 Spring Boot 3 和 Spring Security 构建的权限管理后台系统。它解决的核心痛点很明确中小型团队在起步阶段往往没有精力从零搭建一套安全框架直接用现成的脚手架又经常遇到版本老旧、扩展困难、文档缺失的问题。这套方案的价值在于它把 Spring Security 的过滤链、认证管理器、授权决策器这些底层机制做了合理的封装同时保留了足够的扩展点让你在业务变复杂的时候不至于推倒重来。适合谁来参考如果你已经写过 Spring Boot 项目能看懂 Controller 和 Service 的基本分层但对 Spring Security 的配置总是一知半解每次遇到跨域、Token 失效、权限注解不生效就靠搜索引擎拼凑解决方案那这篇内容就是写给你的。我会从整体设计思路讲到具体代码实现再到实际部署中踩过的坑尽量把每个关键决策背后的原因说清楚。2. 整体架构设计与技术选型逻辑2.1 为什么是 Spring Boot 3 加 Spring Security 6Spring Boot 3 最大的变化是全面拥抱了 Jakarta EE 9包名从javax.*迁移到了jakarta.*。这个改动看起来只是改了个前缀但实际影响很大——很多老教程里的代码直接复制过来会编译报错。Spring Security 6 则进一步强化了基于组件的安全配置模型废弃了大量WebSecurityConfigurerAdapter时代的写法。这两者组合在一起意味着你不能再照着三年前的博客去配了必须理解新的配置范式。我选择这个组合的原因有三点。第一Spring Security 6 的SecurityFilterChainBean 化配置让过滤链的定制变得非常直观你可以在一个方法里清晰地看到请求从进入到放行的完整路径。第二方法级安全注解PreAuthorize和PostAuthorize在 SpEL 表达式的支持下能写出非常灵活的权限判断逻辑比如“只有部门经理才能审批本部门金额小于五万的报销单”这种规则。第三Spring Boot 3 对 GraalVM 原生镜像的支持更好虽然权限系统本身不一定需要原生编译但这代表了技术栈的长期维护性。2.2 认证方案JWT 与 Session 的取舍认证方案的选择是权限系统的第一个分叉路口。Session 方案依赖服务端存储天然支持注销和在线用户管理但在多实例部署时需要引入 Redis 做集中存储。JWT 方案把状态编码在令牌里服务端不需要存储横向扩展方便但注销和权限变更的实时性是个难题。这套系统采用的是JWT 加 Redis 黑名单的混合方案。具体来说登录成功后签发一个 Access Token 和一个 Refresh Token。Access Token 有效期设置得比较短比如三十分钟减少泄露后的风险窗口。Refresh Token 有效期七天只用来换取新的 Access Token。当用户主动注销或者管理员强制下线时把该用户的 Token 标识写入 Redis 黑名单并设置一个比 Token 剩余有效期稍长的过期时间。每次请求经过过滤器时除了校验签名和有效期还要查一下黑名单。注意黑名单的 Key 设计很关键。不要直接存整个 Token 字符串那样太占空间。我通常取 Token 的 jti 声明或者用用户 ID 加设备指纹做 Key这样既能精确失效又方便批量清理。2.3 授权模型RBAC 的落地变形标准的 RBAC 模型是用户、角色、权限三张表加两张关联表。但在实际业务中纯粹的 RBAC 往往不够用。比如同一个角色在不同部门的数据可见范围应该不同或者某个用户临时需要某个权限但不想给他加角色。这套系统在 RBAC 基础上做了两点扩展一是引入了数据范围概念角色可以绑定数据范围类型全部、本部门、本部门及以下、仅本人、自定义在查询数据时自动拼接过滤条件二是支持用户直接授权作为角色授权的补充用于处理临时性、例外性的权限需求。这种设计的好处是既保持了 RBAC 的清晰结构又能应对实际业务中的灵活场景。代价是权限判断的逻辑会稍微复杂一些需要在 Service 层做合并计算。我的做法是写一个PermissionEvaluator的实现类把角色权限和用户直接权限合并成一个权限集合再交给 Spring Security 的决策器去判断。3. 核心模块的代码级拆解3.1 安全过滤链的配置要点Spring Security 6 中配置过滤链的标准做法是定义一个SecurityFilterChainBean。下面这段代码展示了核心配置我逐段解释关键点。Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/refresh).permitAll() .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(ex - ex .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)); return http.build(); } }csrf关闭是因为我们用的是无状态 Token 认证不存在 CSRF 攻击的前提。sessionCreationPolicy设为STATELESS确保 Spring Security 不会创建 HttpSession。authorizeHttpRequests里的匹配顺序很重要白名单路径必须放在anyRequest之前否则会被拦截。addFilterBefore把自定义的 JWT 过滤器插到用户名密码过滤器之前这样请求进来先解析 Token后续的授权决策才能拿到认证信息。EnableMethodSecurity这个注解替代了老版本的EnableGlobalMethodSecurity它默认启用了PreAuthorize、PostAuthorize、PreFilter、PostFilter四个注解。如果你只需要PreAuthorize可以显式配置prePostEnabled true来减少不必要的代理开销。3.2 JWT 过滤器的实现细节JWT 过滤器的职责很单一从请求头里取出 Token校验有效性解析出用户信息塞进SecurityContextHolder。但实现时有几个细节容易出错。public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token extractToken(request); if (StringUtils.hasText(token) jwtTokenProvider.validateToken(token)) { if (!tokenBlacklistService.isBlacklisted(token)) { Authentication auth jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }继承OncePerRequestFilter而不是直接实现Filter接口是为了保证在异步请求和转发场景下过滤器只执行一次。extractToken方法要从Authorization头里解析出Bearer后面的部分注意处理大小写和空格。校验 Token 时validateToken内部要捕获ExpiredJwtException、SignatureException等异常并返回 false而不是让异常抛出去导致 500 错误。实操心得SecurityContextHolder默认使用ThreadLocal存储认证信息在异步方法或者线程池中会丢失。如果你的系统有异步处理逻辑需要配置SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL)或者手动传递 SecurityContext。3.3 权限注解与 SpEL 表达式的配合方法级权限控制是这套系统的亮点之一。通过PreAuthorize注解可以把权限判断直接写在 Service 方法上代码可读性比在 XML 里配置强得多。PreAuthorize(hasAuthority(system:user:list)) GetMapping(/users) public ResultPageResultUserVO listUsers(UserQuery query) { return Result.ok(userService.page(query)); } PreAuthorize(ss.hasPermission(system:user:edit) and ss.hasDataScope(#userId, dept)) PutMapping(/users/{userId}) public ResultVoid updateUser(PathVariable Long userId, RequestBody UserUpdateDTO dto) { userService.update(userId, dto); return Result.ok(); }第一个例子用的是标准的hasAuthority判断当前用户是否拥有system:user:list这个权限标识。第二个例子用了自定义的 Bean 方法ss.hasPermission和ss.hasDataScope前者是对hasAuthority的封装支持通配符匹配后者用来判断当前用户是否有权操作指定用户的数据比如只能修改本部门的用户。SpEL 表达式里用#userId引用方法参数这是 Spring Security 提供的参数绑定能力。如果参数是对象可以用#dto.deptId这样的方式访问属性。这种灵活性让数据权限的判断可以精确到具体记录。3.4 数据权限的 SQL 拼接策略数据权限的落地方式直接影响到系统的性能和可维护性。常见的做法有三种在 SQL 里硬编码过滤条件、用 MyBatis 拦截器动态改写 SQL、在 Service 层做数据过滤。第一种太死板第三种在数据量大时会把全表查出来再过滤性能不可接受。这套系统采用的是MyBatis 拦截器加注解的方案。具体来说定义一个DataScope注解标注在 Mapper 方法上指定部门字段别名和用户字段别名。拦截器在 SQL 执行前根据当前用户的角色数据范围动态拼接AND dept_id IN (...)或AND create_by ?这样的条件。拼接时要注意 SQL 注入风险所有值都通过参数占位符传入不要直接拼字符串。Intercepts({Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class})}) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String originalSql boundSql.getSql(); // 解析注解获取数据范围条件 String dataScopeSql buildDataScopeSql(handler); if (StringUtils.hasText(dataScopeSql)) { // 用反射修改 BoundSql 的 sql 字段 ReflectUtil.setFieldValue(boundSql, sql, originalSql AND dataScopeSql); } return invocation.proceed(); } }注意修改BoundSql的 sql 字段需要用到反射不同版本的 MyBatis 字段名可能不同。建议在拦截器初始化时打印一下BoundSql的字段列表确认清楚再写反射代码。另外如果原 SQL 已经包含WHERE子句拼接时要用AND而不是WHERE这个判断逻辑不能少。4. 实操部署与常见问题排查4.1 从零搭建的完整步骤假设你已经有一个空的 Spring Boot 3 项目下面是接入这套权限系统的操作顺序。引入依赖在pom.xml中添加spring-boot-starter-security、spring-boot-starter-web、mybatis-plus-boot-starter、jjwt-api、jjwt-impl、jjwt-jackson、spring-boot-starter-data-redis。注意 jjwt 的三个依赖版本要一致否则运行时会报NoClassDefFoundError。配置数据源和 Redis在application.yml中配置数据库连接和 Redis 连接。Redis 用来存 Token 黑名单和在线用户信息建议单独用一个 database 索引避免和业务缓存混在一起。初始化数据库表至少需要用户表、角色表、权限表、用户角色关联表、角色权限关联表、部门表。建表时给权限标识字段加唯一索引给关联表加联合主键这些约束能避免很多数据不一致的问题。实现 UserDetailsService从数据库加载用户信息返回UserDetails实现类。注意getAuthorities方法要返回权限标识的集合不要返回角色名否则hasAuthority判断会失败。配置 JWT 工具类生成 Token 时设置subject为用户 IDclaims里放用户名和权限列表expiration根据配置设置。签名密钥要足够长HS256 算法建议至少 256 位。编写安全配置类按照前面展示的SecurityFilterChain配置注册 JWT 过滤器、认证入口点、拒绝处理器。测试验证先用 Postman 调登录接口拿 Token再带着 Token 调受保护接口验证权限注解是否生效。测试时要覆盖 Token 过期、Token 篡改、无权限访问三种异常场景。4.2 常见问题速查表问题现象可能原因排查方向登录接口返回 401白名单配置未生效检查permitAll路径是否匹配注意 ContextPath带 Token 访问仍 401过滤器未执行或 Token 解析失败在过滤器中打断点确认SecurityContext是否设置成功权限注解不生效EnableMethodSecurity未加或代理失效检查配置类注解确认 Service 是被 Spring 管理的 Bean跨域请求失败CORS 配置与 Security 冲突在 Security 配置中显式开启cors()并定义CorsConfigurationSource数据权限 SQL 未拼接拦截器未注册或注解未识别检查 MyBatis 配置中是否添加了拦截器注解的Intercepts是否正确Token 刷新后旧 Token 仍可用黑名单未生效确认 Redis 连接正常黑名单 Key 的过期时间设置合理4.3 性能与安全方面的经验之谈权限系统本身不应该成为性能瓶颈但设计不当确实会拖慢整个应用。我踩过的一个坑是每次请求都去数据库查用户权限QPS 一高数据库就扛不住了。后来改成登录时把权限列表编码进 JWT过滤器直接从 Token 里解析省掉了一次数据库查询。但这样做的代价是权限变更不能实时生效需要等 Token 过期或者强制用户重新登录。折中方案是用 Redis 缓存用户权限设置较短的过期时间比如五分钟既能减少数据库压力又能保证一定的实时性。安全方面有几个容易被忽视的点。第一JWT 的签名密钥不要硬编码在代码里应该通过环境变量或者配置中心注入。第二登录接口要做频率限制防止暴力破解可以用 Redis 的计数器实现比如同一 IP 一分钟内超过十次失败就锁定十分钟。第三返回给前端的错误信息不要暴露太多细节比如“用户不存在”和“密码错误”应该统一返回“用户名或密码错误”避免攻击者枚举用户。第四敏感操作要记录审计日志包括操作人、操作时间、操作类型、请求参数和结果日志要单独存储保留足够长的时间。实操心得Spring Security 的异常处理默认会把异常堆栈打到日志里生产环境建议配置logging.level.org.springframework.securityWARN避免日志被安全相关的调试信息淹没。同时自定义AccessDeniedHandler和AuthenticationEntryPoint返回统一的 JSON 格式错误响应方便前端处理。5. 扩展方向与二次开发建议这套系统作为一个基础脚手架已经覆盖了权限管理的核心场景。但实际业务千差万别二次开发时可以从几个方向入手。多租户支持是一个常见的扩展点可以在用户表和角色表加tenant_id字段在过滤器和数据权限拦截器里统一注入租户条件。权限组是另一个方向把一组权限打包成一个权限组分配给角色时直接选权限组减少配置工作量。动态权限可以让管理员在运行时修改权限规则而不重启服务实现方式是把权限判断逻辑抽象成规则引擎比如用 Drools 或者简单的表达式解析器。我在实际项目中最常做的扩展是接口级别的权限日志。通过 AOP 切面拦截带有权限注解的方法记录每次调用的用户、权限标识、执行结果和耗时。这些数据积累下来可以用来分析哪些权限从未被使用、哪些接口调用频繁但权限配置过严为权限优化提供依据。实现时注意切面的顺序权限切面要在事务切面之前执行避免权限判断失败后事务已经开启。最后分享一个小技巧在开发阶段可以配置一个dev环境的开关让所有权限判断直接返回 true方便调试业务逻辑。但一定要确保这个开关在生产环境被禁用可以通过 Spring 的 Profile 机制来控制比如只在application-dev.yml里设置security.permission.enabledfalse生产配置里不写这个属性或者设为 true。这个开关救过我很多次尤其是在联调阶段不用为了测试一个接口去配一堆权限数据。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑