查看源文件

RBAC,全称为 Role-Based Access Control,翻译过来叫基于角色的访问控制,通过将用户分配到不同的角色,并为角色分配权限,从而实现对资源的访问控制。

那么,如何实现 RBAC?RAG-Agent 使用 Spring Security。Spring Security 本质上是一个基于 Filter 的安全框架。当请求进入应用时,会经过一系列安全过滤器链,每个过滤器负责不同的安全功能,比如认证、授权、CSRF 防护等。

Spring Security 主要解决了两大核心问题:认证(Authentication)和授权(Authorization)。认证就是验证“你是谁”,比如用户登录时验证用户名密码是否正确;授权就是验证“你能做什么”,比如检查用户是否有权限访问某个页面或调用某个接口。

本篇教程将介绍如何基于 Spring Security 实现 RBAC,涵盖用户注册、登录、JWT 认证、请求拦截与授权等核心流程。

一、RBAC 的核心概念

RBAC 的核心思想是给用户赋予某些角色,而每个角色拥有某些权限。也就是说,一个权限可以分配给多个角色,一个角色可以拥有多个权限;一个用户可以分配多个角色,一个角色也可以对应多个用户。对应模型如下:

对于 RAG-Agent 来说,每家公司都有自己的组织架构。比如,管理知识库的人员可能包括王老板和毕小三等;王老板不仅拥有毕小三的权限,也拥有李四眼的权限。这种管理关系中,上层角色兼容下层角色的权限,需要用到角色继承的 RBAC 模型。 角色继承的 RBAC 模型的思路是上层角色继承下层角色的所有权限,并且可以额外拥有其他权限。

在 RAG-Agent 系统中,Spring Security 的 RBAC 实现分为以下几个层次:

①、基础角色的定义:

  • USER(普通用户) :可以上传文件、进行对话、查看自己的历史记录

  • ADMIN(管理员) :除了普通用户的所有权限外,还能管理所有用户、查看系统状态、管理知识库

②、URL 级别的权限控制:

③、组织标签权限控制:

  • 私人空间 :以”PRIVATE_“开头的资源,只有创建者能访问

  • 组织资源 :根据用户的组织标签来控制访问权限

  • 公开资源 :所有认证用户都能访问

二、RAG-Agent中权限控制的代码结构

RAG-Agent 的 Spring Security RBAC 实现采用分层架构,各层职责明确。

配置层(config)是整个安全框架的”大脑”,负责制定游戏规则。SecurityConfig.java 相当于安全系统的总指挥,定义了哪些 URL 需要什么权限,配置过滤器链的执行顺序。JwtAuthenticationFilter.java 则像门卫一样,检查每个请求的身份证(JWT 令牌)是否有效。

控制器层(controller)是用户请求的第一站,负责接收和响应。UserController.java 处理用户相关的所有操作,如登录、注册、获取用户信息等,是用户与系统交互的入口。

服务层(service)是业务逻辑的核心,负责具体的业务处理。UserService.java 处理用户相关的业务逻辑,比如用户注册时的数据验证、密码加密等。CustomUserDetailsService.java 专门为 Spring Security 服务,负责从数据库加载用户信息并转换成 Spring Security 能理解的格式。

数据访问层(repository)是与数据库打交道的地方。UserRepository.java 定义了如何从数据库中查询用户信息,使用 Spring Data JPA 简化数据库操作。

数据模型层(model)是数据的”蓝图”。User.java 定义了用户实体的结构,包括用户名、密码、角色、组织标签等字段。

工具类层(utils)是系统的”工具箱”。PasswordUtil.java 负责密码的加密和验证,确保密码安全。JwtUtils.java 负责 JWT 令牌的生成、解析和验证,是无状态认证的核心。

配置文件(resources)中的 application.yml 是系统的配置中心,包含数据库连接、JWT 密钥、Redis 配置等重要参数。

测试层(test)中的 UserServiceTest.java 确保业务逻辑的正确性,通过单元测试验证各个功能模块。

三、RAG-Agent中权限控制的核心代码实现

01、用户模型类

User 实体类是RAG-Agent系统实现 RBAC 权限控制的数据基础,通过 JPA 注解实现完整的用户信息管理。 @Entity 和 @Table 注解将其映射到数据库的”users”表,并通过 @UniqueConstraint(columnNames = "username") 在数据库层面确保用户名的唯一性,防止重复注册。

