当前位置:网站首页>7. Data permission annotation

7. Data permission annotation

2022-07-06 20:34:00 Little happy

Data access , An annotation is done !

Source code :https://github.com/1040580896/data_scope

Be careful

Place holder

sql.append(String.format(" OR %s.dept_id in(select rd.dept_id from sys_role_dept rd where rd.role_id=%d)", dataScope.deptAlias(), role.getRoleId()));

mysql The function in

mysql in find_in_set() Use of functions

https://www.cnblogs.com/xiaoxi/p/5889486.html

Set up the project

Will introduce Spring Security、Aop、mybatis-plus、mybatis Such dependence

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--aop Dependence -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Mistakes made

No import mybatis rely on , It throws an exception

Invalid bound statement (not found):

Code generator

@SpringBootTest
class DataScopeApplicationTests {
    

    @Test
    void contextLoads() {
    
        FastAutoGenerator.create("jdbc:mysql:///tianchin?serverTimezone=Asia/Shanghai", "root", "th123456")
                .globalConfig(builder -> {
    
                    builder.author("th") //  Set the author 
                            // .enableSwagger() //  Turn on  swagger  Pattern 
                            .fileOverride() //  Overwrite generated file 
                            .outputDir("/Users/xiaokaixin/Desktop/tianchi/demo/data_scope/src/main/java"); //  Specify output directory 
                })
                .packageConfig(builder -> {
    
                    builder.parent("com.th.ds") //  Set the parent package name 
                            // .moduleName("system") //  Set the parent package module name 
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/xiaokaixin/Desktop/tianchi/demo/data_scope/src/main/resources/mapper/")); //  Set up mapperXml Path is generated 
                })
                .strategyConfig(builder -> {
    
                    builder.addInclude("sys_dept","sys_role","sys_user") //  Set the table name to be generated 
                            .addTablePrefix("sys_"); //  Set filter table prefix 
                })
                .templateEngine(new FreemarkerTemplateEngine()) //  Use Freemarker Engine templates , The default is Velocity Engine templates 
                .execute();

    }

}

introduce SpringSecurity The details of the

user class

Rewrite his method

[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-HI5QLkK2-1653987529020)(img/image-20220531090230607.png)]

image-20220531090259005

[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-2hBDROvs-1653987529022)(img/image-20220531090317844.png)]

UserServiceImpl

[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-7KKKxoG0-1653987529023)(img/image-20220531090506537.png)]

1. Thought analysis

First of all, let's take a look at the idea of permission implementation here .

@DataScope The content of annotation processing is called Data permission , That is, what data can you access after logging in . The traditional approach is to authenticate users according to the current id Or the role or authority and other information to query , But this method is more troublesome and troublesome , Every query needs to write a lot SQL, And these SQL There are a lot of similarities in , So we hope to deal with it in a unified way , Then it leads to @DataScope annotation .

stay RuoYi-Vue In scaffolding , Divide the data permissions of users into five categories , They are as follows :

  • 1: This indicates all data permissions , That is, this user can access all data after logging in , Generally speaking, only super administrators have this permission .
  • 2: This indicates user-defined data permissions , User defined data permission means finding out which department data the user can operate according to the user's role , Take this as the basis for data query .
  • 3: This indicates the data permission of the Department , This simple , This user can only query the data of this department .
  • 4: This indicates the data authority of the Department and below , This means that users can query the data of this department and its subordinate departments .
  • 5: This means that this user can only view their own data .

stay TienChin In this project , Data permission is basically based on the design of this scaffold , We just need to understand the implementation idea here , In the future, you can customize the data permission annotation at will .

2. Table structure analysis

Clear your mind , Let's look at the table structure .

Here are the following tables :

  • sys_user: User table
  • sys_role: Role table
  • sys_dept: Departmental table
  • sys_user_role: User role association table
  • sys_role_dept: Role Department association table

There are some details in these tables. Let me sort them out with you . One by one .

There is one in the user table dept_id Indicates the Department this user belongs to id, A user belongs to a department .

There is a field in the role table called data_scope, Indicates the data permission corresponding to this role , The value is 1-5, Meaning is the meaning listed above , This is very important .

When designing the Department table , There is one ancestors Field , Through this field, you can easily query the sub departments of a department .

There is nothing to say about the last two correlation tables .

Okay , These are all analyzed , Let's take a look at the specific implementation .

3. Concrete realization

3.1 @DataScope

Let's first look at the definition of Data permission annotation :

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    
    /** *  Alias of department table  */
    public String deptAlias() default "";

    /** *  Alias of user table  */
    public String userAlias() default "";
}

