完整认证授权流程与权限控制实现
一、整体架构概览
认证授权流程 = 认证(Authentication) + 授权(Authorization)
├── 认证:验证"你是谁" → 身份确认
│ ├── 用户名密码验证
│ ├── Token生成与验证
│ └── 用户信息加载
└── 授权:验证"你能做什么" → 权限控制
├── 按钮权限控制:操作权限
└── 数据权限控制:数据访问范围
二、用户登录认证流程
-
前端提交登录信息:用户在前端界面输入用户名和密码,前端将加密后的凭证发送到后端登录接口。
-
后端验证用户凭证:后端接收到登录请求后,通过用户服务验证用户名和密码。验证通过后,生成一个唯一的用户标识(通常是用户ID)和Token。
-
加载用户权限信息:登录成功后,后端会调用
SysUserDetailServiceImpl.initUserData()方法,加载用户的按钮权限和数据权限。-
按钮权限:通过用户关联的角色,查询角色拥有的菜单权限(即权限标识,如
sys:user:save),形成一个权限标识集合,存储到UserDetail对象的authoritySet字段。 -
数据权限:通过用户关联的角色,查询角色定义的数据权限范围(部门ID列表),存储到
UserDetail对象的deptIdList字段。
-
-
缓存用户信息:将包含权限信息的
UserDetail对象缓存到Redis中,键为user:auth:{userId},并设置过期时间(如2小时)。同时,将权限列表单独缓存,以便快速获取。 -
生成Token并返回:后端生成一个JWT Token,其中包含用户ID和必要信息,并将Token返回给前端。前端将Token存储在本地(如localStorage或cookie),并在后续请求中携带。
三、业务请求授权流程
-
前端携带Token发送请求:前端在请求头中携带JWT Token,发送业务请求到后端。
-
Token验证与用户信息获取:后端的认证过滤器(如
AuthenticationTokenFilter)拦截请求,从请求头中提取Token,并验证Token的有效性。验证通过后,从Token中解析出用户ID,然后从Redis缓存中获取用户信息(即UserDetail对象)。如果缓存不存在,则重新加载用户信息并缓存。 -
设置安全上下文:将获取到的用户信息(包括权限信息)设置到Spring Security的
SecurityContext中,这样在后续的业务逻辑中可以通过SecurityUtils.getUserDetail()获取当前用户信息。 -
按钮权限控制:在控制器方法上,使用
@PreAuthorize("hasAuthority('权限标识')")注解。当请求进入控制器方法前,Spring Security会根据SecurityContext中的用户权限信息,判断用户是否拥有该注解指定的权限标识。如果有,则允许访问;否则,返回403错误。 -
数据权限控制:在服务层方法上,使用自定义的
@DataScope注解标记需要进行数据权限过滤的方法。通过MyBatis拦截器(DataFilterInterceptor)拦截查询SQL,根据当前用户的部门ID列表(从UserDetail对象的deptIdList获取)重写SQL,在WHERE子句中添加部门ID过滤条件。这样,查询结果就只限于用户有权限访问的数据。 -
返回响应:业务逻辑执行完毕后,将结果返回给前端。
四、按钮权限控制实现详解
1. 权限标识体系设计
系统采用三层权限标识设计,确保权限控制的精确性:
-
第一层:模块(sys) - 标识功能模块
-
第二层:实体(user) - 标识操作对象
-
第三层:操作(save) - 标识具体操作
2. 权限加载过程
用户登录时,系统通过以下步骤加载按钮权限:
-
查询用户角色:根据用户ID查询关联的角色列表
-
获取菜单权限:根据角色查询关联的菜单权限
-
提取权限标识:从菜单配置中提取permissions字段的权限标识
-
权限去重合并:多个角色的权限去重合并
-
缓存到Redis:权限集合缓存到Redis,key格式:
menu:permissions:{userId}
3. 权限验证机制
3.1 后端验证(Spring Security)
在控制器方法上使用注解式权限控制:
@PreAuthorize("hasAuthority('sys:user:save')")
public Result saveUser() {
// 只有拥有sys:user:save权限的用户能执行此方法
}
Spring Security的执行流程:
-
拦截请求:Spring Security过滤器链拦截请求
-
读取注解:解析控制器方法上的@PreAuthorize注解
-
获取权限:从SecurityContext中获取用户权限集合
-
验证权限:检查用户权限集合是否包含所需权限
-
决定放行/拒绝:验证通过则放行,否则抛出AccessDeniedException
3.2 前端验证(Vue + Pinia)
前端通过统一的权限验证方法控制UI元素显示:
// 检查权限的公共方法
hasPermission(permission) {
return this.$store.state.user.permissions.includes(permission);
}
// 使用示例
<button v-if="hasPermission('sys:user:delete')">删除</button>
五、数据权限控制实现详解
1. 数据权限模型设计
系统采用基于角色的数据权限模型:
-
角色数据范围:在sys_role_data_scope表中定义每个角色可访问的部门
-
用户数据范围:用户拥有的所有角色数据范围的并集
-
最终数据权限 = 用户所属部门 + 角色数据范围部门
2. 数据权限加载过程
用户登录时,系统通过以下步骤加载数据权限:
-
查询用户直接部门:从sys_user_dept表查询用户直接所属部门
-
查询角色数据范围:根据用户角色查询数据权限范围
-
合并部门列表:合并直接部门和角色数据范围部门,去重
-
缓存到Redis:部门ID列表缓存到Redis,key格式:
data:scope:{userId}
3. 数据权限过滤机制
3.1 MyBatis拦截器实现
系统通过自定义MyBatis拦截器实现数据权限的自动注入:
拦截器工作流程:
-
拦截SQL查询:拦截所有执行查询的Mapper方法
-
检查是否需要过滤:通过@DataScope注解标记需要数据过滤的方法
-
获取用户数据权限:从当前线程的UserContext获取部门ID列表
-
重写SQL语句:在WHERE条件中注入部门过滤条件
-
执行修改后的SQL:将重写后的SQL交给MyBatis执行
SQL重写示例:
-- 原始SQL
SELECT * FROM sys_user WHERE status = 1
-- 重写后的SQL(假设用户可访问部门1,2,3)
SELECT * FROM sys_user
WHERE status = 1
AND dept_id IN (1, 2, 3)
3.2 多租户数据隔离
在SaaS多租户系统中,数据权限还包含租户隔离:
-- 重写后的SQL(同时包含租户隔离和数据权限)
SELECT * FROM sys_user
WHERE status = 1
AND tenant_id = 1001 -- 租户隔离
AND dept_id IN (1, 2, 3) -- 数据权限
六、Redis在权限控制中的作用
1. Redis缓存结构设计
user:auth:{userId} # 用户完整认证信息
menu:permissions:{userId} # 按钮权限集合
data:scope:{userId} # 数据权限部门列表
menu:tree:{userId} # 用户菜单树
token:{jwtToken} # Token与用户ID映射
2. 缓存读写策略
-
写缓存(登录时):
-
查询数据库获取用户权限信息
-
将权限信息序列化存储到Redis
-
设置合理的过期时间(如2小时)
-
-
读缓存(请求时):
-
从请求头获取Token
-
从Redis获取用户权限信息
-
如果缓存不存在,重新从数据库加载
-
-
缓存失效策略:
-
主动失效:用户权限变更时主动清除缓存
-
被动失效:设置TTL自动过期
-
强制失效:用户登出时清除相关缓存
-
七、总结
该系统的认证授权流程实现了完整的权限控制闭环:
-
认证流程:从用户登录到Token生成,确保用户身份安全可靠
-
按钮权限控制:通过权限标识体系,实现操作级别的精细控制
-
数据权限控制:通过SQL重写技术,实现数据访问范围的控制
-
缓存优化:利用Redis缓存权限信息,大幅提升系统性能
-
实时生效:通过事件驱动机制,保证权限变更及时生效
这种设计实现了多层次、细粒度、高性能的权限控制,既能满足企业级应用的安全要求,又能保证系统的响应速度和用户体验。通过前后端协同、数据库拦截器和缓存技术的结合,构建了一个完整、安全、高效的权限控制体系。
浙公网安备 33010602011771号