• shiro的整合“心路历程”


    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/
    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>

    public interface AdministratorMapper extends BaseMapper<Administrator> {
    }

    pojo添加注解

    @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

    public interface AdminService {
    }

    @Service
    @Transactional
    public class AdminServiceImpl implements AdminService {
       @Autowired
       private AdministratorMapper adminMapper;
    }

    controller

    省略...

    视图

    <!--使用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

    @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)
    

    用户创建

    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();
    

     

    5.授权

    当用户查看用户列表时,需要登录、需要有该权限
    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);
        }
    }
    

    6.注解权限控制

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

    7.页面标签权限控制

    需要引入依赖

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

     

    8.会话管理(redis)

    自定义会话管理器

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

     

    9.缓存管理(redis)

    每次访问带有权限相关的判断的请求时,都会执行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>
    

    10.异常处理

    可以使用全局异常处理器来捕获权限异常

    @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();
            }
        }
    
    }
    
  • 相关阅读:
    二叉树的存储方式以及递归和非递归的三种遍历方式
    java基础04 匿名内部类
    jvm007 jvm知识点总览
    jvm学习006 jvm内存结构分配
    java基础03 位运算符
    java基础02 数据类型转
    jvm005 从jvm的角度谈谈线程的实现
    Arcgis投影变换后图像变深的问题
    win 7 64位如何安装erdas 9.2
    Win7 64bit 成功安装ArcView3.X
  • 原文地址:https://www.cnblogs.com/juddy/p/13568970.html
Copyright © 2020-2023  润新知