在第一篇中,我们说过,用户<–>角色<–>权限三层中,暂时不考虑权限,在这一篇,是时候把它完成了。

为了方便演示,这里的权限只是对角色赋予权限,也就是说同一个角色的用户,权限是一样的。当然了,你也可以精细化到为每一个用户设置权限,但是这不在本篇的探讨范围,有兴趣可以自己实验,原理都是一样的。

一、数据准备

1.1 创建 sys_permission 表

让我们先创建一张权限表,名为 sys_permission

CREATE TABLE `sys_permission`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) DEFAULT NULL,
`role_id` int(11) DEFAULT NULL,
`permission` varchar(255) DEFAULT NULL,
PRIMARY KEY(`id`),
KEY `fk_roleId`(`role_id`),
CONSTRAINT `fk_roleId` FOREIGN KEY(`role_id`) REFERENCES `sys_role`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
)ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

内容就是两条数据,通过 url + role_id + permission 唯一标识了一个角色访问某一url时的权限,其中权限暂定为c、r、u、d,代表了增、删、改、查。

sys_permission 表数据

1.2 创建 Model、Mapper、Service

(1)Model

public class SysPermission implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
    private String url;
    private Integer roleId;
    private String permission;
    private List permissions;
    //省略getter,setter
}

这里需要注意的时相比于数据库,多了一个 permissions 属性,该字段将 permission 按逗号分割为了 list。

(2)mapper

@Mapper
public interface SysPermissionMapper {
    @Select("SELECT * FROM sys_permission WHERE role_id=#{roleId}")
    List<SysPermission>listByRoleId(Integer roleId);
}
@Mapper
public interface SysRoleMapper {
    @Select("SELECT * FROM sys_role WHERE id = #{id}")
    SysRole selectById(Integer id);

    @Select("SELECT * FROM sys_role WHERE name = #{name}")
    SysRole selectByName(String name);
}

(3)Service

SysPermissionService 中有一个方法,根据 roleId 获取所有的 SysPermission

@Service
public class SysPermissionService {
    @Autowired
    SysPermissionMapper permissionMapper;
    
    /**获取指定角色所有权限**/
    public List<SysPermission> listByRoleId(Integer roleId){
        return permissionMapper.listByRoleId(roleId);
    }
}
@Service
public class SysRoleService {
    @Autowired
    SysRoleMapper sysRoleMapper;
    
    public SysRole selectById(Integer id){
        return sysRoleMapper.selectById(id);
    }
    public SysRole selectByName(String name){
        return sysRoleMapper.selectByName(name);
    }
}

1.3 修改接口

@Controller
public class LoginController {
    //...

    @RequestMapping("/admin")
    @ResponseBody
    @PreAuthorize("hasPermission('/admin','r')")
    public String printAdminR() {
        return "如果你看见这句话,说明你访问/admin路径具有r权限";
    }

    @RequestMapping("/admin/c")
    @ResponseBody
    @PreAuthorize("hasPermission('/admin','c')")
    public String printAdminC() {
        return "如果你看见这句话,说明你访问/admin路径具有c权限";
    }
}

让我们修改下我们要访问的接口,@PreAuthorize("hasPermission('/admin','r')") 是关键,参数1指明了访问该接口需要的url,参数2指明了访问该接口需要的权限

二、PermissionEvaluator

我们需要自定义对 hasPermission() 方法的处理,就需要自定义 PermissionEvaluator,创建类 CustomPermissionEvaluator,实现 PermissionEvaluator 接口。

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    SysPermissionService permissionService;
    @Autowired
    SysRoleService roleService;

    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object targetPermission) {
        //获得loadUserByUsername()方法的结果
        User user = (User) authentication.getPrincipal();
        //获得loadUserByUsername()中注入的角色
        Collection<GrantedAuthority> authorities = user.getAuthorities();

        //遍历用户所有角色
        for(GrantedAuthority authority:authorities){
            String roleName = authority.getAuthority();
            Integer roleId = roleService.selectByName(roleName).getId();
            //得到角色所有的权限
            List<SysPermission> permissionList = permissionService.listByRoleId(roleId);
            //遍历permissionList
            for(SysPermission sysPermission:permissionList){
                //获取权限集
                List permissions = sysPermission.getPermissions();
                //如果访问的url和权限用户符合的话,返回true
                if(targetUrl.equals(sysPermission.getUrl()) && permissions.contains(targetPermission)){
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

hasPermission() 方法中,参数 1 代表用户的权限身份,参数 2 参数 3 分别和 @PreAuthorize("hasPermission('/admin','r')") 中的参数对应,即访问 url 和权限

思路如下:

  1. 通过 Authentication 取出登录用户的所有 Role
  2. 遍历每一个 Role,获取到每个Role的所有 Permission
  3. 遍历每一个 Permission,只要有一个 Permissionurl 和传入的url相同,且该 Permission 中包含传入的权限,返回 true
  4. 如果遍历都结束,还没有找到,返回false

下面就是在 WebSecurityConfig 中注册 CustomPermissionEvaluator

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return handler;
    }
}

三、运行程序

当我使用角色为 ROLE_USER 的用户仍然能访问,因为该用户访问 /admin 路径具有 r 权限:

img