Loading... ## shiro的整合“心路历程” ### 1.准备数据 **用户-角色-权限** RBAC模型 | 用户 | 角色 | 权限 | | ------- | ------------ | ----------------- | | luo | 用户管理员 | 对后台用户的CRU | | zhou | 仓库管理员 | 对仓库数据的CRU | | admin | 超级管理员 | 所有库中的权限 | **业务描述:** - 当用户访问首页时,尽请访问 - 当用户查看用户列表时,需要登录、需要有该权限 - 当用户查看仓库列表时,需要有仓库权限 - 当用户删除用户时,需要有超级管理员角色 ### 2.springboot项目 #### 2.1 引入依赖 #### 2.2 pojo #### 2.3 DAO ``` 是用mybatis plus https://mp.baomidou.com/guide/ ``` ```yml spring: datasource: url: jdbc:mysql:///shiro_perm?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true ``` 接口继承BaseMapper<T> ```java public interface AdministratorMapper extends BaseMapper<Administrator> { } ``` pojo添加注解 ```java @Data @TableName("tb_admin") public class Administrator implements Serializable { @TableId(type = IdType.AUTO) private Integer id; private String username; private String password; private String realname; private String gender; private String privateSalt; //私有盐,用户密码加密 private String tel; private String userStatus; @TableField(exist = false) private List<Role> roleList; } ``` 引导类添加扫描 ``` @MapperScan("com.itheima.shiro.mapper") ``` #### 2.4 service ```java public interface AdminService { } @Service @Transactional public class AdminServiceImpl implements AdminService { @Autowired private AdministratorMapper adminMapper; } ``` #### controller ``` 省略... ``` #### 视图 ```html <!--使用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> ``` ### 3.shiro配置 #### 3.1 用户访问路径测试 ``` 需求:用户未登录时,访问/user/all路径,告诉用户调到登录页面 ``` 添加shiro配置:安全管理器、realm、shiroFilter ```java @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; } } ``` ### 4.认证(登录) ``` 需求:用户新增时,密码进行加密(md5+随机盐加密): MD5(明文密码+随机salt) ``` 用户创建 ```java 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); } ``` 登录配置、测试、访问 ```java @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 ```java //realm需要密码匹配器设置 public CredentialsMatcher myMd5Matcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("md5"); matcher.setHashIterations(1); return matcher; } ``` realm的认证信息完善: ```java @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()); } } ``` **退出** ```java filterChainMap.put("/backend/logout","logout"); ``` ```java //也可以准备一个controller方法,使用Subject的方法进行退出 Subject subject = SecurityUtils.getSubject(); subject.logout(); ``` ### 5.授权 ``` 当用户查看用户列表时,需要登录、需要有该权限 filterChainMap.put("/user/all","perms[user:select]"); //查询所有用户 需要认证(登录) //当用户查看仓库列表时,需要有仓库权限 filterChainMap.put("/storage/all","perms[storage:select]"); //当用户删除用户时,需要有超级管理员角色 filterChainMap.put("/user/del/*","roles[role_superman]"); ``` 权限控制:角色、权限 ```java filterChainMap.put("/user/all","perms[user:select]"); //需要权限 user:select filterChainMap.put("/user/*","roles[role_user]"); //需要角色 role_user ``` 赋权: ```java 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; } ``` 数据库数据赋权: ```java 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); } } ``` ### 6.注解权限控制 ```java @RequiresPermissions("page:storage") @RequiresRoles("role_superman") ``` 只是用注解是不生效的,需要添加配置 ```java /** * 注解支持: */ @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; } ``` ### 7.页面标签权限控制 需要引入依赖 ```xml <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> ``` 配置标签支持 ```java @Bean public ShiroDialect shiroDialect(){ return new ShiroDialect(); } ``` 在页面中使用标签 ```html <shiro:principal property="username"></shiro:principal> ``` ### 8.会话管理(redis) 自定义会话管理器 ```java @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; } ``` ```java @Bean public SessionDAO redisSessionDAO(){ ShiroRedisSessionDao redisDAO = new ShiroRedisSessionDao(); return redisDAO; } ``` 自定义CachingSessionDao ```java 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); } } ``` 交给安全管理器 ```java @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(sessionManager()); securityManager.setRealm(myRealm()); return securityManager; } ``` ### 9.缓存管理(redis) ``` 每次访问带有权限相关的判断的请求时,都会执行doGetAuthorizationInfo()方法 可以缓存授权权限信息,不需要每次都查询数据库赋权 其实,shiro默认支持的缓存是ehcache(java语言开发的本地缓存技术,依赖jvm) ``` 自定义缓存管理器 ```java 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缓存 ```java 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中设置缓存管理器 ```java // @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的整合: ```http https://github.com/alexxiyang/shiro-redis -- 把会话管理和缓存管理都整合好了,直接依赖即可 ``` ```xml <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> </dependency> ``` ### 10.异常处理 可以使用全局异常处理器来捕获权限异常 ```java @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(); } } } ``` 最后修改:2023 年 02 月 27 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