Spring Boot中基于JWT的角色权限与地域访问控制实现咨询
Spring Boot 角色+地域权限系统设计方案
一、数据库模型设计
核心表结构
1. 用户表(users)
存储用户基础信息:
id:主键,自增username:用户名,唯一password_hash:加密后的密码email:邮箱,可选created_at:创建时间
2. 角色表(roles)
定义系统角色:
id:主键,自增role_name:角色标识(admin/supervisor/customer),唯一description:角色描述,可选
3. 地域表(regions)
用自关联实现州→城市→街道的层级结构:
id:主键,自增name:地域名称(如“华盛顿州”“西雅图市”)parent_id:父地域ID(州的parent_id为null,城市关联对应州ID,街道关联对应城市ID)region_level:层级标识(1=州,2=城市,3=街道)
4. 用户权限关联表(user_role_regions)
记录用户对应角色的地域权限范围:
id:主键,自增user_id:关联users.idrole_id:关联roles.idregion_id:关联regions.id- 联合唯一约束:
(user_id, role_id, region_id),避免重复授权
表结构说明
- 地域表通过
parent_id和region_level实现层级递归,方便查询某一地域的所有下级节点(比如州下辖的所有城市、街道)。 - 用户权限关联表支持用户拥有多角色+多地域的组合权限(比如一个用户既是A州的admin,又是B城市的supervisor)。
二、登录后地域权限校验逻辑(结合JWT)
1. 登录流程优化
- 用户提交用户名密码验证通过后,查询该用户的所有
user_role_regions记录,提取角色标识、授权地域ID、授权地域层级,将这些信息存入JWT的Payload中,示例Payload:{ "sub": "user_001", "exp": 1728000000, "permissions": [ {"role": "admin", "region_id": 1, "region_level": 1}, {"role": "supervisor", "region_id": 5, "region_level": 2} ] } - 无需在登录阶段提前过滤地域,只需把权限元数据存入JWT,后续请求时做实时校验。
2. 请求拦截与权限校验
基于Spring Security实现拦截逻辑,步骤如下:
(1)解析JWT获取权限信息
自定义JWT过滤器,在请求到达Controller前解析JWT,将用户权限信息存入Authentication对象的principal中(可扩展UserDetails类,新增permissions字段)。
(2)地域权限校验逻辑
针对每个业务请求,提取请求中涉及的目标地域ID(比如查询某城市数据时的城市ID),执行以下校验:
- 步骤1:查询目标地域的完整层级链:通过递归SQL获取目标地域及其所有上级地域的ID(比如街道→城市→州)。
递归SQL示例:WITH RECURSIVE region_hierarchy AS ( SELECT id, parent_id FROM regions WHERE id = ? UNION ALL SELECT r.id, r.parent_id FROM regions r JOIN region_hierarchy rh ON r.id = rh.parent_id ) SELECT id FROM region_hierarchy; - 步骤2:匹配用户权限:遍历用户的权限列表,检查是否存在任意一条权限满足:
- 目标地域的层级链包含该权限的
region_id; - 角色对应的权限范围覆盖目标地域层级(比如
admin角色授权州级地域时,允许访问该州及下辖所有城市、街道;customer角色若授权街道级地域,则仅允许访问该街道)。
- 目标地域的层级链包含该权限的
(3)代码实现示例
自定义Spring Security权限表达式,方便在Controller中直接使用:
@Component public class RegionSecurityExpressions { private final RegionRepository regionRepository; public RegionSecurityExpressions(RegionRepository regionRepository) { this.regionRepository = regionRepository; } public boolean hasRegionAccess(Authentication auth, Long targetRegionId) { CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); Set<Long> targetHierarchy = regionRepository.findRegionHierarchyIds(targetRegionId); for (UserPermission perm : user.getPermissions()) { Set<Long> allowedRegions = regionRepository.findSubtreeIds(perm.getRegionId()); // 检查目标地域是否在授权地域的子树中 if (allowedRegions.contains(targetRegionId)) { // 可针对不同角色添加额外限制,比如customer仅允许访问街道级 if ("customer".equals(perm.getRole()) && perm.getRegionLevel() != 3) { continue; } return true; } } return false; } }
在Controller中使用:
@RestController @RequestMapping("/api/streets") public class StreetController { @GetMapping("/{regionId}") @PreAuthorize("@regionSecurityExpressions.hasRegionAccess(authentication, #regionId)") public List<Street> getStreets(@PathVariable Long regionId) { // 业务逻辑实现 } }
3. 性能优化建议
- 缓存授权地域子树:将用户权限对应的地域子树ID列表存入Redis,有效期与JWT一致,避免每次请求都执行递归SQL。
- JWT黑名单机制:当用户权限变更时,将旧JWT加入黑名单,在过滤器中校验,及时失效旧权限。
内容的提问来源于stack exchange,提问作者Temesgen Mihiretu
相关产品推荐
相关产品推荐

