zoukankan      html  css  js  c++  java
  • 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();
            }
        }
    
    }
    
  • 相关阅读:
    January 25th, 2018 Week 04th Thursday
    January 24th, 2018 Week 04th Wednesday
    January 23rd, 2018 Week 04th Tuesday
    January 22nd, 2018 Week 04th Monday
    January 21st, 2018 Week 3rd Sunday
    January 20th, 2018 Week 3rd Saturday
    January 19th, 2018 Week 3rd Friday
    January 18th, 2018 Week 03rd Thursday
    January 17th, 2018 Week 03rd Wednesday
    January 16th, 2018 Week 03rd Tuesday
  • 原文地址:https://www.cnblogs.com/juddy/p/13568970.html
Copyright © 2011-2022 走看看