role 字段是 RBAC 的核心,使用 @Enumerated(EnumType.STRING) 将枚举值以字符串形式存储在数据库中。内部定义的 Role 枚举包含 USER 和 ADMIN 两个角色。Spring Security 可以轻松地将角色转换为 GrantedAuthority ,实现方法级和 URL 级的权限控制。

02、用户数据访问接口

UserRepository 提供了一个方法 findByUsername ,用于根据用户名查找用户。

03、密码工具类

PasswordUtil 工具类在用户注册时调用 encode() 方法对密码进行加密,在用户登录时通过 matches() 方法对密码进行验证。

BCrypt 算法的优势在于它是一种自适应哈希函数,内置了盐值(salt)机制,每次加密同一个密码都会产生不同的哈希值,可以防止彩虹表攻击。同时 BCrypt 算法具有可调节的计算复杂度,能够抵御暴力破解攻击。

04、JWT 工具类

JwtUtils 负责生成、验证和解析 JWT 令牌。类中使用 @Value("${jwt.secret-key}") 从配置文件读取 Base64 编码的密钥,通过 getSigningKey() 方法解码并生成 SecretKey 对象。令牌有效期为24小时,并使用 HS256 签名算法确保令牌的完整性。

generateToken(String username) 方法是权限信息封装的核心,它 不仅将用户名作为 subject,还将权限信息封装到 claims 中,包括用户角色(role)、用户ID(userId)、组织标签(orgTags)和主组织(primaryOrg)等。这种设计使得 JWT 令牌成为一个自包含 的权限载体,避免频繁的数据库查询。

JwtUtils还提供了多个 extract 方法,分别用于提取用户名、用户ID、角色、组织标签等信息。这些方法可以在 JwtAuthenticationFilter 和 OrgTagAuthorizationFilter 中将令牌转换为权限上下文。特别是 extractOrgTagsFromToken() 和 extractPrimaryOrgFromToken() 方法,为组织级别的权限控制提供了数据基础。

05、自定义用户详情服务

CustomUserDetailsService 实现了 Spring Security 的 UserDetailsService 接口,负责在认证过程中加载用户详细信息。

