前情提要
这一篇是关于动态权限和动态目录的,
shiro
的授权器
在碰到权限的校验时候才会去触发,这个时候就可以从数据库
中获取到用户关联的角色
,
角色绑定的权限,大概就如下图了
有兴趣可以了解一下RBAC
,大概就是如下的一个关系
动态目录
就更简单了,用户关联的角色,角色所拥有的目录,这个就是展示的目录了,修改数据库数据就可达到动态的目的。
正文开始
设计五个表,管理员表(也就是用户表,已存在)
、角色表
、目录表
、用户角色表
、角色目录表
,如果没懂这些关联,可以看一下图片,图片最下方标记了关系,希望能看懂
创建数据库sys_menu
创建sys_role
创建sys_role_menu
创建sys_user_role
记得创建对应的controller
和service
和dao
以及xml
、entity
太多了,就不一一创建展示了,记得service
和dao
集成mybatis plus
提供的类
修改上篇没动的授权器UserRealm
@Autowired
private SysMenuService menuService;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("这里是授权");
UserEntity userEntity = (UserEntity) principalCollection.getPrimaryPrincipal();
Integer id = userEntity.getId();
//设置id为空则拥有所有权限,sql中设置了id为空则查询所有权限
List<SysMenuEntity> menuList = menuService.findByUserId(id);
//转存set是为了去重,保证权限唯一
Set<String> collect = menuList.stream().map(SysMenuEntity::getPerms).collect(Collectors.toSet());
//所有权限
Set<String> perms = new HashSet<>();
collect.stream().forEach(y -> {
//防止空的造成异常
if(!StringUtils.isEmpty(y)){
//存放无论是否有多个或者单个,直接变成数组,更加清晰
/**
* 查询的权限中含有sys:user:info,sys:user:list
* 存入的依旧是set,防止权限重复,直接切割分隔的权限
*/
perms.addAll(Arrays.asList(y.split(",")));
}
});
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//此处是放入权限中,不是role角色中
simpleAuthorizationInfo.setStringPermissions(perms);
return simpleAuthorizationInfo;
}
SysMenuService
//用户查询关联的目录
List<SysMenuEntity> findByUserId(Integer userId);
SysMenuServiceImpl
@Autowired
private SysMenuDao menuDao;
@Override
public List<SysMenuEntity> findByUserId(Integer userId) {
return menuDao.selectByUserId(userId);
}
SysMenuDao
List<SysMenuEntity> selectByUserId(@Param("userId") Integer userId);
SysMenuDao.xml
<select id="selectByUserId" resultType="com.macro.entity.SysMenuEntity">
select m.* from sys_user_role ur
LEFT JOIN sys_role_menu rm on rm.role_id = ur.role_id
LEFT JOIN sys_menu m on m.id = rm.menu_id
where 1=1
<if test="userId != null and userId != ''">
and ur.user_id = #{userId}
</if>
GROUP BY m.id
</select>
开启注解,校验权限 ShiroConfig
/**
* @Title: authorizationAttributeSourceAdvisor
* @Description:开启shiro提供的权限相关的注解
* @param defaultWebSecurityManager
* @return AuthorizationAttributeSourceAdvisor
**/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
return authorizationAttributeSourceAdvisor;
}
sysUserController
中加入权限校验
添加的权限Permissions
,授权器添加的也是权限
,并非角色
注解没注意看,结果加错了,一直找,没找到原因,头都大了,后来仔细研究了一下才看到写错了
@RequiresRoles
:角色校验 (如 : user)
@RequiresPermissions
:权限校验 (如 : sys:user:info)
//只添加了@RequiresPermissions("sys:user:info")
//添加的权限Permissions,授权器添加的也是权限,并非角色
@RequiresPermissions("sys:user:info")
@GetMapping("info/{id}")
public Result info(@PathVariable("id") Integer id){
UserEntity userEntity = userService.getById(id);
return Result.success(userEntity);
}
运行项目后准备修改一条数据,这个时候角色
和权限
都是空的
发生了异常,Subject does not have permission [sys:user:info]
然后再sys_role
、sys_user_role
、sys_menu
、sys_role_menu
,中个添加一条数据
sys_role
sys_user_role
sys_menu
sys_role_menu
上面都关联起来了,然后退出,重新登陆
,让他加载一下角色权限,试一下,没啥问题了
整合redis
pom.xml
添加redis
依赖
我这边单独引入spring-boot-starter-data-redis
会发生异常,所以增加commons-pool2
依赖
不信邪的可以试试
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 填redis坑-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
#### yml
引入redis
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 3000
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
在utils
包下新建session
、redis
包
session
包下新建RedisSessionDao
类
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
public class RedisSessionDao extends AbstractSessionDAO {
// Session超时时间,单位为毫秒
private long expireTime = 1200000;
@Autowired
private RedisTemplate redisTemplate;// Redis操作类,对这个使用不熟悉的,可以参考前面的博客
public RedisSessionDao() {
super();
}
public RedisSessionDao(long expireTime, RedisTemplate redisTemplate) {
super();
this.expireTime = expireTime;
this.redisTemplate = redisTemplate;
}
@Override // 更新session
public void update(Session session) throws UnknownSessionException {
if (session == null || session.getId() == null) {
return;
}
session.setTimeout(expireTime);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
}
@Override // 删除session
public void delete(Session session) {
if (null == session) {
return;
}
redisTemplate.opsForValue().getOperations().delete(session.getId());
}
@Override// 获取活跃的session,可以用来统计在线人数,如果要实现这个功能,可以在将session加入redis时指定一个session前缀,统计的时候则使用keys("session-prefix*")的方式来模糊查找redis中所有的session集合
public Collection<Session> getActiveSessions() {
return redisTemplate.keys("*");
}
@Override// 加入session
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
return sessionId;
}
@Override// 读取session
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
Session session = (Session) redisTemplate.opsForValue().get(sessionId);
return session;
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
public RedisTemplate getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
#### 在utils
包下新建ApplicationContextUtil
工具类
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
public static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("初始化");
context = applicationContext;
}
/**
* 根据工厂中的类名获取类实例
*/
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
在redis
包下新建RedisCacheManager
类和RedisCache
类
RedisCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("缓存名称: "+cacheName);
return new RedisCache<K,V>(cacheName);
}
}
RedisCache
重写了shiro内部的缓存方式,采用了redis缓存
import com.macro.utils.ApplicationContextUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.Set;
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public V get(K k) throws CacheException {
System.out.println("获取缓存:"+ k);
return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("设置缓存key: "+k+" value:"+v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
return redisTemplate;
}
}
修改 ShiroConfig
//2.创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
//新增
//配置自定义的session缓存
defaultWebSecurityManager.setSessionManager(configWebSessionManager());
//配置自定义缓存redis
defaultWebSecurityManager.setCacheManager(redisCacheManager());
return defaultWebSecurityManager;
}
//3.将自定义的Realm 设置为Bean ,注入到2中
@Bean
public Realm getRealm(){
UserRealm realm = new UserRealm();
// 设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
//这一步开始就是新加的了
real