【Spring Security】安全框架学习(九)

3.2.3 从数据库查询权限信息

3.2.3.1 RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

一个用户可能有多个权限,所以我们会有一个权限表。正常都是用户关联角色,角色关联权限决定可以进行哪些操作。一个权限对应多个用户,一个用户也会有多个权限,看起来是一个多对多的关系,但是从用户的角度看是一个一对多的关系。

在开发的过程中,给一个用户挨个赋予权限显然比较麻烦,所以我们可以建立起一个角色表,直接给用户赋予角色信息,即可将对应的权限通过角色间接的给用户。

角色表如何与权限表关联起来呢,我们通过一个角色权限关联表来将它们两个关联起来,表示关联关系。

同时,一个用户可以有多个角色,一个角色也有多个用户,这样的多对多的关系同样可以用一个用户角色关联表关联起来。

3.2.3.2 准备工作
/*Table structure for table sys_menu */
DROP TABLE IF EXISTS sys_menu;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT'NULL' COMMENT'菜单名',
`path` varchar(200) DEFAULT NULL COMMENT'路由地址',
`component` varchar(255) DEFAULT NULL COMMENT'组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(O显示 1隐藏)',
`status`char(1) DEFAULT'O'COMMENT '菜单状态(O正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT'权限标识',
`icon` varchar(100) DEFAULT'#'COMMENT'菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

/*Table structure for table sys_user_role */
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE sys_user_role (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT'用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT'角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*Table structure for table sys_role */
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT'角色权限字符串',
`status`char(1) DEFAULT '0' COMMENT '角色状态(O正常 1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

/*Table structure for table sys_role_menu* */
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
编写SQL语句

根据userId查询,perms对应的role和menu都必须是正常状态的。

select * from sys_user_role where user_id = #{userId}

如果只是像上面这么查询,那么就只能查到user_id 和role_id,查不到其他信息,所以我们要使用联表查询。

select * from sys_user_role ur left join sys_role r on ur.role_id = r.id where user_id = #{userId} and r.status=0;

这样做还不够,我们还没有查找到角色id对应的权限。

select * 
from sys_user_role ur 
	left join sys_role r on ur.role_id = r.id
	left join sys_role_menu rm on ur.role_id = rm.role_id
	left join sys_menu m on m.id = rm.menu_id
where user_id = #{userId} 
	and r.status=0
	and m.status=0

再稍微优化一下,我们并不需要查询出这么多信息,根据需求我们只需要perms这一个字段。

select distinct m.perms
from sys_user_role ur 
	left join sys_role r on ur.role_id = r.id
	left join sys_role_menu rm on ur.role_id = rm.role_id
	left join sys_menu m on m.id = rm.menu_id
where user_id = #{userId} 
	and r.status=0
	and m.status=0

要注意的是我们查询出来的结果需要去重,原因是因为每一个用户可以有多个角色,而每一个角色可能拥有的权限会有相同的地方,所以要用distinct来去重。

编写实体类

因为perms在sys_menu这张表中,所以只用创建这一个实体类就行。

package domain;

import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import Tombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;


@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Merlu implements Serializable {
	private static final long serialVersionUID = -54979041104113736L;
	@TableId
	private Long id;
	/**
	 * 菜单名
	 */
	private String menuName;
	/**
 	 * 路由地址
	 */
	private String path;
	/**
	 * 组件路径
	 */
	private String component;
	/**
	 * 菜单状态(0显示1隐藏)
	 */
	private String visible;
	/**
	 * 菜单状态(0显示1隐藏)
	 */
	private String visible;
	/**
	 * 菜单状态(0正常1停用)
	 */
	private String status;
	/**
	 * 权限标识
	 */
	private String perms;
	/**
 	 * 菜单图标
	 */
	private String icon;
	private Long createBy;
	private Date createTime;
	private Long updateBy;
	private Date updateTime;
	/**
	 * 是否删除(0未删除1已删除)
	 */
	private Integer delFlag;
	/**
	 * 备注
	 */
	private String remark;
}

3.2.3.3 代码实现

我们只需要根据用户id去查询到其所对应的权限信息即可。所以我们可以先定义个mapper,其中提供一个方法可以根据userid查询权限信息。

package mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import domain.Menu;
import java.util.List;

public interface MenuMapper extends BaseMapper<Menu> {
	List<String> selectPermsByUserId(Long id);
}

由于mybatis-plus默认的方法不能满足查询的需求,所以我们这里是自定义方法,所以需要创建对应的mapper文件,定义对应的sql语句。这个对应的mapper文件应该放在resources目录下的mapper文件夹中。

<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="mapper.MenuMapper">
	<select id="selectPermsByUserId" resultType="java.lang.String">
    	select distinct m.perms
		from sys_user_role ur 
			left join sys_role r on ur.role_id = r.id
			left join sys_role_menu rm on ur.role_id = rm.role_id
			left join sys_menu m on m.id = rm.menu_id
		where user_id = #{userId} 
			and r.status=0
			and m.status=0
	</select>
</mapper>

除此之外,我们还需要告诉mybatis-plus我们的mapper文件存放在什么地方,这里要配置yaml。

mybaits-plus:
  mapperlocations: classpath*:/mapper/**/*. xml

接下来我们要在UserDetailsServiceImpl中加入对应的方法

package service.impl;

import ...

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
	private MenuMapper menuMapper;
    
    @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        private UserMapper 
        
        //查询用户信息
        LambdaQueryWrapper<User> queryWeapper=new LambdaQueryWrapper();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        
        //如果没有查询到用户,就抛出异常
        if(Object.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }
        
  		//TODO 查询对应的权限信息,将之前的测试代码注释掉,然后调用menuMapper中的方法,并存入list中
		//List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
        List<String> list = menuMapper.selectPermsByUserId(user.getId());

        
        //把数据封装成UserDetails返回
        return new LoginUser(user, list);
	}
}


我们还需要在controller里对这个鉴权做一个测试,将3.2.1中的代码稍作修改,将test这个权限改成数据库中有的权限。我们在这里规定了用户访问受限资源需要的权限,后续只要修改用户的权限就可以灵活实现访问。

@RestController
public class Hellocontroller {
	
    @RequestMapping("/hello")
	@PreAuthorize("hasAuthority('test')")
	public String hel1o(){
		return "hello";
    }
}

end
  • 作者:dicraft(联系作者)
  • 更新时间:2022-09-02 09:31
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论

    新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!