shiro权限控制

shiro权限控制的实现,是由类ShiroRealm继承抽象类AuthorizingRealm实现doGetAuthorizationInfo()方法。授权即为访问控制,根据不同用户所拥有的不同角色,对每个角色进行访问资源的控制。

数据库设计

shiro权限控制中,有三个核心元素,用户、角色、权限,使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。不同的角色拥有不同的访问权限,再通过把角色赋予给用户实现用户访问权限的控制。

三张基本表:用户表t_user、角色表t_role、权限表t_permission

两张关系表:用户和角色关系表:t_user_role,角色权限表:t_role_permission

DROP TABLE IF EXISTS `t_permission`;

CREATE TABLE `t_permission` (
  `id` int(11) NOT NULL,
  `url` varchar(256) DEFAULT NULL,
  `description` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `t_permission`(`id`,`url`,`description`) values (1,'/user','user:user'),(2,'/user/add','user:add'),(1,'/user/delete','user:delete');

DROP TABLE IF EXISTS `t_role`;

CREATE TABLE `t_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(32) DEFAULT NULL,
  `description` varchar(32) DEFAULT NULL,
  KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

insert  into `t_role`(`id`,`rolename`,`description`) values (1,'admin','超级管理员'),(2,'test','测试账户');

DROP TABLE IF EXISTS `t_role_permision`;

CREATE TABLE `t_role_permision` (
  `role_id` int(12) DEFAULT NULL,
  `permission_id` int(12) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `t_role_permision`(`role_id`,`permission_id`) values (1,1),(1,2),(1,3),(2,1);

DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL,
  `username` varchar(25) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `create_time` date DEFAULT NULL,
  `status` varchar(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `t_user`(`id`,`username`,`password`,`create_time`,`status`) values (1,'admin','9f953fb5518da53fe1dc83f47861cb6d','2019-04-11','1'),(2,'test','d655eb5f87c32223e3d037b760a0341d','2019-04-11','0');

DROP TABLE IF EXISTS `t_user_role`;

CREATE TABLE `t_user_role` (
  `user_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `t_user_role`(`user_id`,`role_id`) values (1,1),(2,2);

构建role实体类和permission实体类及相应的dao

public class Role implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id ;
    private String roleName;
    private String description;
    //set/geter方法
}

@Mapper
public interface UserRoleMapper {
    List<Role> findByUserName(String userNmae);
}


public class Permission implements Serializable {
    private  static  final long serialVersionUID = 1L;
    private Integer id;
    private String url;
    private String permission;
    //set/geter方法
}

@Mapper
public interface UserPermissionMapper {
    List<Permission> findByUserName(String userName);
}

xml实现

UserRoleMapper实现查询

<?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="com.springboot.shiro.dao.UserRoleMapper">

    <resultMap type="com.springboot.shiro.entity.Role" id="role">
        <id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"/>
        <id column="rolename" property="roleName" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <id column="description" property="description" javaType="java.lang.String" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="findByUserName" resultMap="role">
        select r.id,r.rolename,r.description from t_role r
        left join t_user_role ur on(r.id = ur.role_id)
        left join t_user u on(u.id = ur.user_id)
        where u.username = #{userName}
    </select>
</mapper>

UserPermissionMapper查询实现

<?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="com.springboot.shiro.dao.UserPermissionMapper">

    <resultMap type="com.springboot.shiro.entity.Permission" id="permission">
        <id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"/>
        <id column="url" property="url" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <id column="permission" property="permission" javaType="java.lang.String" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="findByUserName" resultMap="permission">
        select p.id,p.url,p.permission from t_role r
        left join t_user_role ur on(r.id = ur.role_id)
        left join t_user u on(u.id = ur.user_id)
        left join t_role_permission rp on(rp.role_id = r.id)
        left join t_permission p on(p.id = rp.permission_id )
        where u.username = #{userName}
    </select>
</mapper>

实现shiroRealm访问控制

角色的权限控制是有 doGetAuthorizationInfo()实现的,

