您现在的位置是:首页 >学无止境 >java每日精进2.8(SpringSecurity应用鉴权)网站首页学无止境

java每日精进2.8(SpringSecurity应用鉴权)

为美好的生活献上祝福 2025-03-13 18:15:46
简介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 对象,它的 KeyLong 类型,表示用户的 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;
    }

根据权限获取权限可以访问的菜单,再根据菜单获取可以访问菜单的角色,最后判断和用户的角色是否有交集,要有交集则证明用户可以访问此菜单;

对每个权限对进行判断,有一个即可访问;

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。