芋道框架相关

发布于:2025-07-04 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

1:项目简介,功能列表,技术选型

2:从零开始(基于Cloud微服务版本)

2.4:项目结构讲解

3:业务模块

3.1:依赖管理

3.2:框架封装管理(包含技术组件和业务组件两大类)

3.3:业务模块封装

3.4:开发一个新功能(基于代码生成器)

4:技术模块

4.0:spring security

4.0.1:AuthorizeRequestsCustomizer(最重要的就是可以指定每个服务都可以自定义security规则,最终由总配置文件合并加载)

4.0.2:SecurityProperties(指定配置文件的值)

4.1:功能权限

权限注解

4.2:数据权限

4.2.1:实现原理

4.2.2: 基于部门的数据权限

1 后台配置

2 字段配置

动态拼接 SQL

3:@DataPermission 注解

4. 自定义的数据权限规则

5. 如何忽略数据权限

4.2.3:

权限控制的流程

4.3:用户体系

1. 表结构

2. 如何获取当前登录的用户?

2.1 获取当前用户信息

#2.2 获取当前用户编号(最常用)

#2.3 获取当前用户昵称

注意,仅适合 AdminUser 管理员用户!

2.4 获取当前用户部门

2.5 获取更多信息

3. 账号密码登录

3.1 管理后台的实现

​编辑

3.2 用户 App 的实现

4. 手机验证码登录

#4.1 管理后台的实现

4.2 用户 App 的实现

5. 三方登录

#5.1 管理后台的实现

#5.2 用户 App 的实现

6. 注册

#6.1 管理后台的实现

6.2 用户 App 的实现

7. 用户登出

4.4:三方登录

4.5:OAuth2.0(SS0单点登录)

4.6:websocket实时通信

1. 功能简介

1.1 Token 身份认证

1.2 Session 会话管理

1.3 Message 消息格式

1.4 Message 消息接收

1.5 Message 消息推送

2. 使用方案

2.1 方案一:纯 WebSocket

2.1.1 后端代码

2.1.2 前端代码

2.2 方案二:WebSocket + HTTP

2.2.1 后端代码

2.2.2 前端代码

2.3 如何选择?

3. 实战案例

4.7:异常处理

4.8:参数校验

4.9:分页实现

4.10:文件存储

4.11:日志相关

4.12:Mybatis

4.13:多数据源

4.14:缓存

4.15:异步任务

4.16:分布式锁

4.17:幂等性

418:限流

4.19:HTTP接口签名

4.20:验证码

4.21:工具类

5:线上运维

6:工作流手册

6.1:集成BPM

6.2:审批接入(流程表单-简单场景)

1. 业务接入(流程表单)

#1.1 第一步:定义流程

2. 第二步:发起流程

2.1 发起流程

2.2 查看流程

3. 第三步:审批流程

3.1 部门领导审批

3.2 HR 审批

4. 菜单【流程表单】

1 表结构

6.3:审批接入(业务表单-复杂场景)

1. 业务接入(业务表单)

1.0 第零步:业务开发

1.0.1 新建业务表

1.0.2 【后端】实现业务逻辑

1.0.3 【前端】实现业务逻辑

1.0.4 【实现】实现审批结果的监听

2.1 第一步:定义流程

1.1.1 新建流程

1.1.2:发起流程

1.1.3:审批流程

6.4:业务表单是如何集成的?

6.5:流程设计器(BPMN)

1. 流程模型

1.1 表结构

1.2 流程设计器

1.3 任务(表单)

1.3.1 表单配置

1.3.2 表单效果

1.4 任务(审批人)

1.5 多实例(会签配置)

1.6 执行监听器

1.7 任务监听器

2. 流程定义

2.1 表结构

2.2 流程定义列表(可发起流程)

6.6:选择审批人、发起人自选

1. 审批人配置

2. 选择审批人

3. 自定义 BpmTaskCandidateStrategy 策略

4. 发起人自选

4.1 【流程表单】示例

4.2 【业务表单】示例

