目录
- 前言与项目说明
- 环境准备说明
- [架构演进:传统 C 端 JWT 拦截器 vs B 端 Spring Security 鉴权](#架构演进:传统 C 端 JWT 拦截器 vs B 端 Spring Security 鉴权)
- 实践
-
- [RBAC 权限表结构设计](#RBAC 权限表结构设计)
- [流程概览(Mermaid 流程图)](#流程概览(Mermaid 流程图))
- 第一步:引入依赖
- 第二步:定义自定义用户安全对象(CustomUserDetails)
- [第三步:登录成功时缓存权限数据至 Redis](#第三步:登录成功时缓存权限数据至 Redis)
- 第四步:自定义过滤器(TokenAuthenticationFilter)
- 第五步:安全核心配置类(SpringSecurityConfig)
- 第六步:注解驱动鉴权运用(UserController)
- 总结
前言与项目说明
本篇博客将详细讲解如何在单体 Spring Boot 脚手架项目中实现基于 Spring Security 6 的标准 B 端企业级 RBAC(基于角色的权限控制)安全管理体系。本教程以简单易懂、循序渐进的方式,结合真实项目代码,帮助初学者快速掌握 Spring Security 在生产环境中的实战写法。
环境准备说明
- JDK 版本 :
JDK 17 - Spring Boot 版本 :
Spring Boot 3.3.3 - 项目名称 :
spring-boot-base-demo - 基础分支 :
master(主攻 C 端业务) - 集成分支 :
feature/admin-auth-springsecurity(主攻 B 端管理后台鉴权) - 源码获取 :点击跳转Github获取
架构演进:传统 C 端 JWT 拦截器 vs B 端 Spring Security 鉴权
在脚手架项目的设计中,C 端业务 和B 端管理系统 在安全防护与权限深度上存在根本的差异。通过对比 master 分支和当前安全组件集成分支,我们可以清晰地看到这一架构的演变:
| 维度 | Master 分支(传统 C 端 JWT 鉴权) | Feature 分支(B 端 Spring Security 6 集成) |
|---|---|---|
| 安全场景 | 面向 C 端移动端/小程序,业务高并发,鉴权规则相对简单。 | 面向 B 端管理后台,需要严苛的**角色(Role)与按钮级权限(Permission)**粒度控制。 |
| 拦截机制 | 采用 Spring MVC 自定义的 HandlerInterceptor(TokenInterceptor)。 |
采用 Spring Security 的**过滤器链(Filter Chain)**拦截,防线前置到 Servlet 容器级别。 |
| 权限维度 | 仅做"是否登录"的登录态检查,无法优雅地支持复杂的权限矩阵校验。 | 支持方法/接口级的权限声明(hasRole / hasAuthority),满足复杂的菜单及按钮权限判定。 |
| 代码变动 | 核心逻辑在 TokenInterceptor.java 中。 |
彻底清空并废弃了 TokenInterceptor.java,新建 TokenAuthenticationFilter 过滤器及 SpringSecurityConfig 配置类。 |
| 上下文维护 | 通过自定义 ThreadLocal 的 SessionContext 维护。 |
双重保障:保留 SessionContext 支持轻量业务调用,同时集成框架的 SecurityContextHolder 维护用户权限状态。 |
实践
RBAC 权限表结构设计
本脚手架集成了标准的 RBAC(Role-Based Access Control)权限模型,其关联的数据库实体包括:
User(用户表) :
保存用户账号、姓名、手机号等核心数据。SysRole(角色表) :
保存角色基本信息,例如:系统管理员(admin)、运营人员(operator)等。SysUserRole(用户-角色多对多关联表) :
将用户与角色进行绑定,决定某个用户拥有哪些角色。SysPermission(权限/按钮表) :
按钮或操作级的细粒度权限标识,例如:用户添加(user:add)、用户修改(user:edit)。SysRolePermission(角色-权限关联表) :
将角色与权限进行绑定,决定某个角色能操作哪些具体按钮。SysMenu(菜单表) :
系统前端左侧菜单树结构,包括菜单名称、路由路径、图标等。SysRoleMenu(角色-菜单关联表) :
控制角色能够看到的菜单树范围。
流程概览(Mermaid 流程图)
业务控制器 (UserController) Spring Security 核心上下文 TokenAuthenticationFilter (过滤器) 业务控制器 (UserController) Spring Security 核心上下文 TokenAuthenticationFilter (过滤器) #mermaid-svg-ySySYu2eCONRslgY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ySySYu2eCONRslgY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ySySYu2eCONRslgY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ySySYu2eCONRslgY .error-icon{fill:#552222;}#mermaid-svg-ySySYu2eCONRslgY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ySySYu2eCONRslgY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ySySYu2eCONRslgY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ySySYu2eCONRslgY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ySySYu2eCONRslgY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ySySYu2eCONRslgY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ySySYu2eCONRslgY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ySySYu2eCONRslgY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ySySYu2eCONRslgY .marker.cross{stroke:#333333;}#mermaid-svg-ySySYu2eCONRslgY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ySySYu2eCONRslgY p{margin:0;}#mermaid-svg-ySySYu2eCONRslgY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ySySYu2eCONRslgY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ySySYu2eCONRslgY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ySySYu2eCONRslgY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ySySYu2eCONRslgY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ySySYu2eCONRslgY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ySySYu2eCONRslgY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ySySYu2eCONRslgY .sequenceNumber{fill:white;}#mermaid-svg-ySySYu2eCONRslgY #sequencenumber{fill:#333;}#mermaid-svg-ySySYu2eCONRslgY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ySySYu2eCONRslgY .messageText{fill:#333;stroke:none;}#mermaid-svg-ySySYu2eCONRslgY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ySySYu2eCONRslgY .labelText,#mermaid-svg-ySySYu2eCONRslgY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ySySYu2eCONRslgY .loopText,#mermaid-svg-ySySYu2eCONRslgY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ySySYu2eCONRslgY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ySySYu2eCONRslgY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ySySYu2eCONRslgY .noteText,#mermaid-svg-ySySYu2eCONRslgY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ySySYu2eCONRslgY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ySySYu2eCONRslgY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ySySYu2eCONRslgY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ySySYu2eCONRslgY .actorPopupMenu{position:absolute;}#mermaid-svg-ySySYu2eCONRslgY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ySySYu2eCONRslgY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ySySYu2eCONRslgY .actor-man circle,#mermaid-svg-ySySYu2eCONRslgY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ySySYu2eCONRslgY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 判断是否为放行路径 (如 /user/login) 从请求头/参数提取 Token 从 Redis 中获取缓存的 LoginUser (包含权限 and 角色) 执行权限检查 (@PreAuthorize) alt 权限不足 权限通过 alt Token 无效或过期 Token 有效 alt 是放行路径 不是放行路径 客户端 发起请求 (携带 Token) 1 直接放行 2 返回 401 (RetObj 格式 JSON) 3 设置 SecurityContextHolder (写入 CustomUserDetails) 4 放行到 Controller 5 触发 AccessDeniedHandler, 返回 403 (RetObj 格式 JSON) 6 返回业务数据 (RetObj) 7 客户端
第一步:引入依赖
在项目的 pom.xml 中引入 Spring Security 依赖:
xml
<!-- 引入 spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:定义自定义用户安全对象(CustomUserDetails)
我们需要实现 Spring Security 的 UserDetails 接口,以便将用户 ID、手机号和权限角色列表存入框架上下文。
文件路径:cn/xf/basedemo/common/model/CustomUserDetails.java
java
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
public class CustomUserDetails implements UserDetails {
private Integer userId; // 用户 ID
private String phone; // 手机号
private Collection<? extends GrantedAuthority> authorities; // 权限与角色集合
public CustomUserDetails(Integer userId, String phone, Collection<? extends GrantedAuthority> authorities) {
this.userId = userId;
this.phone = phone;
this.authorities = authorities;
}
@Override
public String getPassword() {
return null; // 前后端分离无状态,无需保存密码
}
@Override
public String getUsername() {
// 返回唯一标识:优先使用手机号,无则使用用户ID
return phone != null ? phone : (userId != null ? String.valueOf(userId) : null);
}
}
第三步:登录成功时缓存权限数据至 Redis
在用户登录时,一次性把用户的角色 and 权限查询出来,并写入 LoginUser 实体中缓存进 Redis,这样在后续过滤器中能够快速获取,避免重复查询数据库。
文件路径:cn/xf/basedemo/service/impl/UserServiceImpl.java
java
// 1. 用户登录验证成功后,构建 LoginUser
LoginUser loginUser = new LoginUser();
loginUser.setId(user.getId());
loginUser.setAccount(user.getAccount());
loginUser.setPhone(user.getPhone());
// 2. 核心性能优化:获取角色与权限数据并直接缓存到 LoginUser 中
loginUser.setPermissions(sysPermissionMapper.getPermissionListByRoleId(user.getId()));
loginUser.setRoles(sysRoleMapper.getRoleListByUserId(user.getId()));
// 3. 生成 Token,并将完整的 LoginUser 缓存到 Redis
String token = JwtTokenUtils.createToken(user.getId());
loginUser.setToken(token);
redisTemplate.opsForValue().set("token:" + token, JSONObject.toJSONString(loginUser), 3600, TimeUnit.SECONDS);
第四步:自定义过滤器(TokenAuthenticationFilter)
拦截进入系统的请求,校验 Token 状态,并从 Redis 读取出用户的缓存数据装载到 Spring Security 安全框架中。
文件路径:cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java
java
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 无需拦截放行的接口列表
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login", "/swagger-ui.html", "/v3/api-docs", "/swagger-ui/index.html");
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String requestURI = request.getRequestURI();
// 1. 过滤放行路径
if (EXCLUDE_PATH_LIST.contains(requestURI) || requestURI.contains("/swagger-ui") || requestURI.contains("/v3/api-docs")) {
filterChain.doFilter(request, response);
return;
}
// 2. 提取并校验 Token
String token = RequestHeaderUtil.getToken(request);
String value = (String) redisTemplate.opsForValue().get("token:" + token);
if (StringUtils.isEmpty(value)) {
throw new LoginException();
}
LoginUser loginUserInfo = JSONObject.parseObject(value, LoginUser.class);
if (loginUserInfo == null || loginUserInfo.getId() <= 0) {
throw new LoginException(SystemStatus.USER_INPUT_ERROR);
}
// 3. 延长 Token 有效期 (Key: token:xxx)
redisTemplate.expire("token:" + token, 86700, TimeUnit.SECONDS);
// 4. 将用户信息与权限设置到 Spring Security 上下文
this.setSpringSecurityContext(loginUserInfo);
filterChain.doFilter(request, response);
} catch (LoginException e) {
// 5. 异常情况输出统一的 RetObj JSON 结构
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONObject.toJSONString(new RetObj<>(e.getStatus().getCode(), e.getMessage())));
} finally {
// 6. 强行清理上下文,彻底杜绝高并发线程池环境下的 ThreadLocal 内存泄漏隐患
SecurityContextHolder.clearContext();
}
}
private void setSpringSecurityContext(LoginUser loginUserInfo) {
// 直接从缓存中获取角色与权限列表,免去查库操作,极大提升响应性能
List<String> permissionList = loginUserInfo.getPermissions();
List<String> roleList = loginUserInfo.getRoles();
List<String> authoritiesList = new java.util.ArrayList<>();
if (!CollectionUtils.isEmpty(permissionList)) {
authoritiesList.addAll(permissionList);
}
if (!CollectionUtils.isEmpty(roleList)) {
// Spring Security 角色匹配默认带有 ROLE_ 前缀
List<String> roleAuthorities = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());
authoritiesList.addAll(roleAuthorities);
}
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesList);
UserDetails userDetails = new CustomUserDetails(loginUserInfo.getId(), loginUserInfo.getPhone(), authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
第五步:安全核心配置类(SpringSecurityConfig)
配置跨域、禁用CSRF防范、声明放行路由、添加鉴权拦截器,并为 401 (未登录) 与 403 (无权限) 设置统一的 API JSON 返回格式。
文件路径:cn/xf/basedemo/interceptor/SpringSecurityConfig.java
java
@Slf4j
@Configuration
@EnableMethodSecurity(prePostEnabled = true) // 开启方法级权限控制 (@PreAuthorize)
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
TokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
http
.cors(Customizer.withDefaults()) // 开启跨域
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF 防御
.authorizeHttpRequests(auth -> auth
// 放行匿名访问路径
.requestMatchers("/user/login", "/web/login").permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/doc.html", "/webjars/**", "/swagger-resources/**").permitAll()
// 其他所有请求必须通过鉴权
.anyRequest().authenticated()
)
// 生产优化:配置无状态会话策略,防止框架在后台创建 HttpSession 导致内存飙升
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 将我们自定义 of Token 验证过滤器加到 UsernamePasswordAuthenticationFilter 之前
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 配置 401 与 403 异常处理器,输出与项目高度统一 of RetObj JSON 数据格式
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
RetObj<Void> retObj = new RetObj<>(SystemStatus.UNAUTHORIZED.getCode(), "未登录或Token失效");
response.getWriter().write(JSONObject.toJSONString(retObj));
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
RetObj<Void> retObj = new RetObj<>(SystemStatus.FORBIDDEN.getCode(), "没有权限访问该资源");
response.getWriter().write(JSONObject.toJSONString(retObj));
})
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
第六步:注解驱动鉴权运用(UserController)
最终,在 Controller 的映射方法上,使用简单的表达式即可安全限制访问权限。
文件路径:cn/xf/basedemo/controller/business/UserController.java
java
// 只有具有 'user:add' 权限的用户才可以访问
@Operation(summary = "用户信息", description = "用户信息")
@PostMapping("/info")
@PreAuthorize("hasAuthority('user:add')")
public RetObj info(){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
return RetObj.success(user);
}
// 只有角色为 'admin' 的用户才可以访问
@Operation(summary = "获取用户权限数据", description = "用户信息")
@GetMapping("/getPermission")
@PreAuthorize("hasRole('admin')")
public RetObj getPermission(){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
return RetObj.success(user.getAuthorities());
}
总结
本文以脚手架项目实战代码为例,演示了如何集成标准 Spring Security 6 框架以实现标准的 RBAC 鉴权机制。通过引入自定义安全对象 CustomUserDetails、核心配置配置类 SpringSecurityConfig 以及过滤器拦截链路,项目无缝支持了基于角色的菜单与细粒度按钮权限校验。这不仅标准化了系统安全防御能力,还使接口校验逻辑与业务解耦,是构建高性能、可扩展单体脚手架不可或缺的核心安全模块。