【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月

目录
相关文章
|
5月前
|
JSON 安全 Java
【SpringSecurity新手村系列】(6)基于角色的权限控制、权限拦截注解与自定义无权限页面
本章在 RBAC 角色体系上,开启 @EnableMethodSecurity,使用 @PreAuthorize 配合 hasRole / hasAnyRole 实现 Controller 方法级拦截,并配置自定义 403 无权限页面,重点拆解 ROLE_ 前缀重复拼接的常见坑位。
441 1
|
5月前
|
SQL 安全 API
【SpringSecurity新手村系列】(7)基于资源权限码(Authority)的接口权限控制实战
本章完成从“用户-角色-权限资源”数据模型到 `@PreAuthorize` 方法级拦截的完整闭环。和上一章“角色控制(Role)”不同,本章重点是 **资源权限码(Authority)**,即 `clue:list`、`clue:edit` 这类细粒度权限。你将得到一套能直接用于企业项目的权限控制方案,同时规避 `Controller 未注册`、`Mapper SQL 字段写错`、`权限码字段映射错位` 等高频坑位。
393 4
|
2月前
|
人工智能 监控 测试技术
银行业AI架构:从裸调API到六层技能体系
# 银行AI智能体架构实战:从单体到Skill协同的技术演进 ## 痛点:银行IT架构的三重困境 走在任何一家银行的科技部走廊里,你都能听到同样的叹息:系统又慢了、需求又排不上、监管又来查了。这不是某一家银行的困境,而是整个银行业IT架构的共性问题。我们把它拆解为三重困境。 **困境一:单体系
|
NoSQL Java 关系型数据库
【AgentScope Java新手村系列】(5)记忆与会话管理
记忆与会话管理 — AgentState 管理上下文窗口,AgentStateStore 持久化,RuntimeContext.sessionId 隔离多用户会话。
645 0
|
Java easyexcel
Java:SpringBoot使用EasyExcel实现Excel文件的导出下载和上传导入功能
Java:SpringBoot使用EasyExcel实现Excel文件的导出下载和上传导入功能
1449 0
Java:SpringBoot使用EasyExcel实现Excel文件的导出下载和上传导入功能
|
3月前
|
Java Windows
JDK 8 安装与环境变量配置教程(jdk-8u121-windows-x64.exe 详细步骤)
本教程详解JDK 8u121 Windows 64位安装与配置:含管理员运行、路径选择、JAVA_HOME及Path环境变量设置,并通过java/javac -version命令快速验证,步骤清晰,适配Win10/Win11。
|
3月前
|
存储 运维 监控
《告别日志排查:OpenClaw如何修复工具错误指南》
传统工具调用系统依赖预先枚举的错误码,面对异构工具的指数级参数组合和隐蔽语义错误时彻底失效,只能靠人工排查海量日志救火。本文深入拆解OpenClaw的革命性设计,它彻底抛弃被动防御思路,构建了语法校验、语义验证、目标对齐三层递进的语义自愈体系。通过异常语义化建模、工具间协同纠错、动态粒度控制和自学习闭环,将异常转化为系统进化的养分,实现95%以上常见异常的自主修复。这套机制为通用智能体的鲁棒性提供了全新技术路径,重新定义了工具调用的可靠性标准。
304 9
|
5月前
|
安全 前端开发 Java
【SpringSecurity新手村系列】(1)初识安全框架
本文从零开始引入 Spring Security,演示默认登录页与接口保护效果,并解释认证、授权与过滤器链的基础机制,帮助你快速建立安全开发的整体认知。
356 1
|
5月前
|
JSON 前端开发 API
【LangGraph新手村系列】(1)LangGraph 入门:StateGraph 与带记忆的 ReAct 循环
介绍 LangGraph 核心思想:用 StateGraph 把单次 LLM 调用串成可循环的 ReAct 工作流。通过节点、边与公共状态黑板,实现模型思考、工具调用、条件跳转的闭环,并引入检查点让 Agent 拥有跨轮次记忆。
961 1
【LangGraph新手村系列】(1)LangGraph 入门:StateGraph 与带记忆的 ReAct 循环
|
5月前
|
NoSQL Java 数据库
【SpringAIAlibaba新手村系列】(11)Embedding 向量化与向量数据库
本文围绕 Embedding 与向量数据库展开,讲解了文本向量化、相似度检索和 VectorStore 的基本用法,并结合 SimpleVectorStore 示例说明了 Spring 中自动装配与手动注册 Bean 的区别,为后续学习 RAG 打下基础。
1279 5
【SpringAIAlibaba新手村系列】(11)Embedding 向量化与向量数据库

热门文章

最新文章