您现在的位置是:首页 >学无止境 >java每日精进2.8(SpringSecurity应用鉴权)网站首页学无止境
java每日精进2.8(SpringSecurity应用鉴权)
1.一个接口的执行流程
@GetMapping("/list")
@Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面")
@PreAuthorize("@ss.hasPermission('system:menu:query')")
public CommonResult<List<MenuRespVO>> getMenuList(MenuListReqVO reqVO) {
List<MenuDO> list = menuService.getMenuList(reqVO);
list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuRespVO.class));
}
1.请求发送(HTTP 请求)
假设有一个 HTTP 请求访问 /list
路径,方法是 GET
,请求的 URL 类似于 http://example.com/list
。
2.Spring MVC 接收到请求:
Spring Boot 的 @RestController
或 @Controller
会接收到这个请求,并通过 @GetMapping("/list")
注解来匹配这个请求,找到对应的 getMenuList
方法。
3.方法入口:
当请求到达 getMenuList
方法时,Spring 会将请求中的参数绑定到 MenuListReqVO
对象(例如:reqVO
)。如果请求中有参数(如分页信息、过滤条件等),Spring 会根据请求的参数自动映射到 MenuListReqVO
类的字段。
例如,假设请求 URL 是 http://example.com/list?page=1&size=10
,那么 reqVO
对象会被填充为:
MenuListReqVO reqVO = new MenuListReqVO(); reqVO.setPage(1); reqVO.setSize(10);
4.权限校验(@PreAuthorize 注解):
在方法执行之前,@PreAuthorize
注解指定了一个表达式 @ss.hasPermission('system:menu:query')
,这个表达式会被 Spring Security 执行。
@ss
是 Spring Security 中的一个预定义变量,通常它代表一个自定义的权限检查服务类。可以在 Spring 容器中配置并注入该类。
@PreAuthorize("@ss.hasPermission('system:menu:query')")
会调用 SecurityFrameworkServiceImpl
中的 hasPermission
方法来验证当前用户是否具有 system:menu:query
权限
@Override
@SneakyThrows
public boolean hasAnyPermissions(String... permissions) {
Long userId = getLoginUserId();
if (userId == null) {
return false;
}
return hasAnyPermissionsCache.get(new KeyValue<>(userId, Arrays.asList(permissions)));
}
/**
* 针对 {@link #hasAnyPermissions(String...)} 的缓存
*/
private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
@Override
public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
}
});
这段代码定义了一个缓存 hasAnyPermissionsCache
,用于存储基于 KeyValue<Long, List<String>>
作为键值对的权限检查结果。它使用了 LoadingCache
来实现缓存,缓存的过期时间设置为 1 分钟。每当缓存中没有对应的值时,会通过 CacheLoader
进行加载并进行权限检查。
1.LoadingCache<KeyValue<Long, List<String>>, Boolean>
LoadingCache
是 Guava 库提供的一个缓存类,它在缓存中没有相应值时,能够自动加载数据,并将加载的结果缓存起来。在这段代码中:
KeyValue<Long, List<String>>
:缓存的键是一个 KeyValue
对象,它的 Key
是 Long
类型,表示用户的 ID 或其他相关的键;Value
是一个 List<String>
,表示权限的列表(多个权限名称)。
Boolean
:缓存的值是一个 Boolean
类型,表示是否拥有这些权限的检查结果。
2.buildCache(...)
方法
buildCache(...)
是一个方法,返回一个已经配置好的 LoadingCache
实例。通过这个方法,我们可以设置缓存的行为,例如过期时间、最大缓存数量等。
Duration.ofMinutes(1L)
:设置缓存的过期时间为 1 分钟,也就是说,缓存存储的数据会在 1 分钟后失效,过期后需要重新加载数据。
3. CacheLoader<KeyValue<Long, List<String>>, Boolean>
CacheLoader
是 Guava 中的一个接口,提供了缓存未命中时的加载机制。它包含一个 load()
方法,用于定义缓存值的加载逻辑。当缓存中没有需要的数据时,会调用 load()
方法来加载并返回相应的值。
5. 调用远程服务进行权限验证
如果缓存中没有相应的权限信息,hasAnyPermissionsCache.get
会通过 PermissionApi
调用远程服务,验证用户是否具备 system:menu:query
权限。
@Override
public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
}
permissionApi.hasAnyPermissions
会发起 HTTP 请求到权限服务,通过 FeignClient
调用 PermissionApi
中的 hasAnyPermissions
接口。
PermissionApi
中的 hasAnyPermissions
方法:
@GetMapping(PREFIX + "/has-any-permissions")
@Operation(summary = "判断是否有权限,任意一个即可")
@Parameters({
@Parameter(name = "userId", description = "用户编号", example = "1", required = true),
@Parameter(name = "permissions", description = "权限", example = "read,write", required = true)
})
CommonResult<Boolean> hasAnyPermissions(@RequestParam("userId") Long userId,
@RequestParam("permissions") String... permissions);
这个接口会判断给定的 userId
是否有传入的 permissions
中的任意一个权限。它通过 CommonResult<Boolean>
返回结果,表示是否有权限。
@Override
public boolean hasAnyPermissions(Long userId, String... permissions) {
// 如果为空,说明已经有权限
if (ArrayUtil.isEmpty(permissions)) {
return true;
}
// 获得当前登录的角色。如果为空,说明没有权限
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
if (CollUtil.isEmpty(roles)) {
return false;
}
// 情况一:遍历判断每个权限,如果有一满足,说明有权限
for (String permission : permissions) {
if (hasAnyPermission(roles, permission)) {
return true;
}
}
// 情况二:如果是超管,也说明有权限
return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId));
}
/**
* 判断指定角色,是否拥有该 permission 权限
*
* @param roles 指定角色数组
* @param permission 权限标识
* @return 是否拥有
*/
private boolean hasAnyPermission(List<RoleDO> roles, String permission) {
List<Long> menuIds = menuService.getMenuIdListByPermissionFromCache(permission);
// 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限
if (CollUtil.isEmpty(menuIds)) {
return false;
}
// 判断是否有权限
Set<Long> roleIds = convertSet(roles, RoleDO::getId);
for (Long menuId : menuIds) {
// 获得拥有该菜单的角色编号集合
Set<Long> menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId);
// 如果有交集,说明有权限
if (CollUtil.containsAny(menuRoleIds, roleIds)) {
return true;
}
}
return false;
}
根据权限获取权限可以访问的菜单,再根据菜单获取可以访问菜单的角色,最后判断和用户的角色是否有交集,要有交集则证明用户可以访问此菜单;
对每个权限对进行判断,有一个即可访问;