/**
 * 获取用户角色和权限
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
    User user = (User) SecurityUtils.getSubject().getPrincipal();
    String userName = user.getUserName();
    System.out.println("用户" + userName + "获取权限-----ShiroRealm.doGetAuthorizationInfo");
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

    //获取角色集
    List<Role> roleList = userRoleMapper.findByUserName(userName);
    Set<String> roleSet = new HashSet<String>();
    for (Role r:roleList){
        roleSet.add(r.getRoleName());
    }
    authorizationInfo.setRoles(roleSet);
    // 获取用户权限集
    List<Permission> permissionList = userPermissionMapper.findByUserName(userName);
    Set<String> permissionSet = new HashSet<String>();
    for (Permission p : permissionList) {
        permissionSet.add(p.getPermission());
    }
    authorizationInfo.setStringPermissions(permissionSet);
    return authorizationInfo;
}

通过userRoleMapper.findByUserName(userName)和userPermissionMapper.findByUserName(userName)查询数据库获取角色信息和所对应的权限信息,并存储在SimpleAuthorizationInfo的对象中,返回给shiro。

shiroConfig的实现

shiro提供了几种和权限相关的注解,

注解名 描述
@RequiresAuthenticatio 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。
@RequiresUser 表示当前Subject已经身份验证或者通过记住我登录的
@RequiresGuest 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND) 表示当前Subject需要角色admin和user。
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR) 表示当前Subject需要权限user:a或user:b

在shiroconfig中添加如下代码开启注解的使用

@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

/**
 *DefaultAdvisorAutoProxyCreator 是用来扫描上下文,寻找所有的Advistor(通知器)<@RequiresPermissions("user:user")></>,将这些Advisor应用到所有符合切入点的Bean中。
 *  所以必须在lifecycleBeanPostProcessor创建之后创建,所以用了depends-on=”lifecycleBeanPostProcessor”>
 * @return
 */
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
}


@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
}

controller实现

编写userController类用户处理用户权限的访问控制:

@Controller
@RequestMapping("/user")
public class UserController {
    @RequiresPermissions("user:user")
    @RequestMapping("list")
    public String userList(Model model){
        model.addAttribute("value","获取用户信息");
        return "user";
    }

    @RequiresPermissions("user:add")
    @RequestMapping("add")
    public String userAdd(Model model){
        model.addAttribute("value","新增用户");
        return "user";
    }    

    @RequiresPermissions("user:delete")
    @RequestMapping("delete")
    public String userDelete(Model model){
        model.addAttribute("value","删除用户信息");
        return "user";
    }
}

@GetMapping("/403")
public String forbid() {
    return "403";
}

有个小问题,当用户没有权限访问资源时会报错,

虽然在ShiroConfig中配置了 shiroFilterFactoryBean.setUnauthorizedUrl(“/403”); ,没有权限的访问会自动重定向到/403,结果证明并不是这样。后来研究发现,该设置只对filterChain起作用,比如在filterChain中设置了 filterChainDefinitionMap.put(“/user/update”, “perms[user:update]”); ,如果用户没有 user:update 权限,那么当其访问 /user/update 的时候,页面会被重定向到/403。
对于上面这个问题,可以定义一个全局异常捕获类:

@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {

    @ExceptionHandler(value = AuthorizationException.class)
    public String handleAuthorizationException(){
        return "403";
    }
}

相关html页面省略,详细请见GitHub。

Redis实现权限的缓存

上述基本实现权限的访问控制,但还一个需要优化的地方,就是每次查询用户的角色和对应的权限是时,都是实时去查询数据库实现的,一般权限是不会一直变的,直接造成了资源的浪费,添加Redis缓存,把权限存到Redis中,不需要每次都查询数据库。

引入Redis依赖

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>2.4.2.1-RELEASE</version>
</dependency>

配置application.yml

spring
  redis:
     host: localhost
     port: 6379
     pool:
       max-active: 8
       max-wait: -1
       max-idle: 8
       min-idle: 0
     timeout: 0

shiroConfig配置Redis

/**
 * 增加权限缓存,
 * @return
 */
public RedisManager redisManager(){
    RedisManager redisManager = new RedisManager();
    return redisManager;
}

public RedisCacheManager cacheManager(){
    RedisCacheManager redisCacheManager = new RedisCacheManager();
    redisCacheManager.setRedisManager(redisManager());
    return redisCacheManager;
}

@Bean
public SecurityManager securityManager(){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(shiroRealm());
    securityManager.setRememberMeManager(rememberMeManager());
    securityManager.setCacheManager(cacheManager());
    return securityManager;
}