There are two properties in this annotation , One is deptAlias and userAlias. Because the core idea of Data permission implementation is to implement SQL Dynamically append query conditions , Then dynamically added SQL We must consider the original SQL Department table alias and user table alias when defining . These two attributes are used to do this .

So the partners may also see , This @DataScope It's not quite the same as our previous annotations , Other annotations customized in the past are less coupled with the business , This @DataScope The coupling degree with the business is relatively high , You have to look at your business SQL What is the alias of department table and user table in , Then configure it on this annotation .

therefore ,@DataScope Annotation is not a particularly flexible annotation , Let's take a learning attitude and understand his realization method .

3.2 Section analysis

The annotation is defined , Next is the section analysis .

@Aspect
@Component
public class DataScopeAspect {
    

    public static final String DATA_SCOPE_ALL = "1";
    public static final String DATA_SCOPE_CUSTOM = "2";
    public static final String DATA_SCOPE_DEPT = "3";
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
    public static final String DATA_SCOPE_SELF = "5";


    public static final String DATA_SCOPE = "data_scope";

    @Before("@annotation(dataScope)")
    public void doBefore(JoinPoint jp, DataScope dataScope) {
    
        clearDataScope(jp);
        //1、 Get the current login user information 
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (user.getUserId() == 1L) {
    
            // Description is super administrator , No permission filtering 
            return;
        }
        StringBuilder sql = new StringBuilder();
        List<Role> roles = user.getRoles();
        //select * from sys_dept d where d.del_flag='0' and (xxx OR xxx OR xxx)
        //d.dept_id in(select rd.dept_id from sys_user_role ur,sys_role_dept rd where ur.user_id=2 and ur.role_id=rd.role_id)  Representing one  xxx
        for (Role role : roles) {
    
            // Get the data permission corresponding to the role 
            String ds = role.getDataScope();
            if (DATA_SCOPE_ALL.equals(ds)) {
    
                // If users can view all data permissions , There's nothing to do here 
                return;
            } else if (DATA_SCOPE_CUSTOM.equals(ds)) {
    
                // Customized data permissions , So based on   User role to find the Department  id
                //todo  Place holder 
                sql.append(String.format(" OR %s.dept_id in(select rd.dept_id from sys_role_dept rd where rd.role_id=%d)", dataScope.deptAlias(), role.getRoleId()));
            } else if (DATA_SCOPE_DEPT.equals(ds)) {
    
                sql.append(String.format(" OR %s.dept_id=%d", dataScope.deptAlias(), user.getDeptId()));
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(ds)) {
    
                //todo find_in_set
                sql.append(String.format(" OR %s.dept_id in(select dept_id from sys_dept where dept_id=%d or find_in_set(%d,`ancestors`))", dataScope.deptAlias(), user.getDeptId(), user.getDeptId()));
            } else if (DATA_SCOPE_SELF.equals(ds)) {
    
                String s = dataScope.userAlias();
                if ("".equals(s)) {
    
                    // Data permission is limited to myself 
                    sql.append(" OR 1=0");
                } else {
    
                    sql.append(String.format(" OR %s.user_id=%d", dataScope.userAlias(), user.getUserId()));
                }
            }
        }

        // and( xxx or xxx or xxx)
        Object arg = jp.getArgs()[0];
        if (arg != null && arg instanceof BaseEntity) {
    
            BaseEntity baseEntity = (BaseEntity) arg;
            baseEntity.getParams().put(DATA_SCOPE, " AND ("+sql.substring(4)+")");
        }

    }