4.3 【流程表单】实现原理

4.4 【业务表单】实现原理

5. 流程表达式

6.7:会签、或签、依次审批

1. 多人审批

1. 会签(并行会签)

2. 或签(并行或签)

3. 依次审批

2. 实现原理

2.1 并行 BpmParallelMultiInstanceBehavior

2.2 顺序 BpmSequentialMultiInstanceBehavior

6.8:流程发起、取消、重新发起

1. 发起流程

1.1 表结构

1.2 流程状态

1.3 具体实现

2. 我的流程

2.1 表结构

2.2 具体实现

3 取消流程

4 重新发起流程

5. 流程实例

6.9:审批通过、不通过、驳回

1. 待办任务

1.1 表结构

1.2 任务状态

1.3 审批通过

1.4 审批不通过

1.5 驳回

2. 已办任务

2.1 表结构

3. 流程任务

4. 更多功能

6.10:审批加签、减签

1. 向前加签

1.1 发起向前加签

1.1.1 操作步骤

1.1.2 后端实现

1.2 审批向前加签子任务

1.2.1 操作步骤

1.2.2 后端实现

2. 向后加签

2.1 发起向后加签

2.1.1 操作步骤

2.1.2 审批向后加签子任务

2.2 审批当前(原)任务

2.2.1 操作步骤

2.2.2 后端实现

2.3 审批向后加签子任务

2.3.1 操作步骤

2.3.2 后端实现

3. 减签

3.1 操作步骤

3.2 后端实现

6.11:审批转办、委派、抄送

1. 转办、委派

1.1 转办

1.2 委派

2. 抄送

2.1 表结构

2.2 发起抄送

2.3 查看抄送

6.12:执行监听器、任务监听器

1. 执行监听器

1.1 Java 类监听器

1.2 委托表达式监听器

1.3 Spring 表达式监听器

2. 任务监听器

2.1 Java 类监听器

2.2 委托表达式监听器

2.3 Spring 表达式监听器

3. 流程监听器的模版

3.1 使用场景

3.2 表结构

6.13:流程表达式

1. 流程表达式

1.1 BpmTaskAssignStartUserExpression

1.2 BpmTaskAssignLeaderExpression

2. 流程表达式的模版

2.1 使用场景

2.2 表结构

6.14:流程审批通知

7:商城手册


1:项目简介,功能列表,技术选型

2:从零开始(基于Cloud微服务版本)

2.4:项目结构讲解

3:业务模块

3.1:依赖管理

因为所有的依赖管理都是基于yudao-dependencies的,而整个工程的父模块依赖了dependencies,所以其他模块都会通过父模块间接引入depencies的依赖。

3.2:框架封装管理(包含技术组件和业务组件两大类)

3.3:业务模块封装

3.4:开发一个新功能(基于代码生成器)

先启动前端。暂时是哦那个的node版本是18的,16的不支持。

4:技术模块

4.0:spring security

4.0.1:AuthorizeRequestsCustomizer(最重要的就是可以指定每个服务都可以自定义security规则,最终由总配置文件合并加载)

package cn.iocoder.yudao.framework.security.config;

import cn.iocoder.yudao.framework.web.config.WebProperties;
import jakarta.annotation.Resource;
import org.springframework.core.Ordered;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;

/**
 * 自定义的 URL 的安全配置
 * 目的:每个 Maven Module 可以自定义规则!
 *
 * @author 芋道源码
 */
public abstract class AuthorizeRequestsCustomizer
        implements Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>, Ordered {

    @Resource
    private WebProperties webProperties;

    protected String buildAdminApi(String url) {
        return webProperties.getAdminApi().getPrefix() + url;
    }

    protected String buildAppApi(String url) {
        return webProperties.getAppApi().getPrefix() + url;
    }

    @Override
    public int getOrder() {
        return 0;
    }

}

这个类实现了Customizer接口泛型参数是AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry,说明它用于定制Spring Security的授权请求配置。Customizer在Spring Security中用来提供自定义配置的方式,通常通过实现接口中的方法来实现具体的配置逻辑。

