用户-角色-权限 RBAC模型
用户 | 角色 | 权限 |
---|---|---|
luo | 用户管理员 | 对后台用户的CRU |
zhou | 仓库管理员 | 对仓库数据的CRU |
admin | 超级管理员 | 所有库中的权限 |
业务描述:
当用户访问首页时,尽请访问
当用户查看用户列表时,需要登录、需要有该权限
当用户查看仓库列表时,需要有仓库权限
当用户删除用户时,需要有超级管理员角色
是用mybatis plus
https://mp.baomidou.com/guide/
spring
接口继承BaseMapper<T>
public interface AdministratorMapper extends BaseMapper<Administrator> {
}
pojo添加注解
引导类添加扫描
@MapperScan("com.itheima.shiro.mapper")
public interface AdminService {
}
?
省略...
<!--使用thymeleaf 首先完成一个登陆页面--> <!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org/"> <head> <meta charset="UTF-8"> <title>登录页面</title> </head> <body> <!--<h5 th:text="${err_msg}"></h5>--> <form action="/backend/login" method="post"> <input name="username"/><br> <input name="password"/><br> <input type="submit" value="登录"/> </form> </body> </html>
需求:用户未登录时,访问/user/all路径,告诉用户调到登录页面
添加shiro配置:安全管理器、realm、shiroFilter
@Configuration public class ShiroConfig { //0.配置shiroFilter @Bean public ShiroFilterFactoryBean shiroFilter(){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager()); shiroFilterFactoryBean.setLoginUrl("/backend/toLogin"); Map filterChainMap = new LinkedHashMap<String,String>(); filterChainMap.put("/backend/toLogin","anon"); //跳转登录页面放行 filterChainMap.put("/backend/login","anon"); //登录请求 放行 filterChainMap.put("/**","authc"); //认证 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap); return shiroFilterFactoryBean; } //1.配置安全管理器 @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } //2.配置realm @Bean public Realm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } }
需求:用户新增时,密码进行加密(md5+随机盐加密): MD5(明文密码+随机salt)
用户创建
public void saveAdmin(Administrator admin) { String password = admin.getPassword(); String salt = RandomStringUtils.randomNumeric(6,8); admin.setPrivateSalt(salt); Md5Hash md5Hash = new Md5Hash(password,salt); //模拟md5加密一次 admin.setPassword(md5Hash.toString()); admin.setUserStatus("1"); adminMapper.insert(admin); }
登录配置、测试、访问
@RequestMapping("/login") public String login(@RequestParam String username, @RequestParam String password){ //登录 try{ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password); subject.login(token); }catch (Exception e){ e.printStackTrace(); } return "success"; }
配置、开发realm
//realm需要密码匹配器设置 public CredentialsMatcher myMd5Matcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("md5"); matcher.setHashIterations(1); return matcher; }
realm的认证信息完善:
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("经过认证用户的获取"); UsernamePasswordToken loginToken = (UsernamePasswordToken)token; String username = loginToken.getUsername(); //根据用户名查询用户 Administrator admin = adminService.findAdminByUsername(username); if(admin == null){ return null; //框架自动抛出位置账户异常 }else{ ByteSource saltBS = new SimpleByteSource(admin.getPrivateSalt()); return new SimpleAuthenticationInfo(admin,admin.getPassword(),saltBS,getName()); } }
退出
filterChainMap.put("/backend/logout","logout");
//也可以准备一个controller方法,使用Subject的方法进行退出 Subject subject = SecurityUtils.getSubject(); subject.logout();
当用户查看用户列表时,需要登录、需要有该权限 filterChainMap.put("/user/all","perms[user:select]"); //查询所有用户 需要认证(登录) //当用户查看仓库列表时,需要有仓库权限 filterChainMap.put("/storage/all","perms[storage:select]"); //当用户删除用户时,需要有超级管理员角色 filterChainMap.put("/user/del/*","roles[role_superman]");
权限控制:角色、权限
filterChainMap.put("/user/all","perms[user:select]"); //需要权限 user:select filterChainMap.put("/user/*","roles[role_user]"); //需要角色 role_user
赋权:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("经过权限获取"); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //从数据库查询该用户的权限列表 Administrator principal = (Administrator) principals.getPrimaryPrincipal(); String password = principal.getPassword(); simpleAuthorizationInfo.addStringPermission("user:select"); //为当前登录用户主体赋权 return simpleAuthorizationInfo; }
数据库数据赋权:
private void addPerms(String username,SimpleAuthorizationInfo simpleAuthorizationInfo){ Set<String> roleSet = adminService.findRolesByUsername(username); if(roleSet != null && roleSet.size() >0){ simpleAuthorizationInfo.addRoles(roleSet); } Set<String> permissionSet = adminService.findPermissionsByUsername(username); if(permissionSet != null && permissionSet.size() >0){ simpleAuthorizationInfo.addStringPermissions(permissionSet); } }
@RequiresPermissions("page:storage") @RequiresRoles("role_superman")
只是用注解是不生效的,需要添加配置
/** * 注解支持: */ @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
需要引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
配置标签支持
@Bean public ShiroDialect shiroDialect(){ return new ShiroDialect(); }
在页面中使用标签
<shiro:principal property="username"></shiro:principal>
自定义会话管理器
@Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); //设置会话过期时间 sessionManager.setGlobalSessionTimeout(3*60*1000); //默认半小时 sessionManager.setDeleteInvalidSessions(true); //默认自定调用SessionDAO的delete方法删除会话 //设置会话定时检查 // sessionManager.setSessionValidationInterval(180000); //默认一小时 // sessionManager.setSessionValidationSchedulerEnabled(true); return sessionManager; }
@Bean public SessionDAO redisSessionDAO(){ ShiroRedisSessionDao redisDAO = new ShiroRedisSessionDao(); return redisDAO; }
自定义CachingSessionDao
public class ShiroRedisSessionDao extends CachingSessionDAO { public static final String SHIRO_SESSION_KEY = "shiro_session_key"; private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RedisTemplate redisTemplate; @Override protected void doUpdate(Session session) { this.saveSession(session); } @Override protected void doDelete(Session session) { if (session == null || session.getId() == null) { logger.error("session or session id is null"); return ; } //根据session id删除session redisTemplate.boundHashOps(SHIRO_SESSION_KEY).delete(session.getId()); } @Override protected Serializable doCreate(Session session) { Serializable sessionId = this.generateSessionId(session); this.assignSessionId(session, sessionId); this.saveSession(session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { if(sessionId == null){ logger.error("传入的 session id is null"); return null; } return (Session)redisTemplate.boundHashOps(SHIRO_SESSION_KEY).get(sessionId); } /** * 将session 保存进redis 中 * @param session 要保存的session */ private void saveSession(Session session) { if (session == null || session.getId() == null) { logger.error("session or session id is null"); return ; } redisTemplate.boundHashOps(SHIRO_SESSION_KEY).put(session.getId(),session); } }
交给安全管理器
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(sessionManager()); securityManager.setRealm(myRealm()); return securityManager; }
每次访问带有权限相关的判断的请求时,都会执行doGetAuthorizationInfo()方法 可以缓存授权权限信息,不需要每次都查询数据库赋权 其实,shiro默认支持的缓存是ehcache(java语言开发的本地缓存技术,依赖jvm)
自定义缓存管理器
public class MyRedisCacheManager implements CacheManager { @Autowired private RedisTemplate redisTemplate; @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { return new ShiroRedisCache(name,redisTemplate); } }
自定义redis缓存
package com.itheima.shiroConfig; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * */ public class ShiroRedisCache<K, V> implements Cache<K, V> { private static Logger LOGGER = LogManager.getLogger(ShiroRedisCache.class); /** * key前缀 */ private static final String REDIS_SHIRO_CACHE_KEY_PREFIX = "shiro_cache_key_"; /** * cache name */ private String name; /** * jedis 连接工厂 */ private RedisTemplate redisTemplate; /** * 序列化工具 */ private RedisSerializer serializer = new JdkSerializationRedisSerializer(); /** * 存储key的redis.list的key值 */ private String keyListKey; private RedisConnection getConnection(){ return this.redisTemplate.getConnectionFactory().getConnection(); } public ShiroRedisCache(String name,RedisTemplate redisTemplate) { this.name = name; this.redisTemplate = redisTemplate; this.keyListKey = REDIS_SHIRO_CACHE_KEY_PREFIX + name; } @Override public V get(K key) throws CacheException { LOGGER.debug("shiro redis cache get.{} K={}", name, key); RedisConnection redisConnection = null; V result = null; try { redisConnection = getConnection(); result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key)))); } catch (Exception e) { LOGGER.error("shiro redis cache get exception. ", e); } finally { if (null != redisConnection) { redisConnection.close(); } } return result; } @Override public V put(K key, V value) throws CacheException { LOGGER.debug("shiro redis cache put.{} K={} V={}", name, key, value); RedisConnection redisConnection = null; V result = null; try { redisConnection = getConnection(); result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key)))); redisConnection.set(serializer.serialize(generateKey(key)), serializer.serialize(value)); redisConnection.lPush(serializer.serialize(keyListKey), serializer.serialize(generateKey(key))); } catch (Exception e) { LOGGER.error("shiro redis cache put exception. ", e); } finally { if (null != redisConnection) { redisConnection.close(); } } return result; } @Override public V remove(K key) throws CacheException { LOGGER.debug("shiro redis cache remove.{} K={}", name, key); RedisConnection redisConnection = null; V result = null; try { redisConnection = getConnection(); result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key)))); redisConnection.expireAt(serializer.serialize(generateKey(key)), 0); redisConnection.lRem(serializer.serialize(keyListKey), 1, serializer.serialize(key)); } catch (Exception e) { LOGGER.error("shiro redis cache remove exception. ", e); } finally { if (null != redisConnection) { redisConnection.close(); } } return result; } @Override public void clear() throws CacheException { LOGGER.debug("shiro redis cache clear.{}", name); RedisConnection redisConnection = null; try { redisConnection = getConnection(); Long length = redisConnection.lLen(serializer.serialize(keyListKey)); if (0 == length) { return; } List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1); for (byte[] key : keyList) { redisConnection.expireAt(key, 0); } redisConnection.expireAt(serializer.serialize(keyListKey), 0); keyList.clear(); } catch (Exception e) { LOGGER.error("shiro redis cache clear exception.", e); } finally { if (null != redisConnection) { redisConnection.close(); } } } @Override public int size() { LOGGER.debug("shiro redis cache size.{}", name); RedisConnection redisConnection = null; int length = 0; try { redisConnection = getConnection(); length = Math.toIntExact(redisConnection.lLen(serializer.serialize(keyListKey))); } catch (Exception e) { LOGGER.error("shiro redis cache size exception.", e); } finally { if (null != redisConnection) { redisConnection.close(); } } return length; } @Override public Set keys() { LOGGER.debug("shiro redis cache keys.{}", name); RedisConnection redisConnection = null; Set resultSet = null; try { redisConnection = getConnection(); Long length = redisConnection.lLen(serializer.serialize(keyListKey)); if (0 == length) { return resultSet; } List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1); resultSet = keyList.stream().map(bytes -> serializer.deserialize(bytes)).collect(Collectors.toSet()); } catch (Exception e) { LOGGER.error("shiro redis cache keys exception.", e); } finally { if (null != redisConnection) { redisConnection.close(); } } return resultSet; } @Override public Collection values() { RedisConnection redisConnection = getConnection(); Set keys = this.keys(); List<Object> values = new ArrayList<Object>(); for (Object key : keys) { byte[] bytes = redisConnection.get(serializer.serialize(key)); values.add(serializer.deserialize(bytes)); } return values; } /** * 重组key * 区别其他使用环境的key * * @param key * @return */ private String generateKey(K key) { return REDIS_SHIRO_CACHE_KEY_PREFIX + name + "_" + key; } private byte[] getByteKey(K key) { if (key instanceof String) { String preKey = generateKey(key); return preKey.getBytes(); } return serializer.serialize(key); } }
可以只在realm中设置缓存管理器
// @Bean public Realm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(myMd5Matcher()); myShiroRealm.setAuthorizationCacheName("perms"); myShiroRealm.setAuthorizationCachingEnabled(true); myShiroRealm.setAuthenticationCachingEnabled(false); //设置缓存管理器 myShiroRealm.setCacheManager(cacheManager()); return myShiroRealm; } //缓存管理 @Bean public CacheManager cacheManager(){ MyRedisCacheManager cacheManager = new MyRedisCacheManager(); return cacheManager; }
注意,我在此处做得会话和缓存管理没有对过期的缓存数据进行定时清理!!!
有一个已经第三方框架做了对shiro和redis的整合:
https://github.com/alexxiyang/shiro-redis -- 把会话管理和缓存管理都整合好了,直接依赖即可
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> </dependency>
可以使用全局异常处理器来捕获权限异常
@ControllerAdvice public class GloableExceptionResolver { @ExceptionHandler(UnauthorizedException.class) public void calUnauthorizedException(UnauthorizedException e){ PrintWriter writer = null; try{ //判断是否是异步请求 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); HttpServletResponse response = requestAttributes.getResponse(); String header = request.getHeader("X-Requested-With"); if(StringUtils.isNoneBlank(header) && "XMLHttpRequest".equalsIgnoreCase(header)){ response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); writer = response.getWriter(); // {"status":401,"message":"无权访问"} // String respStr = "" writer.write("{\"status\":401,\"message\":\"无权访问\"}"); }else{ String contextPath = request.getContextPath(); if("/".equals(contextPath)) contextPath = ""; response.sendRedirect(request.getContextPath() + "/backend/toDenied"); } }catch (IOException io){ io.printStackTrace(); }finally { if(writer != null) writer.close(); } } }
原文:https://www.cnblogs.com/juddy/p/13568970.html