美文网首页
shiro学习笔记

shiro学习笔记

作者: wch853 | 来源:发表于2017-12-16 09:58 被阅读55次

    权限管理

    为了实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问且只能访问自己被授权的资源。

    用户认证

    为了验证用户访问系统的合法性。

    用户授权

    在用户认证通过后,只能访问被系统授权的资源,授权过程可以理解为who对what(which)进行how操作

    关键对象

    • subject:主体
      访问系统资源的对象,权限管理需要对主体进行身份认证
    • principal:身份信息
      身份信息通常是唯一的,一个主体可能会有多个身份信息,都有一个主身份信息(primary principal)
    • credential:凭证信息
      密码、证书等,主体在进行身份认证时需要身份信息和凭证信息
    • resource 资源
      必须具备相应权限才可以访问的对象
    • permission 权限/许可
      主体需要相应权限才能访问、操作相应资源

    权限模型

    • 主体:账户、密码
    • 角色:角色名称
    • 权限:权限名称、资源名称、资源访问地址
    • 主体与角色的关系
    • 角色与权限的关系

    权限控制

    基于角色的访问控制

    RBAC(Role Based Access Control),基于角色的访问控制

    基于资源的访问控制

    RBAC(Resource Based Access Control),基于资源的访问控制

    权限粒度

    • 粗粒度权限管理:对资源类型的权限管理
    • 细粒度权限管理:对资源实例的权限管理

    shiro架构

    shiro架构
    • Subject:主体
    • SecurityManager:安全管理器,进行主体的认证和授权
    • Authenticator:用户认证管理器
    • Authorizer:权限管理器
    • SessionManager:web应用中一般是用web容器对session管理,shiro也提供一套管理session的方式
    • SessionDao:对Session进行CRUD操作(可与redis集成管理session数据)
    • CacheManager:缓存管理器,主要对session和授权数据进行缓存
    • Cryptography:加密方式
    • Realm:存取认证、授权相关数据(逻辑)
    shiro缓存

    当需要访问受限资源时,会实时去查询权限数据,这样的查询是频繁的,而权限信息又不是经常变化的,所以需要配置缓存来提高性能。
    缓存带来的问题:当用户不退出系统(正常退出、非正常退出),是不会清空缓存的,如果权限发生变更,不能及时改变用户所拥有的权限。

    shiro会话

    shiro支持通过SessionManager取代web容器来管理会话,可以通过配置SessionDao(对Session的CRUD)集成Reis集群来对session进行共享、更新、删除。

    使用Spring集成Shiro

    数据库设计
    DROP TABLE IF EXISTS users;
    CREATE TABLE users (
      id       INT          NOT NULL AUTO_INCREMENT
      COMMENT '用户编号',
      name     VARCHAR(255) NOT NULL
      COMMENT '用户名称',
      username VARCHAR(255) NOT NULL
      COMMENT '账号',
      password VARCHAR(255) NOT NULL
      COMMENT '密码',
      salt     VARCHAR(255) NOT NULL
      COMMENT '盐',
      status   TINYINT      NOT NULL DEFAULT 1
      COMMENT '用户状态 0-无效,1-有效',
      PRIMARY KEY (id),
      UNIQUE KEY (username)
    )
      ENGINE = INNODB
      DEFAULT CHARSET = utf8
      COMMENT = '用户';
    
    DROP TABLE IF EXISTS roles;
    CREATE TABLE roles (
      id        INT          NOT NULL AUTO_INCREMENT
      COMMENT '角色编号',
      role_name VARCHAR(255) NOT NULL
      COMMENT '角色名称',
      PRIMARY KEY (id)
    )
      ENGINE = INNODB
      DEFAULT CHARSET = utf8
      COMMENT = '角色';
    
    DROP TABLE IF EXISTS permission;
    CREATE TABLE permission (
      id       INT          NOT NULL AUTO_INCREMENT
      COMMENT '权限编号',
      url      VARCHAR(255) NOT NULL
      COMMENT 'url地址',
      url_name VARCHAR(255) NOT NULL
      COMMENT 'url描述',
      perm     VARCHAR(255) NOT NULL
      COMMENT '权限标识符',
      PRIMARY KEY (id)
    )
      ENGINE = INNODB
      DEFAULT CHARSET = utf8
      COMMENT = '权限';
    
    DROP TABLE IF EXISTS user_roles;
    CREATE TABLE user_roles (
      user_id INT NOT NULL
      COMMENT '用户编号',
      role_id INT NOT NULL
      COMMENT '角色编号',
      PRIMARY KEY (user_id, role_id)
    )
      ENGINE = INNODB
      DEFAULT CHARSET = utf8
      COMMENT = '用户-角色';
    
    DROP TABLE IF EXISTS role_permissions;
    CREATE TABLE role_permissions (
      role_id       INT NOT NULL
      COMMENT '角色编号',
      permission_id INT NOT NULL
      COMMENT '权限编号',
      PRIMARY KEY (role_id, permission_id)
    )
      ENGINE = INNODB
      DEFAULT CHARSET = utf8
      COMMENT = '角色-权限';
    
    依赖

    除了基本的Spring依赖,还需要shiro-spring、shiro-cache、aspectj。

            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
    
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-ehcache</artifactId>
                <version>1.4.0</version>
            </dependency>
    
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.8.11</version>
            </dependency>
    
    spring-shiro配置文件
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!-- shiroFilter -->
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <property name="securityManager" ref="securityManager"/>
            <!-- 登录地址(登录页面地址,不拦截,登录失败跳回该页) -->
            <property name="loginUrl" value="/"/>
            <!-- 成功登录跳转地址 -->
            <property name="successUrl" value="/home"/>
            <!-- 自定义表单验证filter配置 -->
            <property name="filters">
                <map>
                    <entry key="authc" value-ref="authFormFilter" />
                </map>
            </property>
            <!-- 过滤器链定义,由上往下顺序执行 -->
            <property name="filterChainDefinitions">
                <value>
                    <!-- 设置静态资源匿名访问 -->
                    /resources/** = anon
                    <!-- ajax登录url,不拦截 -->
                    /login = anon
                    <!-- 配置登出url -->
                    /logout = logout
                    <!-- 此处可以配置权限,也可在类或方法上标注
                    /home = authc
                    /query = perms[/query]
                    /add = perms[/add]
                    /update = perms[/update]
                    /delete = perms[/delete]
                    -->
                </value>
            </property>
        </bean>
    
        <!-- securityManager -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <property name="realm" ref="authRealm"/>
            <property name="cacheManager" ref="cacheManager"/>
            <property name="sessionManager" ref="sessionManager"/>
        </bean>
    
        <!-- 配置realm,用于认证、授权 -->
        <bean id="systemRealm" class="com.wch.ssm.shiro.realm.SystemRealm">
            <property name="credentialsMatcher" ref="credentialsMatcher"/>
        </bean>
    
        <!-- 配置凭证匹配器,加密方式和hash次数 -->
        <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="md5"/>
            <property name="hashIterations" value="1"/>
        </bean>
    
        <!-- cacheManager -->
        <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
            <property name="cacheManagerConfigFile" value="classpath:config/shiro-ehcache.xml"/>
        </bean>
    
        <!-- sessionManager -->
        <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
            <!-- 设置session的失效时长 -->
            <property name="globalSessionTimeout" value="600000"/>
            <!-- 删除失效的session -->
            <property name="deleteInvalidSessions" value="true"/>
        </bean>
    
        <!-- 配置自定义表单验证过滤器 -->
        <bean id="authFormFilter" class="com.wch.ssm.shiro.AuthFormFilter"/>
    
    </beans>
    
    自定义Realm
    /**
     * 自定义Realm,用于认证和授权
     */
    public class AuthRealm extends AuthorizingRealm {
    
        @Resource
        private SecurityService securityService;
    
        private static final Logger LOGGER = LoggerFactory.getLogger(AuthRealm.class);
    
        /**
         * 认证
         *
         * @param token token
         * @return AuthenticationInfo
         * @throws AuthenticationException AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            UsernamePasswordToken upToken = (UsernamePasswordToken) token;
            String username = upToken.getUsername();
            User user = securityService.getPasswordAndSalt(username);
    
            if (null == user) {
                throw new UnknownAccountException("不存在该账户!");
            }
    
            String name = user.getName();
            String password = user.getPassword();
            String salt = user.getSalt();
            if (null == name || null == password || null == salt) {
                throw new AccountException("账户异常!");
            }
    
            // 身份信息,密码(数据库中加密后的密码),salt,realmName
            return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
        }
    
        /**
         * 授权
         *
         * @param principals principals
         * @return AuthorizationInfo
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            SimpleAuthorizationInfo info = null;
            try {
                // 获取身份信息
                User user = (User) principals.getPrimaryPrincipal();
                // 查询权限信息
                Set<String> permissions = securityService.getStringPermissions(user.getId());
                info = new SimpleAuthorizationInfo();
                info.addStringPermissions(permissions);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
            return info;
        }
    
        /**
         * 用户权限发生变动,调用此方法清除缓存
         */
        public void clearCache() {
            PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
            super.clearCache(principals);
        }
    }
    
    控制器
        /**
         * 验证登录
         *
         * @return json data
         * @throws ShiroException ShiroException
         */
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public @ResponseBody
        Result login(String username, String password) {
            try {
                UsernamePasswordToken token = new UsernamePasswordToken(username, password);
                // 登录失败:包括账户不存在、密码错误等,都会抛出ShiroException
                SecurityUtils.getSubject().login(token);
                return Result.response(ResultEnum.SUCCESS);
            } catch (ShiroException e) {
                LOGGER.error("登录失败,{},{}", e.getClass().getName(), e.getMessage());
                return Result.response(ResultEnum.FAIL);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
                return Result.response(ResultEnum.FAIL);
            }
        }
    
        /**
         * successUrl
         * 使用注解 @RequiresAuthentication 来标注该访问该url需要认证
         *
         * @param model model
         * @return Page
         */
        @RequestMapping("/home")
        @RequiresAuthentication
        public String home(Model model) {
            // 获取在身份认证时放入的身份信息
            User user = (User) SecurityUtils.getSubject().getPrincipal();
            model.addAttribute("name", user.getName());
            return "home";
        }
    
        /**
         * unauthorizedUrl,未授权时跳转该url
         *
         * @return json
         */
        @ExceptionHandler(UnauthorizedException.class)
        @RequiresAuthentication
        public @ResponseBody
        String forbidden() {
            return "403";
        }
    
        /**
         * 使用 @RequiresPermissions 注解来标注访问该url需要 "user:query" 权限
         *
         * @return json
         */
        @RequestMapping("/query")
        @RequiresPermissions("user:query")
        public @ResponseBody
        String query() {
            return "permit query.";
        }
    
    登录交互
    <script type="text/javascript">
        $('#submit').click(function () {
            $.ajax({
                url: 'login',
                type: 'POST',
                data: {
                    username: $('#username').val().trim(),
                    password: $('#password').val().trim()
                },
                success: function (res) {
                    if (res.code === 200) {
                        window.location.href = 'home'
                    } else {
                        alert("Login Failed!");
                    }
                }
            });
        });
    </script>
    

    使用SpringBoot集成shiro

    配置ShiroConfig

    对于需要配置权限的url,每个都配置注解是很不方便的,可以通过应用启动时查询持久化到数据库中的权限配置来生成拦截器链。
    ShiroConfig加载到容器中时,查询权限的Service可能还未注入,导致空指针异常。因此在ShiroConfig中应使用手动注入的方式来获取查询权限Service。

    获取ApplicationContext

    为了获取ApplicationContext,ShiroConfig需要实现ApplicationContextAware接口,实现setApplicationContext()方法。

        private ApplicationContext context;
    
        /**
         * 获取ApplicationContext
         *
         * @param applicationContext applicationContext
         * @throws BeansException BeansException
         */
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
    
    适配权限标识

    在自定义Realm中重写的doGetAuthorizationInfo()方法,返回类型SimpleAuthorizationInfo,添加权限的方式是通过ddStringPermissions(Collection<String> permissions)添加权限的字符串形式,例如sys:add,但是在拦截器链中配置权限的要求是perms[sys:add]的形式,因此需要对权限标识进行适配。

         /**
         * 适配拦截器权限标识符
         *
         * @param perm perm
         * @return perms[]
         */
        private String adaptPerms(String perm) {
            StringBuilder sb = new StringBuilder();
            sb.append("perms[").append(perm).append("]");
            return sb.toString();
        }
    
    配置拦截器链
        // 拦截器链,由上到下顺序执行
        Map<String, String> filterChain = new LinkedHashMap<>();
    
        // 动态添加权限
        SecurityService securityService = null;
        while (securityService == null) {
            securityService = (SecurityService) context.getBean("securityServiceImpl");
        }
        List<Permission> permissions = securityService.getPermissions();
        for (Permission permission : permissions) {
          filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
        }
    
    完整配置
    @Configuration
    public class ShiroConfig implements ApplicationContextAware {
    
        private ApplicationContext context;
    
        /**
         * 配置realm,用于认证、授权
         *
         * @return Realm
         */
        @Bean
        public Realm authRealm() {
            // 凭证匹配器,配置加密方式和hash次数
            HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(CommonConstants.HASH_CREDENTIAL_NAME);
            credentialsMatcher.setHashIterations(CommonConstants.HASH_ITERATIONS);
    
            AuthRealm authRealm = new AuthRealm();
            authRealm.setCredentialsMatcher(credentialsMatcher);
            return authRealm;
        }
    
        /**
         * 配置EhCache缓存管理器,用于授权信息缓存
         *
         * @return CacheManager
         */
        private CacheManager getEhCacheManager() {
            EhCacheManager cacheManager = new EhCacheManager();
            cacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
            return cacheManager;
        }
    
        /**
         * 配置SecurityManager
         *
         * @return SecurityManager
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(authRealm());
            securityManager.setCacheManager(getEhCacheManager());
            return securityManager;
        }
    
        /**
         * 设置由servlet容器管理filter生命周期
         *
         * @return LifecycleBeanPostProcessor
         */
        @Bean
        public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         * 开启aop,对类代理
         *
         * @return Proxy
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        /**
         * 开启shiro注解支持
         *
         * @return Advisor
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager());
            return advisor;
        }
    
        /**
         * 配置shiroFilter,beanName必须为shiroFilter
         *
         * @return ShiroFilter
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilter() {
            ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
            // 配置SecurityManager
            filter.setSecurityManager(securityManager());
            // 配置登录页
            filter.setLoginUrl("/");
            // 登录成功跳转链接
            filter.setSuccessUrl("/sys");
            // 未授权界面
            filter.setUnauthorizedUrl("/403");
            // 拦截器链,由上到下顺序执行
            Map<String, String> filterChain = new LinkedHashMap<>();
            // 配置ajax登录url匿名访问
            filterChain.put("/login", "anon");
            // 配置登出路径
            filterChain.put("/logout", "logout");
            // 静态资源处理
            filterChain.put("/js/**", "anon");
            filterChain.put("/css/**", "anon");
            filterChain.put("/img/**", "anon");
    
            // 动态添加权限
            SecurityService securityService = null;
            while (securityService == null) {
                securityService = (SecurityService) context.getBean("securityServiceImpl");
            }
            List<Permission> permissions = securityService.getPermissions();
            for (Permission permission : permissions) {
                filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
            }
    
            // 认证后访问
            filterChain.put("/**", "authc");
            filter.setFilterChainDefinitionMap(filterChain);
            return filter;
        }
    
        /**
         * 适配拦截器权限标识符
         *
         * @param perm perm
         * @return perms[]
         */
        private String adaptPerms(String perm) {
            StringBuilder sb = new StringBuilder();
            sb.append("perms[").append(perm).append("]");
            return sb.toString();
        }
    
        /**
         * 获取ApplicationContext
         *
         * @param applicationContext applicationContext
         * @throws BeansException BeansException
         */
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
    }
    

    相关文章

      网友评论

          本文标题:shiro学习笔记

          本文链接:https://www.haomeiwen.com/subject/ueclbxtx.html