这个类还实现了Ordered接口,所以必须实现getOrder方法,这里返回0。这可能影响多个Customizer的执行顺序,但具体要看Spring Security的处理方式。

4.0.2:SecurityProperties(指定配置文件的值)

这个文件可以在yml中配置,也可以使用其中默认的值。其中yml中指定了值的话,优先级是要高于文件中的默认值

package cn.iocoder.yudao.framework.security.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;

@ConfigurationProperties(prefix = "yudao.security")
@Validated
@Data
public class SecurityProperties {

    /**
     * HTTP 请求时,访问令牌的请求 Header
     */
    @NotEmpty(message = "Token Header 不能为空")
    private String tokenHeader = "Authorization";
    /**
     * HTTP 请求时,访问令牌的请求参数
     *
     * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
     */
    @NotEmpty(message = "Token Parameter 不能为空")
    private String tokenParameter = "token";

    /**
     * mock 模式的开关
     */
    @NotNull(message = "mock 模式的开关不能为空")
    private Boolean mockEnable = false;
    /**
     * mock 模式的密钥
     * 一定要配置密钥,保证安全性
     */
    @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
    private String mockSecret = "test";

    /**
     * 免登录的 URL 列表
     */
    private List<String> permitAllUrls = Collections.emptyList();

    /**
     * PasswordEncoder 加密复杂度,越高开销越大
     */
    private Integer passwordEncoderLength = 4;
}
4.0.3:YudaoSecurityAutoConfiguration这个类有以下作用:
  • 自动配置 Spring Security 组件:通过 @AutoConfiguration 注解,该类会在 Spring Boot 启动时自动加载,配置 Spring Security 所需的核心组件。

  • 优先级控制:通过 @AutoConfigureOrder(-1) 注解,确保该配置类在 Spring Security 默认配置之前加载,避免冲突。(必须在YudaoWebSecurityConfigurerAdapter 之前被加载,否则启动会报错)

  • 自定义安全配置:提供自定义的安全组件(如认证过滤器、加密器、异常处理器等),以满足项目的特定需求。

核心配置项:

以下是该类中定义的核心配置项及其作用:

(1)认证失败处理类

(2)权限不足处理类

(3)密码加密器

(4)Token 认证过滤器

(5)安全框架服务

(6)Security 上下文策略

package cn.iocoder.yudao.framework.security.config;

import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

/**
 * Spring Security 自动配置类,主要用于相关组件的配置
 *
 * 注意,不能和 {@link YudaoWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。
 * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。
 *
 * @author 芋道源码
 */
@AutoConfiguration
@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
@EnableConfigurationProperties(SecurityProperties.class)
public class YudaoSecurityAutoConfiguration {

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 认证失败处理类 Bean
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPointImpl();
    }

    /**
     * 权限不够处理器 Bean
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new AccessDeniedHandlerImpl();
    }

    /**
     * Spring Security 加密器
     * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器
     *
     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength());
    }

    /**
     * Token 认证过滤器 Bean
     */
    @Bean
    public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
                                                               OAuth2TokenApi oauth2TokenApi) {
        return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
    }

    @Bean("ss") // 使用 Spring Security 的缩写,方便使用
    public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
        return new SecurityFrameworkServiceImpl(permissionApi);
    }

    /**
     * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
     * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
     */
    @Bean
    public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
        methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
        return methodInvokingFactoryBean;
    }

}
其中TransmittableThreadLocalSecurityContextHolderStrategy是配置的security上下文内容:
  • Spring Security 默认使用 ThreadLocal 存储 SecurityContext(用户认证信息),但 ThreadLocal 在异步线程中无法自动传递,导致异步任务无法获取当前用户身份。

  • 解决方案
    使用 TransmittableThreadLocal(TTL) 替代默认的 ThreadLocal,确保 SecurityContext 能在父子线程间(尤其是线程池场景)正确传递。

import com.alibaba.ttl.TransmittableThreadLocal;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.util.Assert;