当用户尝试登录时,Spring Security 会调用 loadUserByUsername 方法根据用户名查找用户信息。并将 User 实体转换为 Spring Security 标准的 UserDetails 对象,包含三个关键信息:用户名( user.getUsername() )、加密密码( user.getPassword() )和 权限集合(当用户尝试登录时,Spring Security 会调用 loadUserByUsername 方法根据用户名查找用户信息。并将 User 实体转换为 Spring Security 标准的 UserDetails 对象,包含三个关键信息:用户名( user.getUsername() )、加密密码( user.getPassword() )和权限集合(

getAuthorities(user.getRole()) )。

getAuthorities(User.Role role) 方法则将用户的角色枚举转换为 Spring Security 的 GrantedAuthority 格式。通过在角色名前添加”ROLE_“前缀(如”ROLE_ADMIN”、“ROLE_USER”),使得系统能够使用 Spring Security 的方法级和 URL 级权限注解,如 @PreAuthorize(“hasRole(‘ADMIN’)”) 和 @Secured(“ROLE_USER”) 。

06、JWT 认证过滤器

JwtAuthenticationFilter 继承了 OncePerRequestFilter,确保每个请求只被处理一次,负责在每个 HTTP 请求中验证 JWT 令牌并设置 Spring Security 认证上下文。

doFilterInternal() 方法是过滤器的核心,在每个请求到达控制器之前被调用。方法首先通过 extractToken(request) 从HTTP请求头的”Authorization”字段中提取 JWT 令牌,支持标准的”Bearer “前缀格式。如果请求头中没有令牌或格式不正确,则跳过认证过程,让请求继续传递到下一个过滤器。

当成功提取到令牌后,过滤器调用 jwtUtils.validateToken(token) 验证令牌的有效性,包括签名验证、过期时间检查等。验证通过后,使用

jwtUtils.extractUsernameFromToken(token) 从令牌中提取用户名,然后通过 userDetailsService.loadUserByUsername(username) 加载完整的用户详细信息,包括用户权限。

07、安全配置类

SecurityConfig 定义了哪些 URL 需要什么级别的权限,并配置了认证和授权过滤器的执行顺序,以及会话管理策略。

比如说静态资源和 WebSocket 连接使用 permitAll() 允许匿名访问;登录注册接口对所有用户开放;文件上传、对话历史、搜索等业务接口要求 hasAnyRole(“USER”, “ADMIN”) ,允许普通用户和管理员访问;管理员专属接口使用 hasRole(“ADMIN”) 严格限制只有管理员才能访问;其他所有请求都需要通过 authenticated() 进行身份验证。

并采用 SessionCreationPolicy.STATELESS 无状态会话策略,完全依赖 JWT 令牌进行身份验证,不在服务器端维护会话状态。

四、RBAC 的实现流程

整个认证与权限控制流程从用户注册开始。用户首先通过 /api/v1/users/register 接口提交用户名和密码,系统会先检查用户名是否已存在,如果通过校验,则会为该用户分配一个默认角色(通常是 USER)。

注册完成后,用户可以通过 /api/v1/users/login 接口进行登录。系统会根据提交的用户名和密码进行验证,验证通过后,会生成一个包含用户信息(如用户名和角色)的 JWT Token,返回给客户端。

客户端在后续访问其他接口时,需要将这个 JWT Token 放入请求头中的 Authorization 字段中。系统会通过自定义的 JwtAuthenticationFilter 对请求进行拦截,提取并解析 Token,验证其合法性,并将对应的用户信息加载到 Spring Security 的上下文中。

最后,访问控制是基于角色来实现的。Spring Security 会根据配置类 SecurityConfig 中设定的权限规则,判断当前用户是否有权访问请求的接口。例如,只有拥有 ADMIN 角色的用户才能访问后台管理接口。

五、面试题:RAG-Agent的 RBAC 还能继续优化吗?

首先是 支持更多角色。如果系统有新的业务需求,比如需要增加一个“版主(MODERATOR)”角色,只需要在 User.Role 枚举中添加对应的枚举值,并在 SecurityConfig 中为这个新角色配置访问规则即可。

其次是引入 动态权限管理。相比在代码里写死角色权限,我们可以将角色与权限信息存储到数据库中,通过服务在运行时动态加载。配合 Spring Security 提供的 @PreAuthorize 注解,可以实现粒度更细、控制更灵活的方法级权限校验。

最后是 Token 刷新机制。为提升用户体验,我们可以增加刷新 Token 的功能。当用户的访问 Token 快过期时,通过一个专门的接口获取新的 Token,从而延长登录有效期,避免频繁重新登录。这一步通常需要配合 Refresh Token 实现。这也是面试中可能考察的一个知识点。

例如,项目中的 JWT 使用哪种类型的 token(长 token 还是短 token、access token 还是 refresh token)?

第一种回答是使用长 token,并将有效期设为 30 天,这样用户不必频繁重新登录,体验更流畅。用户登录成功后,后端将生成的 JWT 返回给前端,前端再将其保存在本地缓存。之后,前后端每次交互时,前端都会把 JWT 放在请求头中,例如 HTTP 身份认证的 Authorization 请求头,也可以通过自定义请求头传递。后端收到请求后,从请求头中获取 JWT 并校验;校验通过后才响应接口,否则视为未登录。

追问:JWT 不支持续期的话,用户正在使用时会不会遇到登录失效?

**是的,确实会这样。但可以通过以下方式解决:**每次用户请求时,如果 token 快要过期(比如只剩 5 分钟),就自动生成一个新 token 并返回给前端,由前端替换旧 token;也可以用 Redis 设置有效期和 TTL( Time To Live,生存时间),并滑动延长。

用户登录时生成 token 并存入 Redis,设置有效期,例如三十分钟。用户发起请求时,拦截器会获取 token 并检查它是否存在于 Redis 中、是否过期。如果仍然有效,就自动延长 30 分钟。每次请求都会延期,让 TTL 滑动前进:用户只要保持活跃就不会掉线;长时间不操作时,TTL 会自动过期,登录状态也随之失效。

登录接口

拦截器:

Redis 工具类: