Skip to content

引言

在项目中,可以通过自定义权限校验逻辑结合 @PreAuthorize 注解实现细粒度的权限控制。@PreAuthorize 允许在方法调用前执行权限校验,通过 SpEL(Spring Expression Language) 表达式调用自定义的权限校验方法。

添加依赖

pom.xml 配置文件中添加以下依赖:

js
<!-- spring security 安全认证 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

xiaomayi-common/xiaomayi-security 模块中已经引入此依赖,在实际使用时直接引入以下依赖即可:

js
<!-- 安全认证依赖模块 -->
<dependency>
    <groupId>com.xiaomayi</groupId>
    <artifactId>xiaomayi-security</artifactId>
</dependency>

自定义权限校验服务

创建一个服务类,用于实现权限校验逻辑。例如,检查当前用户是否拥有某个权限节点。

js
package com.xiaomayi.security.service;

import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.security.constant.SecurityConstant;
import com.xiaomayi.security.security.LoginUser;
import com.xiaomayi.security.utils.SecurityUtils;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 自定义权限
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
// 此处给服务起别名,便于节点权限设置时使用
@Service("pms")
public class PermissionService {

    /**
     * 自定义节点权限
     *
     * @param authority 权限标识
     * @return 返回结果
     */
    public boolean hasAuthority(String authority) {
        // 权限节点判空
        if (StringUtils.isEmpty(authority)) {
            return false;
        }
        // 获取当前用户
        LoginUser user = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(user)) {
            return false;
        }
        // 获取权限节点集合
        List<String> permissions = user.getPermissions();
        // 判断是否拥有全部权限
        if (permissions.contains(SecurityConstant.ALL_PERMISSION)) {
            return true;
        }
        // 判断是否拥有节点权限
        if (permissions.contains(authority)) {
            return true;
        }
        return false;
    }

    /**
     * 自定义角色权限
     *
     * @param authority 权限标识
     * @return 返回结果
     */
    public boolean hasRole(String authority) {
        // 权限节点判空
        if (StringUtils.isEmpty(authority)) {
            return false;
        }
        // 获取当前用户
        LoginUser user = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(user)) {
            return false;
        }
        // 获取角色权限集合
        List<String> roles = user.getRoles();
        // 判断是否拥有超管权限
        if (roles.contains(SecurityConstant.SUPER_ADMIN)) {
            return true;
        }
        // 判断是否拥有角色权限
        if (roles.contains(authority)) {
            return true;
        }
        return false;
    }

}

启用方法级安全控制

在 [安全认证服务] 章节中的 SecurityConfig 规则配置类中启用方法级安全控制:

js
// 启用@PreAuthorize等注解
@EnableMethodSecurity(prePostEnabled = true)
js
package com.xiaomayi.security.config;

import com.xiaomayi.security.filter.JwtAuthenticationTokenFilter;
import com.xiaomayi.security.filter.ResponseFilter;
import com.xiaomayi.security.handler.*;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.CorsFilter;

import java.util.stream.Collectors;

/**
 * <p>
 * 安全认证配置类
 * 特别提醒:新版本中WebSecurityConfigurerAdapter已启用,目前采用新的写法
 * 注意:SpringSecurity 6 没有了需要继承类这个做法,但是需要配置注解
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
@AllArgsConstructor
public class SecurityConfig {

    // 以下内容全部省略...

}

在方法上使用

在需要权限控制的方法上使用 @PreAuthorize 注解,调用自定义的权限校验方法,以 用户管理 模块控制器为例。

js
package com.xiaomayi.admin.controller;

import cn.hutool.core.util.RandomUtil;
import com.itextpdf.text.DocumentException;
import com.xiaomayi.core.config.AppConfig;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.excel.annotation.RequestExcel;
import com.xiaomayi.excel.annotation.ResponseExcel;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.security.utils.SecurityUtils;
import com.xiaomayi.system.dto.user.*;
import com.xiaomayi.system.service.UserService;
import com.xiaomayi.system.utils.ParamResolver;
import com.xiaomayi.system.vo.user.UserExcelVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

/**
 * <p>
 * 用户 前端控制器
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-03-23
 */
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户管理")
@AllArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * 查询分页列表
     *
     * @param userPageDTO 查询条件
     * @return 返回结果
     */
    @Operation(summary = "查询分页列表", description = "查询分页列表")
    @PreAuthorize("@pms.hasAuthority('sys:user:page')")
    @GetMapping("/page")
    public R page(UserPageDTO userPageDTO) {
        return R.ok(userService.page(userPageDTO));
    }

    /**
     * 查询数据列表
     *
     * @param userListDTO 查询条件
     * @return 返回结果
     */
    @Operation(summary = "查询数据列表", description = "查询数据列表")
    @PreAuthorize("@pms.hasAuthority('sys:user:list')")
    @GetMapping("/list")
    public R getList(UserListDTO userListDTO) {
        return R.ok(userService.getList(userListDTO));
    }

    /**
     * 根据ID查询详情
     *
     * @param id 用户ID
     * @return 返回结果
     */
    @Operation(summary = "根据ID查询详情", description = "根据ID查询详情")
    @PreAuthorize("@pms.hasAuthority('sys:user:detail')")
    @GetMapping("/detail/{id}")
    public R getDetail(@PathVariable("id") Integer id) {
        return R.ok(userService.getDetail(id));
    }

    /**
     * 添加用户
     *
     * @param userAddDTO 参数
     * @return 返回结果
     */
    @Operation(summary = "添加用户", description = "添加用户")
    @RequestLog(title = "添加用户", type = RequestType.INSERT, exclude = {"password"})
    @PreAuthorize("@pms.hasAuthority('sys:user:add')")
    @PostMapping("/add")
    public R add(@RequestBody @Validated UserAddDTO userAddDTO) {
        // 登录密码
        String password = userAddDTO.getPassword();
        if (StringUtils.isNotEmpty(password)) {
            // 加密盐
            String salt = RandomUtil.randomString(10);
            userAddDTO.setSalt(salt);
            // 密码基于SpringSecurity加密处理
            userAddDTO.setPassword(SecurityUtils.encryptPassword(salt + password));
        } else {
            userAddDTO.setPassword(null);
        }
        // 租户ID
        userAddDTO.setTenantId(SecurityUtils.getTenantId());
        return userService.add(userAddDTO);
    }

    /**
     * 更新用户
     *
     * @param userUpdateDTO 参数
     * @return 返回结果
     */
    @Operation(summary = "更新用户", description = "更新用户")
    @RequestLog(title = "更新用户", type = RequestType.UPDATE, exclude = {"password"})
    @PreAuthorize("@pms.hasAuthority('sys:user:update')")
    @PutMapping("/update")
    public R update(@RequestBody @Validated UserUpdateDTO userUpdateDTO) {
        // 登录密码
        String password = userUpdateDTO.getPassword();
        if (StringUtils.isNotEmpty(password)) {
            // 加密盐
            String salt = RandomUtil.randomString(10);
            userUpdateDTO.setSalt(salt);
            // 密码基于SpringSecurity加密处理
            userUpdateDTO.setPassword(SecurityUtils.encryptPassword(salt + password));
        } else {
            userUpdateDTO.setPassword(null);
        }
        return userService.update(userUpdateDTO);
    }

    /**
     * 删除用户
     *
     * @param id 记录ID
     * @return 返回结果
     */
    @Operation(summary = "删除用户", description = "删除用户")
    @RequestLog(title = "删除用户", type = RequestType.DELETE)
    @PreAuthorize("@pms.hasAuthority('sys:user:delete')")
    @DeleteMapping("/delete/{id}")
    public R delete(@PathVariable Integer id) {
        List<Integer> idList = Collections.singletonList(id);
        return userService.delete(idList);
    }

    /**
     * 批量删除用户
     *
     * @param idList 记录ID
     * @return 返回结果
     */
    @Operation(summary = "批量删除用户", description = "批量删除用户")
    @RequestLog(title = "批量删除用户", type = RequestType.BATCH_DELETE)
    @PreAuthorize("@pms.hasAuthority('sys:user:batchDelete')")
    @DeleteMapping("/batchDelete")
    public R batchDelete(@RequestBody @Validated List<Integer> idList) {
        return userService.delete(idList);
    }

    /**
     * 重置用户密码
     *
     * @param userResetPwdDTO 参数
     * @return 返回结果
     */
    @Operation(summary = "重置用户密码", description = "重置用户密码")
    @RequestLog(title = "重置用户密码", type = RequestType.RESET_PWD)
    @PreAuthorize("@pms.hasAuthority('sys:user:resetPwd')")
    @PutMapping("/resetPwd")
    public R resetPwd(@RequestBody @Validated UserResetPwdDTO userResetPwdDTO) {
        // 加密盐
        String salt = RandomUtil.randomString(10);
        userResetPwdDTO.setSalt(salt);
        // 获取租户默认密码参数
        String userPassword = ParamResolver.getParamValue("USER_DEFAULT_PASSWORD", "123456");
        // 设置密码
        userResetPwdDTO.setPassword(SecurityUtils.encryptPassword(salt + userPassword));
        return userService.resetPwd(userResetPwdDTO);
    }

    /**
     * 导入用户
     *
     * @param userExcelVOList 导入Excel
     * @return 返回结果
     */
    @Operation(summary = "导入用户", description = "导入用户")
    @RequestLog(title = "导入用户", type = RequestType.IMPORT)
    @PreAuthorize("@pms.hasAuthority('sys:user:import')")
    @PostMapping("/import")
    public R importExcel(@RequestExcel List<UserExcelVO> userExcelVOList) {
        return userService.importExcel(userExcelVOList);
    }

    /**
     * 导出用户
     *
     * @return 返回结果
     */
    @Operation(summary = "导出用户", description = "导出用户")
    @RequestLog(title = "导出用户", type = RequestType.EXPORT)
    @PreAuthorize("@pms.hasAuthority('sys:user:export')")
    @ResponseExcel(name = "用户信息", sheetName = "用户信息")
    @GetMapping("/export")
    public List<UserExcelVO> exportExcel() {
        return userService.exportExcel();
    }

    /**
     * 生成文档
     * 特别备注:此处文档生成仅为提供以编码的方式生成PDF文件案例参考,不涉及任何业务;
     * 以此举一反三,理解逻辑后可以根据实际业务生成任何你所需要的文档,包括动态追加写入数据、图片等等;
     *
     * @param userId 用户ID
     * @return 返回结果
     * @throws IOException       异常处理
     * @throws DocumentException 异常处理
     */
    @Operation(summary = "生成文档", description = "生成文档")
    @RequestLog(title = "生成文档", type = RequestType.OTHER)
    @PreAuthorize("@pms.hasAuthority('sys:user:document')")
    @GetMapping("/document/{userId}")
    public R generateDocument(@PathVariable("userId") Integer userId) throws IOException, DocumentException {
        return userService.generateDocument(userId);
    }

}