/**
 * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
 * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
 *
 */
public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

    /**
     * 使用 TransmittableThreadLocal 作为上下文
     */
    private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>();

    @Override
    public void clearContext() {
        CONTEXT_HOLDER.remove();
    }

    @Override
    public SecurityContext getContext() {
        SecurityContext ctx = CONTEXT_HOLDER.get();
        if (ctx == null) {
            ctx = createEmptyContext();
            CONTEXT_HOLDER.set(ctx);
        }
        return ctx;
    }

    @Override
    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        CONTEXT_HOLDER.set(context);
    }

    @Override
    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }

}
4.0.4:YudaoSecurityRpcAutoConfiguration 

这个类 YudaoSecurityRpcAutoConfiguration 是一个 Spring Boot 自动配置类,主要用于配置与 Feign 客户端 相关的安全组件。它的核心作用是为应用程序在远程调用(RPC)时提供安全支持,确保在 Feign 调用中能够正确传递用户身份信息(如登录用户信息)。

package com.oceania.framework.security.config;

import com.oceania.framework.security.core.rpc.LoginUserRequestInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

/**
 * Security 使用到 Feign 的配置项(比如feign调用的时候透传token)
 *
 */
@AutoConfiguration
public class SecurityRpcAutoConfiguration {

    @Bean
    public LoginUserRequestInterceptor loginUserRequestInterceptor() {
        return new LoginUserRequestInterceptor();
    }

}
package com.oceania.framework.security.core.rpc;

import com.oceania.framework.security.core.LoginUser;
import com.oceania.framework.security.core.util.SecurityFrameworkUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
 * LoginUser 的 RequestInterceptor 实现类:
 * Feign 请求时,将 Token信息 设置到 header 中,继续透传给被调用的服务
 *
 */
@Slf4j
public class LoginUserRequestInterceptor implements RequestInterceptor {

    @Override
    @SneakyThrows
    public void apply(RequestTemplate requestTemplate) {
        String token = SecurityFrameworkUtil.getToken();
        if (token == null) {
            return;
        }
        try {
            requestTemplate.header("Authorization", "Bearer " + token);
        } catch (Exception ex) {
            log.error("[apply] 无可用 Token,可能导致远程请求鉴权失败", ex);
            throw ex;
        }
    }

}

4.1:功能权限

系统采用 RBAC 权限模型,全称是 Role-Based Access Control 基于角色的访问控制。“用户<->角色<->菜单” 的授权模型。 在这种模型中,用户与角色、角色与菜单之间构成了多对多的关系。

安全框架使用的是 Spring Security (opens new window)+ Token 方案,整体流程如下图所示:

Token 存储在数据库中,对应 system_oauth2_access_token 访问令牌表的 id 字段。考虑到访问的性能,缓存在 Redis 的 oauth2_access_token:%s (opens new window)键中。

默认配置下,Token 有效期为 30 天,可通过 system_oauth2_client 表中 client_id = default 的记录进行自定义:

  • 修改 access_token_validity_seconds 字段,设置访问令牌的过期时间,默认 1800 秒 = 30 分钟
  • 修改 refresh_token_validity_seconds 字段,设置刷新令牌的过期时间,默认 2592000 秒 = 30 天
权限注解

@PreAuthorize (opens new window)是 Spring Security 内置的前置权限注解,添加在接口方法上,声明需要的权限,实现访问权限的控制。

① 基于【权限标识】的权限控制

权限标识,对应 system_menu 表的 permission 字段,推荐格式为 ${系统}:${模块}:${操作},例如说 system:admin:add 标识 system 服务的添加管理员。

@PreAuthenticated (opens new window)是项目自定义的认证注解,添加在接口方法上,声明登录的用户才允许访问。