    /** *  If  params  There are already parameters in , Then delete the , prevent  sql  Inject  * * @param jp */
    private void clearDataScope(JoinPoint jp) {
    
        Object arg = jp.getArgs()[0];
        if (arg != null && arg instanceof BaseEntity) {
    
            BaseEntity baseEntity = (BaseEntity) arg;
            baseEntity.getParams().put(DATA_SCOPE, "");
        }
    }

}
  1. First of all, five different data permission types are defined , We have already introduced these five types , I won't go into that here .
  2. Next doBefore The method is a pre notification . because @DataScope The annotation is added to service Layer method , Therefore, pre notification is used here , Supplement for the implementation of the method SQL Parameters , This is the way of thinking : Annotated with data permission service The parameter of the layer method must be an object , And this object must inherit from BaseEntity,BaseEntity One of them Map Type of params attribute , If we need to service The implementation of layer method adds SQL, Then put the supplementary content in this params variable , Supplementary key That's what I said dataScope,value Is a SQL. stay doBefore Method first clearDataScope To clear params What is already in the variable , prevent SQL Inject ( Because of this params The content of can also be transmitted from the front end ); And then execute handleDataScope Method to filter data permissions .
  3. stay handleDataScope In the method , Mainly to query the current user , And then call dataScopeFilter Method to filter data , This is the core method of filtering .
  4. Because a user may have multiple roles , So in dataScopeFilter Method must first traverse the role , Different roles have different data permissions , These different data permissions pass through OR Connected to a , Finally generated supplement SQL The format is similar to this AND(xxx OR xxx OR xxx) such , every last xxx Represents the filter conditions generated by a role .
  5. The next step is to generate supplements according to different data permissions SQL 了 : If the data permission is 1, Then it generates SQL It's empty , I.e. inquiry SQL Do not add restrictions ; If the data permission is 2, Indicates custom data permission , At this time, the user's department is queried according to the user's role , Generate query restricted SQL; If the data permission is 3, It means that the user's Data permission is limited to his own department , Then pick out the Department to which the user belongs as the query limit ; If the data permission is 4, Indicates that the user's authority is his own department and his sub department , Then the user's department and its sub departments will be selected as the restricted query criteria ; If the data permission is 5, It means that the user's Data permission is limited to himself , That is, you can only view your own data , Then use the user's own id As the limiting condition of query . Last , And then put the generated SQL Deal with it a little , become AND(xxx OR xxx OR xxx) Format , This is a little bit easier , Is string interception + String splicing .

4. case analysis

Let's take a look @DataScope Annotate three specific applications, and you will understand .

stay RuoYi-Vue In scaffolding , This annotation mainly has three usage scenarios :

  1. Inquiry Department .
  2. Query roles .
  3. Query the user .

Suppose I now take ry This user logs in , The role of this user is ordinary , The data permissions of ordinary roles are 2, That is, user-defined data permissions , Let's take a look at how this user queries data .

Let's look at .

4.1 Inquiry Department

First, the method of querying the Department is located in org.javaboy.tienchin.system.service.impl.SysDeptServiceImpl#selectDeptList Location , The specific method is as follows :

@Override
@DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept) {
    
    return deptMapper.selectDeptList(dept);
}

This parameter SysDept Inherited from BaseEntity, and BaseEntity There is one of them. params attribute , We have already introduced this before , I won't repeat .

Let's take a look at this selectDeptList Method corresponding to SQL:

<sql id="selectDeptVo">
    select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time 
    from sys_dept d
</sql>
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
    <include refid="selectDeptVo"/>
    where d.del_flag = '0'
 <if test="deptId != null and deptId != 0">
  AND dept_id = #{deptId}
 </if>
    <if test="parentId != null and parentId != 0">
  AND parent_id = #{parentId}
 </if>
 <if test="deptName != null and deptName != ''">
  AND dept_name like concat('%', #{deptName}, '%')
 </if>
 <if test="status != null and status != ''">
  AND status = #{status}
 </if>
 <!--  Data range filtering  -->
 ${params.dataScope}
 order by d.parent_id, d.order_num
</select>

You can see , stay SQL There is a sentence at the end of ${params.dataScope}, Is to put in DataScopeAspect Spliced in the section SQL Add in .

So this SQL The final form is similar to the following :

SELECT
	d.dept_id,
	d.parent_id,
	d.ancestors,
	d.dept_name,
	d.order_num,
	d.leader,
	d.phone,
	d.email,
	d.STATUS,
	d.del_flag,
	d.create_by,
	d.create_time 
FROM
	sys_dept d 
WHERE
	d.del_flag = '0' 
	AND ( d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 2 ) ) 
ORDER BY
	d.parent_id,
	d.order_num

** You can see , The last SQL after , Data filtering is realized ( Here is to filter according to user-defined data permissions ).** Then here is another detail , front SQL In defining , What is the table alias used , We are @DataScope What is the alias specified in .

4.2 Query roles

First, the method of querying roles is located in org.javaboy.tienchin.system.service.impl.SysRoleServiceImpl#selectRoleList Location , The specific method is as follows :

@Override
@DataScope(deptAlias = "d")
public List<SysRole> selectRoleList(SysRole role) {
    
    return roleMapper.selectRoleList(role);
}

This parameter SysRole Inherited from BaseEntity, and BaseEntity There is one of them. params attribute , We have already introduced this before , I won't repeat .

Let's take a look at this selectRoleList Method corresponding to SQL:

<sql id="selectRoleVo">
    select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly,
        r.status, r.del_flag, r.create_time, r.remark 
    from sys_role r
        left join sys_user_role ur on ur.role_id = r.role_id
        left join sys_user u on u.user_id = ur.user_id
        left join sys_dept d on u.dept_id = d.dept_id