常用权限节点控制实现:

// 原生写法,需完全匹配
@PreAuthorize("hasAuthority('sys:demo:index')")
// 满足其一即可访问资源
@PreAuthorize("hasAnyAuthority('test','admin','sys:demo:index')")
// 原生写法,需完全匹配
@PreAuthorize("hasRole('admin')")
// 原生写法,满足其一即可访问资源
@PreAuthorize("hasAnyRole('test','ADMIN')")
// 自定义写法,需完全匹配
@PreAuthorize("@pms.hasAuthority('sys:demo:index')")

总结

通过以上方案,你可以在项目中实现基于 @PreAuthorize 的自定义权限节点控制,满足复杂的权限管理需求。

1. 核心功能:
    通过 @PreAuthorize 注解实现方法级权限控制。
    结合自定义权限校验服务,实现灵活的权限节点控制。
2. 优点:
    细粒度的权限控制,适合复杂业务场景。
    与 Spring Security 无缝集成,配置简单。
3. 适用场景:
    需要基于权限节点控制资源访问的应用。
    需要动态权限管理的系统。
4. 注意事项:
    权限节点设计应清晰合理,避免过于复杂。
    生产环境中建议将权限数据存储在数据库中,并结合缓存提高性能。

小蚂蚁云团队 · 提供技术支持

小蚂蚁云 新品首发
新品首发,限时特惠,抢购从速! 全场95折
赋能开发者,助理企业发展,提供全方位数据中台解决方案。
获取官方授权