主要使用场景是,针对用户 App 的 /app-app/** 的 RESTful API 接口,默认是无需登录的,通过 @PreAuthenticated 声明它需要进行登录。使用示例如下:

具体的代码实现,可见 PreAuthenticatedAspect (opens new window)类。

4.2:数据权限

数据权限,实现指定用户可以操作指定范围的数据。例如说,针对员工信息的数据权限:

用户 数据范围
普通员工 自己
部门领导 所属部门的所有员工
HR 小姐姐 整个公司的所有员工

上述的这个示例,使用硬编码是可以实现的,并且也非常简单。但是,在业务快速迭代的过程中,类似这种数据需求会越来越多,如果全部采用硬编码的方式,无疑会给我们带来非常大的开发与维护成本。

因此,项目提供 yudao-spring-boot-starter-biz-data-permission (opens new window)技术组件,只需要少量的编码,无需入侵到业务代码,即可实现数据权限。

4.2.1:实现原理

yudao-spring-boot-starter-biz-data-permission 技术组件的实现原理非常简单,每次对数据库操作时,他会自动拼接 WHERE data_column = ? 条件来进行数据的过滤。

例如说,查看员工信息的功能,对应 SQL 是 SELECT * FROM system_users,那么拼接后的 SQL 结果会是:

用户 数据范围 SQL
普通员工 自己 SELECT * FROM system_users WHERE id = 自己
部门领导 所属部门的所有员工 SELECT * FROM system_users WHERE dept_id = 自己的部门
HR 小姐姐 整个公司的所有员工 SELECT * FROM system_users 无需拼接

明白了实现原理之后,想要进一步加入理解,后续可以找时间 Debug 调试下 MyBatis Plus 的 DataPermissionInterceptor 类的这三个方法:

  • #processSelect(...) 方法:处理 SELECT 语句的 WHERE 条件。
  • #processUpdate(...) 方法:处理 UPDATE 语句的 WHERE 条件。
  • #processDelete(...) 方法:处理 DELETE 语句的 WHERE 条件。

主要还是基于 MyBatis Plus 的 数据权限插件 (opens new window)

具体的条件生成,可见项目的 DataPermissionRuleHandler 类。

4.2.2: 基于部门的数据权限

项目内置了基于部门的数据权限,支持 5 种数据范围:

  1. 全部数据权限:无数据权限的限制。
  2. 指定部门数据权限:根据实际需要,设置可操作的部门。
  3. 本部门数据权限:只能操作用户所在的部门。
  4. 本部门及以下数据权限:在【本部门数据权限】的基础上,额外可操作子部门。
  5. 仅本人数据权限:相对特殊,只能操作自己的数据。
1 后台配置

可通过管理后台的 [系统管理 -> 角色管理] 菜单,设置用户角色的数据权限。

2 字段配置

每个 Maven Module, 通过自定义 DeptDataPermissionRuleCustomizer (opens new window)Bean,配置哪些表的哪些字段,进行数据权限的过滤。

这段代码的核心作用是:

  1. 定义数据权限规则

    • 告诉系统哪些表需要根据 部门(Dept) 或 用户(User) 进行数据过滤。

  2. 配置规则自定义器

    • 通过 DeptDataPermissionRuleCustomizer 接口,动态添加数据权限规则。

以 yudao-module-system 模块来举例子,代码如下:

@Configuration(proxyBeanMethods = false)
public class DataPermissionConfiguration {

    @Bean
    public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() {
        return rule -> {
            // dept 基于部门的数据权限
            rule.addDeptColumn(AdminUserDO.class); // WHERE dept_id = ?
            rule.addDeptColumn(DeptDO.class, "id"); // WHERE id = ?
            
            // user 基于用户的数据权限
            rule.addUserColumn(AdminUserDO.class, "id"); // WHERE id = ?
//          rule.addUserColumn(OrderDO.class); // WHERE user_id = ?
        };
    }

}

注意,数据库的表字段必须添加:

  • 基于【部门】过滤数据权限的表,需要添加部门编号字段,例如说 dept_id 字段。
  • 基于【用户】过滤数据权限的表,需要添加部门用户字段,例如说 user_id 字段。
  • DeptDataPermissionRuleCustomizer

    • 一个函数式接口,用于自定义数据权限规则。

  • rule -> { ... }

    • Lambda 表达式,实现 了DeptDataPermissionRuleCustomizer 接口并添加自己的功能。

  • addDeptColumn

    • 添加基于 部门 的数据权限规则。

    • 参数说明:

      • AdminUserDO.class:表示对 AdminUserDO 表启用部门数据权限。

      • DeptDO.class, "id":表示对 DeptDO 表的 id 字段启用部门数据权限。

  • addUserColumn

    • 添加基于 用户 的数据权限规则。

    • 参数说明:

      • AdminUserDO.class, "id":表示对 AdminUserDO 表的 id 字段启用用户数据权限。

  • 动态拼接 SQL
  • 在查询数据时,系统会根据配置的规则动态拼接 SQL。

  • 比如:

    • 如果配置了 addDeptColumn(AdminUserDO.class),查询 AdminUserDO 表时会自动拼接 WHERE dept_id = 当前用户部门ID

    • 如果配置了 addUserColumn(AdminUserDO.class, "id"),查询时会自动拼接 WHERE id = 当前用户ID

3:@DataPermission 注解

使用示例如下,可见 UserProfileController (opens new window)类:

// UserProfileController.java

@GetMapping("/get")
@Operation(summary = "获得登录用户信息")
@DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。
public CommonResult<UserProfileRespVO> profile() {
    // .. 省略代码
    if (user.getDeptId() != null) {
        DeptDO dept = deptService.getDept(user.getDeptId());
        resp.setDept(UserConvert.INSTANCE.convert02(dept));
    }
    // .. 省略代码
}

② includeRules 属性,配置生效的 DataPermissionRule (opens new window)数据权限规则。例如说,项目里有 10 种 DataPermissionRule 规则,某个方法只想其中的 1 种生效,则可以使用该属性。

③ excludeRules 属性,配置排除的 DataPermissionRule (opens new window)数据权限规则。例如说,项目里有 10 种 DataPermissionRule 规则,某个方法不想其中的 1 种生效,则可以使用该属性。

4. 自定义的数据权限规则

如果想要自定义数据权限规则,只需要实现 DataPermissionRule (opens new window)数据权限规则接口,并声明成 Spring Bean 即可。需要实现的只有两个方法:

public interface DataPermissionRule {

    /**
     * 返回需要生效的表名数组
     * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
     *
     * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
     *
     * @return 表名数组
     */
    Set<String> getTableNames();

    /**
     * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
     *
     * @param tableName 表名
     * @param tableAlias 别名,可能为空
     * @return 过滤条件 Expression 表达式
     */
    Expression getExpression(String tableName, Alias tableAlias);

}
  • #getTableNames() 方法:哪些数据库表,需要使用该数据权限规则。
  • #getExpression(...) 方法:当操作这些数据库表,需要额外拼接怎么样的 WHERE 条件

下面,艿艿带你写个自定义数据权限规则的示例,它的数据权限规则是:

  • 针对 system_dict_type 表,它的创建人 creator 要是当前用户。
  • 针对 system_post 表,它的更新人 updater 要是当前用户。

具体实现代码如下:

package cn.iocoder.yudao.module.system.framework.datapermission;

import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import com.google.common.collect.Sets;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component // 声明为 Spring Bean,保证被 yudao-spring-boot-starter-biz-data-permission 组件扫描到
public class DemoDataPermissionRule implements DataPermissionRule {

    @Override
    public Set<String> getTableNames() {
        return Sets.newHashSet("system_dict_type", "system_post");
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        assert userId != null;
        switch (tableName) {
            case "system_dict_type":
                return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, "creator"), new LongValue(userId));
            case "system_post":
                return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, "updater"), new LongValue(userId));
            default: return null;
        }
    }

}

① 启动前端 + 后端项目。

② 访问 [系统管理 -> 字典管理] 菜单,查看 IDEA 控制台,可以看到 system_dict_type 表的查询自动拼接了 AND creator = 1 的查询条件。

② 访问 [系统管理 -> 岗位管理] 菜单,查看 IDEA 控制台,可以看到 system_post 表的查询自动拼接了 AND updater = 1 的查询条件。

