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;
}