</sql>
<select id="selectRoleList" parameterType="SysRole" resultMap="SysRoleResult">
 <include refid="selectRoleVo"/>
 where r.del_flag = '0'
 <if test="roleId != null and roleId != 0">
  AND r.role_id = #{roleId}
 </if>
 <if test="roleName != null and roleName != ''">
  AND r.role_name like concat('%', #{roleName}, '%')
 </if>
 <if test="status != null and status != ''">
  AND r.status = #{status}
 </if>
 <if test="roleKey != null and roleKey != ''">
  AND r.role_key like concat('%', #{roleKey}, '%')
 </if>
 <if test="params.beginTime != null and params.beginTime != ''"><!--  Start time Retrieval  -->
  and date_format(r.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
 </if>
 <if test="params.endTime != null and params.endTime != ''"><!--  End time Retrieval  -->
  and date_format(r.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
 </if>
 <!--  Data range filtering  -->
 ${params.dataScope}
 order by r.role_sort
</select>

You can see , stay SQL There is a sentence at the end of ${params.dataScope}, Is to put in DataScopeAspect Spliced in the section SQL Add in .

So this SQL The final form is similar to the following :

SELECT DISTINCT
	r.role_id,
	r.role_name,
	r.role_key,
	r.role_sort,
	r.data_scope,
	r.menu_check_strictly,
	r.dept_check_strictly,
	r.STATUS,
	r.del_flag,
	r.create_time,
	r.remark 
FROM
	sys_role r
	LEFT JOIN sys_user_role ur ON ur.role_id = r.role_id
	LEFT JOIN sys_user u ON u.user_id = ur.user_id
	LEFT JOIN sys_dept d ON u.dept_id = d.dept_id 
WHERE
	r.del_flag = '0' 
	AND ( d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 2 ) ) 
ORDER BY
	r.role_sort 
	LIMIT ?

You can see , The last SQL after , Data filtering is realized ( Here is to filter according to user-defined data permissions ).

The logic of filtering is based on the Department the user belongs to id Find the user id, Then according to the user id Find the corresponding role id, Finally, return the queried roles .

In fact, I think the query department and query users conduct data filtering , This is easy to understand , What departments can the currently logged in user operate , Which users can operate , These are easy to understand , Which roles can be operated and how to understand ? Especially the above query SQL It's a big circle , Some of them may say , There is one in the system sys_role_dept Do you have a watch ? This table is related to role information and department information , Take the user's Department directly id Come to this table to query the roles that users can operate id No, just go ? That's not the case ! Here I think you should understand it in this way : Department to which the user belongs this is the Department to which the user belongs , The Department that users can operate is the Department that can operate , There is no necessary connection between the two .sys_user In the table dept_id The field represents the Department to which this user belongs id, and sys_role_dept The table describes which departments a role can operate , It's different , Straighten this out , above SQL It's easy to understand .

4.3 Query the user

Finally, let's look at the query user .

The method of querying users is org.javaboy.tienchin.system.service.impl.SysUserServiceImpl#selectUserList Location , Corresponding SQL as follows :

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
    select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
 left join sys_dept d on u.dept_id = d.dept_id
 where u.del_flag = '0'
 <if test="userId != null and userId != 0">
  AND u.user_id = #{userId}
 </if>
 <if test="userName != null and userName != ''">
  AND u.user_name like concat('%', #{userName}, '%')
 </if>
 <if test="status != null and status != ''">
  AND u.status = #{status}
 </if>
 <if test="phonenumber != null and phonenumber != ''">
  AND u.phonenumber like concat('%', #{phonenumber}, '%')
 </if>
 <if test="params.beginTime != null and params.beginTime != ''"><!--  Start time Retrieval  -->
  AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
 </if>
 <if test="params.endTime != null and params.endTime != ''"><!--  End time Retrieval  -->
  AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
 </if>
 <if test="deptId != null and deptId != 0">
  AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
 </if>
 <!--  Data range filtering  -->
 ${params.dataScope}
</select>

This is easier , Query the user according to the department or the current user . Finally generated SQL Similar to the following ( This is the custom data permission , That is to find users according to their departments ):

SELECT
	u.user_id,
	u.dept_id,
	u.nick_name,
	u.user_name,
	u.email,
	u.avatar,
	u.phonenumber,
	u.PASSWORD,
	u.sex,
	u.STATUS,
	u.del_flag,
	u.login_ip,
	u.login_date,
	u.create_by,
	u.create_time,
	u.remark,
	d.dept_name,
	d.leader 
FROM
	sys_user u
	LEFT JOIN sys_dept d ON u.dept_id = d.dept_id 
WHERE
	u.del_flag = '0' 
	AND ( d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 2 ) ) 
	LIMIT ?
原网站

版权声明
本文为[Little happy]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/187/202207061228358478.html