5. 如何忽略数据权限

可以使用 DataPermissionUtils 的 #executeIgnore(...) 方法,设置忽略数据权限。

具体的案例,可以通过 IDEA 查找下项目里,哪些地方调用了这个方法噢!

    4.2.3:
    权限控制的流程
    1. 用户登录

      • 用户输入账号密码,系统验证通过后生成 Token。

      • Token 中包含用户ID和角色ID。

    2. 加载权限

      • 根据角色ID,从数据库加载用户的权限列表和数据范围。

    3. 权限校验

      • 用户访问某个功能时,系统检查该功能是否在权限列表中。

      • 如果没有权限,直接拒绝访问。

    4. 数据过滤

      • 用户查询数据时,系统根据数据范围动态拼接 SQL。

      • 返回过滤后的数据。

    4.3:用户体系

    系统提供了 2 种类型的用户,分别满足对应的管理后台、用户 App 场景。

    虽然是不同类型的用户,他们访问 RESTful API 接口时,都通过 Token 认证机制,具体可见 《开发指南 —— 功能权限》

    1. 表结构

    2 种类型的时候,采用不同数据库的表进行存储,管理员用户对应 system_users (opens new window)表,会员用户对应 member_user (opens new window)表。如下图所示:

    如果表需要关联多种类型的用户,例如说上述的 system_oauth2_access_token 访问令牌表,可以通过 user_type 字段进行区分。并且 user_type 对应 UserTypeEnum (opens new window)全局枚举,代码如下:

    2. 如何获取当前登录的用户?

    使用 SecurityFrameworkUtils (opens new window)提供的如下方法,可以获得当前登录用户的信息:

    2.1 获取当前用户信息
    public static LoginUser getLoginUser()
    
    #2.2 获取当前用户编号(最常用)
    public static Long getLoginUserId()
    
    #2.3 获取当前用户昵称
    public static LoginUser getLoginUserNickname()
    
    注意,仅适合 AdminUser 管理员用户!
    2.4 获取当前用户部门
    public static Long getLoginUserDeptId()
    

    注意,仅适合 AdminUser 管理员用户!

    2.5 获取更多信息

    ① 在 OAuth2TokenServiceImpl 的 #buildUserInfo(...) 方法中,补充读取更多的用户信息,例如说 mobilesex 等等。如下图所示:

    ② 在 SecurityFrameworkUtils 新增对应的 getXXX() 静态方法,参考如下图所示:

    3. 账号密码登录
    3.1 管理后台的实现

    使用 username 账号 + password 密码进行登录,由 AuthController (opens new window)提供 /admin-api/system/auth/login 接口。代码如下:

    @PostMapping("/login")
    @Operation(summary = "使用账号密码登录")
    public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
        String token = authService.login(reqVO, getClientIP(), getUserAgent());
        // 返回结果
        return success(AuthLoginRespVO.builder().token(token).build());
    }
    

    3.2 用户 App 的实现

    使用 mobile 手机 + password 密码进行登录,由 AppAuthController (opens new window)提供 /app-api/member/auth/login 接口。代码如下:

    @PostMapping("/login")
    @Operation(summary = "使用手机 + 密码登录")
    public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
        String token = authService.login(reqVO, getClientIP(), getUserAgent());
        // 返回结果
        return success(AppAuthLoginRespVO.builder().token(token).build());
    }
    
    4. 手机验证码登录
    #4.1 管理后台的实现

    ① 使用 mobile 手机号获得验证码,由 AuthController (opens new window)提供 /admin-api/system/auth/send-sms-code 接口。代码如下:

    @PostMapping("/send-sms-code")
    @Operation(summary = "发送手机验证码")
    public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AuthSendSmsReqVO reqVO) {
        authService.sendSmsCode(getLoginUserId(), reqVO);
        return success(true);
    }
    

    ② 使用