【SpringSecurity新手村系列】(5)RBAC角色权限与账户状态校验

简介: 本章在数据库认证基础上,通过 RBAC 三表(用户-角色-权限)把角色与权限从数据库接入 Spring Security,并补齐账户状态字段(过期、锁定、启用等),实现“可登录 + 可授权 + 可控状态”的完整认证闭环。

第五章 RBAC角色权限与账户状态校验

本章继续完善 Spring Security:把数据库中的用户、角色、权限真正接入认证流程,同时补齐账户状态字段(是否过期、是否锁定、凭证是否过期、是否启用),实现“可登录 + 可授权 + 可控状态”的完整认证授权闭环。

在前四章里,我们已经完成了表单登录、验证码、数据库查用户等基础能力。本章开始进入更贴近真实项目的一步:让权限从数据库驱动,而不是写死在代码里。


一、问题切入

只完成“用户名密码能登录”还不够,企业项目通常还需要三个层次:

  • 用户是否可登录(禁用、锁定、过期等状态)
  • 登录后具备哪些角色(如 ROLE_ADMINROLE_USER
  • 角色对应哪些细粒度权限(如 content:moderate

如果这三块没打通,系统会出现典型问题:

  • 用户状态失效:数据库标记禁用,但系统依然放行
  • 权限粗糙:所有登录用户权限一致
  • 运维成本高:每次改权限都要改代码

二、解决方案概览

本章的做法是:

  1. users 表存账号与四个状态字段(enabledaccount_non_expiredaccount_non_lockedcredentials_non_expired)。
  2. roles 表新增 role 字段,专门存角色编码(如 ROLE_ADMIN)。
  3. 通过 user_rolesrole_permissionspermissions 建立 RBAC 授权关系。
  4. UserServiceImpl#loadUserByUsername 中查询并组装 GrantedAuthority
  5. Users 实体实现 UserDetails,把数据库状态直接映射到认证判定。

三、数据库设计(RBAC + 账户状态)

3.1 账户状态字段

users 表中推荐保留以下字段:

  • enabled:账号是否启用
  • account_non_expired:账号是否未过期
  • account_non_locked:账号是否未锁定
  • credentials_non_expired:凭证(密码)是否未过期

MySQL 中通常用 TINYINT(1) 存储,Java 侧映射为 Boolean/boolean

3.2 RBAC 关系

  • roles:角色定义(包含展示名 name 与角色编码 role
  • permissions:权限点定义(如 perm_key=content:moderate
  • user_roles:用户-角色关联
  • role_permissions:角色-权限关联

推荐的数据语义是:

  • roles.name:角色展示名(如“管理员”)
  • roles.role:角色编码(如 ROLE_ADMIN,用于授权匹配)
  • permissions.perm_key:权限编码(如 content:moderate

roles.role 与 Spring Security 的约定保持一致,例如:

  • ROLE_ADMIN
  • ROLE_USER
  • ROLE_MODERATOR

3.3 一份最小可用的 RBAC 结构

roles(id, name, role, description)
permissions(id, perm_key, perm_name)
user_roles(id, user_id, role_id)
role_permissions(id, role_id, permission_id)

这样可以同时支持:

  • 按角色授权(hasRole
  • 按权限点授权(hasAuthority
  • 后台动态调整权限,无需重启服务

四、代码落地要点

4.1 Users 实现 UserDetails

Users 实体中实现 UserDetails 的四个状态方法,直接返回数据库字段:

@Override
public boolean isAccountNonExpired() {
   
    return this.accountNonExpired;
}

@Override
public boolean isAccountNonLocked() {
   
    return this.accountNonLocked;
}

@Override
public boolean isCredentialsNonExpired() {
   
    return this.credentialsNonExpired;
}

@Override
public boolean isEnabled() {
   
    return this.enabled;
}

Spring Security 在 AbstractUserDetailsAuthenticationProvider 中执行密码校验前,会依次调用这四个方法。只要其中一个返回 false,就立即抛出对应异常(如 DisabledExceptionLockedException 等),终止登录流程。因此,数据库中的状态字段会直接决定用户能否成功认证。

4.2 动态组装权限集合

UserServiceImpl 中,先查用户,再查角色码与权限码,并转换成 SimpleGrantedAuthority,最后回填到 Usersauthorities 字段:

List<String> roleCodes = usersMapper.selectRoleCodesByUserId(users.getId());
List<String> permissionKeys = usersMapper.selectPermissionKeysByUserId(users.getId());

List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (roleCodes != null) {
   
    authorities.addAll(roleCodes.stream().map(SimpleGrantedAuthority::new).toList());
}
if (permissionKeys != null) {
   
    authorities.addAll(permissionKeys.stream().map(SimpleGrantedAuthority::new).toList());
}
users.setAuthorities(authorities);

确保 Users 实体中有 private List<SimpleGrantedAuthority> authorities; 字段,并在 getAuthorities() 方法中返回它。

这样 UserDetailsService 返回的 Users 就携带了完整的角色和权限集合,供后续授权阶段使用。

这里有两个实践细节:

  1. 建议角色和权限都装入 authorities,避免“URL 用角色、方法用权限”时出现缺失。
  2. 角色建议固定 ROLE_ 前缀,权限点保持业务风格(如 module:action)。

4.3 Mapper 联表 SQL

角色查询:

<select id="selectRoleCodesByUserId" parameterType="java.lang.Long" resultType="java.lang.String">
  select r.role
  from roles r
  inner join user_roles ur on ur.role_id = r.id
  where ur.user_id = #{userId,jdbcType=BIGINT}
</select>

权限查询:

<select id="selectPermissionKeysByUserId" parameterType="java.lang.Long" resultType="java.lang.String">
  select distinct p.perm_key
  from permissions p
  inner join role_permissions rp on rp.permission_id = p.id
  inner join user_roles ur on ur.role_id = rp.role_id
  where ur.user_id = #{userId,jdbcType=BIGINT}
</select>

五、常见坑位与排查

5.1 Invalid bound statement (not found)

如果出现:

Invalid bound statement (not found): xxxMapper.xxxMethod

优先检查:

  • Mapper XML 的 namespace 是否和接口全限定名一致
  • XML 中 id 是否和接口方法名一致
  • mapper-locations 是否能扫描到对应 XML

5.2 Collection<? extends GrantedAuthority> 不能 add

错误示例:

Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

应改为:

List<GrantedAuthority> authorities = new ArrayList<>();

List<SimpleGrantedAuthority>,再作为 Collection<? extends GrantedAuthority> 返回。

5.3 rolesList 为空导致 NPE

getAuthorities() 中如果依赖角色列表,务必判空,避免空指针。

5.4 账户状态字段为 null

Users 中四个状态字段如果是 Boolean 包装类型,数据库脏数据可能导致 null。建议:

  • 初始化数据时给默认值 1
  • 读取后做 Boolean.TRUE.equals(...) 风格的安全判定

5.5 只看到登录失败,不知道具体原因

账户状态失败通常对应异常:

  • DisabledException
  • LockedException
  • AccountExpiredException
  • CredentialsExpiredException

例如,AuthenticationFailureHandler 中可根据异常类型返回不同提示:

if (exception instanceof DisabledException) {
   
    response.getWriter().write("账号已被禁用");
} else if (exception instanceof LockedException) {
   
    response.getWriter().write("账号已被锁定");
}
// ... 其他异常同理

这部分将在统一异常处理章节中详细展开。


六、快速验收清单

可使用以下测试账号验证状态字段是否生效:

  • admin/admin123:应登录成功
  • disabled/disabled123:应被判定禁用
  • locked/locked123:应被判定锁定
  • expired/expired123:应被判定账号过期
  • credential_expired/credential123:应被判定凭证过期

七、核心概念总结

概念 说明
UserDetails Spring Security 的认证用户模型
GrantedAuthority 权限抽象,角色和权限点最终都可映射为它
RBAC 用户-角色-权限三层授权模型
roles.role 角色编码字段,建议使用 ROLE_ 前缀
四状态字段 enabled / accountNonExpired / accountNonLocked / credentialsNonExpired

八、总结

本章完成的核心升级:

  1. 建立了更规范的 RBAC 数据结构。
  2. 将账户状态字段真正接入 Spring Security 认证判定。
  3. 实现从数据库动态装载角色与权限集合。
  4. 梳理了 Mapper 绑定、泛型集合、空指针等高频问题。

到这里,你的项目已经不再是“演示级登录”,而是具备了真实系统常见的认证授权骨架。下一步可以继续做接口级授权规则(hasRole / hasAuthority)与异常返回统一化。


编辑者:Flittly
更新时间:2026年4月

目录
相关文章
|
27天前
|
JSON 安全 Java
【SpringSecurity新手村系列】(6)基于角色的权限控制、权限拦截注解与自定义无权限页面
本章在 RBAC 角色体系上,开启 @EnableMethodSecurity,使用 @PreAuthorize 配合 hasRole / hasAnyRole 实现 Controller 方法级拦截,并配置自定义 403 无权限页面,重点拆解 ROLE_ 前缀重复拼接的常见坑位。
136 1
|
2月前
|
人工智能 JSON 监控
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(10)Team Protocols (团队协议)
本章详解AI团队协议设计:通过request-id、状态机(pending→approved/rejected)和内存追踪器,实现关机握手与计划审批两大结构化交互。告别s09的随意消息,构建可追溯、可审批、线程安全的协作范式。
279 7
|
7天前
|
人工智能 Java API
多端CRM客户关系管理系统源码下载(PHP/Java/Python)完整开源版
本文深度解析PHP、Java、Python三大技术栈的开源CRM方案,涵盖多端协同架构、RBAC权限控制、客户公海回收、RESTful API设计及AI智能化演进,助成长型企业以低成本实现私有化、可定制、高扩展的CRM自主建设。
|
10天前
|
人工智能 监控 数据可视化
AI智能体的开发平台及特点
AI智能体开发平台已形成多层次生态:零代码平台(如Coze、Dify、Copilot Studio)面向业务人员,支持拖拽编排与企业集成;开发者框架(LangGraph、CrewAI、AutoGen)提供精细控制与多Agent协作;轻量平台(Poe)助力创作者快速分发变现。按需选择,高效落地。
|
27天前
|
SQL 安全 API
【SpringSecurity新手村系列】(7)基于资源权限码(Authority)的接口权限控制实战
本章完成从“用户-角色-权限资源”数据模型到 `@PreAuthorize` 方法级拦截的完整闭环。和上一章“角色控制(Role)”不同,本章重点是 **资源权限码(Authority)**,即 `clue:list`、`clue:edit` 这类细粒度权限。你将得到一套能直接用于企业项目的权限控制方案,同时规避 `Controller 未注册`、`Mapper SQL 字段写错`、`权限码字段映射错位` 等高频坑位。
172 4
|
17天前
|
SQL 关系型数据库 MySQL
一张5000万行的表,加索引从45秒到0.02秒——索引设计你真的会吗
本文实测5000万订单表:无索引查询45秒,加索引后仅0.02秒(提升2250倍)。详解索引原理、建索引时机、联合索引最左前缀、覆盖索引及隐式转换陷阱,干货不啰嗦!
|
26天前
|
数据采集 缓存 运维
IP查询工具如何评估IP负载?云上资源分配的实战方法
我们曾因P99延迟骤升盲目扩容无效,最终靠IP分桶定位到某云厂商ASN段的爬虫流量。IP查询工具不测性能,而是为请求打标签(ASN/代理类型/风险分等),结合监控数据精准识别“谁拖垮了系统”。分四类桶、设三条件、按优先级调度(分流>限流>扩容>封禁),离线缓存+二次验证,避免误伤。
|
1月前
|
存储 人工智能 开发者
AI Agent 越来越难迭代,你缺少的不是功能
还在担心 Token 消耗过多?还在纠结 Agent 难以优化?不改一行业务代码,LoongSuite Python 探针帮你把一次请求从头到尾捋顺:哪一步访问了什么模型、调用了什么工具、召回了哪些文档、花费了多少 token、上下文发生了什么变化。
219 31
|
1月前
|
缓存 NoSQL 网络协议
如何为我的网站或应用集成IP归属地查询功能?
本文为网站/应用集成IP归属地查询的落地指南:强调“取对IP”是前提(仅信可信上游、严滤私网),采用“本地+Redis缓存+在线API+硬超时熔断”架构,失败自动降级至省/国家;区分展示型与风控型模型,确保可解释、可审计、可回滚,并严守隐私合规红线。(239字)
203 13
|
1月前
|
缓存 资源调度 BI
《零成本提升QClaw运行速度,这5招就够了》
本文针对QClaw随使用时长增加逐渐卡顿的普遍痛点,打破“卡顿必升级硬件”的常见误区,指出问题根源在于默认配置不合理与错误使用习惯。作者通过三周系统性实测,总结出五个零成本、立竿见影的性能优化技巧,涵盖模型分层加载、动态上下文裁剪、任务批量合并、本地缓存分级管理与后台进程资源隔离。这些技巧无需额外投入,可让QClaw运行速度直接翻倍,且适用于所有本地运行的智能体工具,为技术从业者提供了可直接落地的通用性能优化方案。
373 9

热门文章

最新文章