shiro实现方法级别的细粒度url权限控制

项目源码:https://github.com/weimingge14/Shiro-project
演示地址:http://liweiblog.duapp.com/Shiro-project/login

关于 Shiro 的权限匹配器和过滤器

上一节,我们实现了自定义的 Realm,方式是继承 AuthorizingRealm 这个抽象类,分别实现认证的方法和授权的方法。

这一节实现的代码的执行顺序:
1、Shiro定义的过滤器和自定义的过滤器,在自定义的过滤器中执行 Subject 对象的判断是否具有某项权限的方法 isPermitted() ,传入某一个跟当前登录对象相关的特征值(这里是登录对象正在访问的 url 连接)
2、程序走到自定义的 Realm 中的授权方法中,根据已经认证过的主体查询该主体具有的角色和权限字符串,通常情况下是一个角色的集合和一个权限的集合。

此时我们第 1 步有一个字符串,第 2 步有一个字符串的集合(权限的集合)。
程序要帮我们做的就是看第 1 步的字符串在不在第 2 步的字符串集合中。那么这件事情是如何实现的呢?

3、此时程序检测到配置文件中有声明一个实现了 PermissionResolver 的类,这个时候程序就会到这个类中去查找所采用的权限匹配策略。
4、到上一步返回的实现了 Permission 的类的对象中的 implies() 方法中去进行判断。如果第 2 步的权限字符串数量多于 1 个,这个 implies() 就会执行多次,直到该方法返回 true 为止,第 1 步的 isPermitted() 才会返回 true。

下面我们来关注一下 [urls] 这个节点下面的部分。

[urls]
# 配置 url 与使用的过滤器之间的关系
/admin/**=authc,resourceCheckFilter
/login=anon
   
   
  • 1
  • 2
  • 3
  • 4

其中

/admin/**=authc,resourceCheckFilter
   
   
  • 1

表示,当请求 /admin/** 的时候,会依次经过 (1)authc 和 (2)resourceCheckFilter 这两个过滤器。

过滤器在有些地方也叫拦截器,他们的意思是一样的。

(1)authc 这个过滤器是 Shiro 自定义的认证过滤器,即到自定义 Realm 的认证方法里面去按照指定的规则进行用户名和密码的匹配。
DefaultFilter 这个枚举类里面定义了多个自定义的过滤器,可以直接使用。

(2)resourceCheckFilter 是一个自定义的过滤器,我们来看看它的声明:

[filters]
# 声明一个自定义的过滤器
resourceCheckFilter = com.liwei.shiro.filter.ResourceCheckFilter
# 为上面声明的自定义过滤器注入属性值
resourceCheckFilter.errorUrl=/unAuthorization
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

实现:

public class ResourceCheckFilter extends AccessControlFilter {

    private String errorUrl;

    public String getErrorUrl() {
        return errorUrl;
    }

    public void setErrorUrl(String errorUrl) {
        this.errorUrl = errorUrl;
    }

    private static final Logger logger = LoggerFactory.getLogger(ResourceCheckFilter.class);

    /**
     * 表示是否允许访问 ,如果允许访问返回true,否则false;
     * @param servletRequest
     * @param servletResponse
     * @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        String url = getPathWithinApplication(servletRequest);
        logger.debug("当前用户正在访问的 url => " + url);
        return subject.isPermitted(url);
    }


    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回 true 表示需要继续处理;如果返回 false 表示该拦截器实例已经处理了,将直接返回即可。

     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        logger.debug("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");

        HttpServletRequest request =(HttpServletRequest) servletRequest;
        HttpServletResponse response =(HttpServletResponse) servletResponse;
        response.sendRedirect(request.getContextPath() + this.errorUrl);

        // 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
        return false;
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

注意:我们首先要关注 isAccessAllowed() 方法,在这个方法中,如果返回 true,则表示“通过”,走到下一个过滤器。如果没有下一个过滤器的话,表示具有了访问某个资源的权限。如果返回 false,则会调用 onAccessDenied 方法,去实现相应的当过滤不通过的时候执行的操作,例如跳转到某一个指定的登录页面,去引导用户输入另一个具有更大权限的用户名和密码进行登录。

isAccessAllowed() 方法的最后一个参数 o,可以获得我们自定义的过滤器后面中括号中所带的参数。

我们再跳回到 isAccessAllowed() 中:subject.isPermitted(url)。说明通过继承 AccessControlFilter 我们可以得到认证主体 Subject 和当前请求的 url 链接,它们的 API 分别是:

获得认证主体:

Subject subject = getSubject(servletRequest,servletResponse);
   
   
  • 1


获得当前请求的 url

String url = getPathWithinApplication(servletRequest);
   
   
  • 1

然后,我们调用了 subject.isPermitted(url) 方法,将 url 这个字符串对象传入。

此时我们的流程应该走到 Realm 的授权方法中,通过查询(经过了认证的)用户信息去查询该用户具有的权限信息。此时的代码走到了这里。
在授权方法中,我们看到 SimpleAuthorizationInfo 的角色信息和权限信息都是通过字符串来解析的。
角色信息和权限信息都是集合。

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    logger.info("--- MyRealm doGetAuthorizationInfo ---");

    // 获得经过认证的主体信息
    User user = (User)principalCollection.getPrimaryPrincipal();

    //
    // 此处为节约篇幅,突出重点省略
    //

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.setRoles(new HashSet<>(roleSnList));
    info.setStringPermissions(new HashSet<>(resStrList));

    // 以上完成了动态地对用户授权
    logger.debug("role => " + roleSnList);
    logger.debug("permission => " + resStrList);

    return info;
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这个 Realm 的授权方法中,完成了对该认证后的主体所具有的角色和权限的查询,然后放入 SimpleAuthorizationInfo 对象中。

接下来就要进行 subject.isPermitted(url) 中的 url 和 自定义 Realm 中的授权方法中的 info.setStringPermissions(new HashSet<>(resStrList)); 权限字符串集合的匹配操作了。

权限信息:
这里写图片描述
它们都是从数据库中查询出来的。

那么如何实现匹配呢?比较简单的一个思路就是比较字符串,但是这件简单的比较的事情被 Shiro 定义为一个 PermissionResolver ,通过实现 PermissionResolver ,我们可以为完成自定义的权限匹配操作,可以是简单的字符串匹配,也可以稍有灵活性的通配符匹配,这都取决于我们程序员自己。

public class UrlPermissionResolver implements PermissionResolver {

    private static final Logger logger = LoggerFactory.getLogger(UrlPermissionResolver.class);

    /**
     * 经过调试发现
     * subject.isPermitted(url) 中传入的字符串
     * 和自定义 Realm 中传入的权限字符串集合都要经过这个 resolver
     * @param s
     * @return
     */
    @Override
    public Permission resolvePermission(String s) {
        logger.debug("s => " + s);

        if(s.startsWith("/")){
            return new UrlPermission(s);
        }
        return new WildcardPermission(s);
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

可以看到,权限信息是通过字符串:“/admin/**”等来进行匹配的。这时就不能使用 Shiro 默认的权限匹配器 WildcardPermission 了。

而 UrlPermission 是一个实现了 Permission 接口的类,它的 implies 方法的实现决定了权限是否匹配,所以 implies 这个方法的实现是很重要的。

public class UrlPermission implements Permission {

    private static final Logger logger = LoggerFactory.getLogger(UrlPermission.class);

    // 在 Realm 的授权方法中,由数据库查询出来的权限字符串
    private String url;

    public UrlPermission(String url){
        this.url = url;
    }

    /**
     * 一个很重要的方法,用户判断 Realm 中设置的权限和从数据库或者配置文件中传递进来的权限信息是否匹配
     * 如果 Realm 的授权方法中,一个认证主体有多个权限,会进行遍历,直到匹配成功为止
     * this.url 是在遍历状态中变化的
     *
     * urlPermission.url 是从 subject.isPermitted(url)
     * 传递到 UrlPermissionResolver 中传递过来的,就一个固定值
     *
     * @param permission
     * @return
     */
    @Override
    public boolean implies(Permission permission) {
        if(!(permission instanceof UrlPermission)){
            return false;
        }
        //
        UrlPermission urlPermission = (UrlPermission)permission;
        PatternMatcher patternMatcher = new AntPathMatcher();

        logger.debug("this.url(来自数据库中存放的通配符数据),在 Realm 的授权方法中注入的 => " + this.url);
        logger.debug("urlPermission.url(来自浏览器正在访问的链接) => " +  urlPermission.url);
        boolean matches = patternMatcher.matches(this.url,urlPermission.url);
        logger.debug("matches => " + matches);
        return matches;
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

重点说明:如果在自定义的 Realm 中的授权方法中传入的授权信息中的权限信息是一个集合,那么这里的 implies 就会进行遍历,直到这个方法返回 true 为止,如果遍历的过程全部返回 false,就说明该认证主体不具有访问某个资源的权限。

shiro-spring配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <!-- 声明一个密码匹配器 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!-- 设置该密码匹配器使用的算法是 md5 -->
        <property name="hashAlgorithmName" value="md5"/>
    </bean>

    <!-- 声明一个自定义的 Realm -->
    <bean id="myRealm" class="com.liwei.shiro.realm.MyRealm">
        <!-- 将上面声明的密码匹配器注入到自定义 Realm 的属性中去 -->
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
        <!-- 将自定义的权限匹配器注入到自定义 Realm 中 -->
        <property name="permissionResolver" ref="permissionResolver"/>

        <!-- 配置缓存相关 -->
        <!-- 启用缓存 -->
        <property name="cachingEnabled" value="true"/>
        <!-- 开启认证缓存-->
        <property name="authenticationCachingEnabled" value="true"/>
        <!-- 指定认证缓存的名字(与 ehcache.xml 中声明的相同) -->
        <property name="authenticationCacheName" value="shiro-authenticationCache"/>
        <!--开启授权缓存-->
        <property name="authorizationCachingEnabled" value="true"/>
        <!-- 指定授权缓存的名字(与 ehcache.xml 中声明的相同) -->
        <property name="authorizationCacheName" value="shiro-authorizationCache"/>

    </bean>

    <!-- 自定义一个权限匹配器 -->
    <bean id="permissionResolver" class="com.liwei.shiro.permission.UrlPermissionResolver"/>



    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 如果认证不通过,浏览器通过 Get 方式请求到 /login 上 -->
        <property name="loginUrl" value="/login"/>

        <!-- override these for application-specific URLs if you like:

        <property name="successUrl" value="/home.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
        <!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean  -->
        <!-- defined will be automatically acquired and available via its beanName in chain        -->
        <!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
        <!-- <property name="filters">
            <util:map>
                <entry key="anAlias" value-ref="someFilter"/>
            </util:map>
        </property> -->
        <property name="filterChainDefinitions">
            <value>
                /admin/**=authc,resourceCheckFilter
                /login=anon
            </value>
        </property>
    </bean>

    <!-- 声明一个自定义的过滤器 -->
    <bean id="resourceCheckFilter" class="com.liwei.shiro.filter.ResourceCheckFilter">
        <!-- 为上面声明的自定义过滤器注入属性值 -->
        <property name="errorUrl" value="/unAuthorization"/>
    </bean>

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- Single realm app.  If you have multiple realms, use the 'realms' property instead. -->
        <!-- 设置安全管理器的安全数据源为自定义的 Realm -->
        <property name="realm" ref="myRealm"/>
        <!-- By default the servlet container sessions will be used.  Uncomment this line
             to use shiro's native sessions (see the JavaDoc for more): -->
        <!-- <property name="sessionMode" value="native"/> -->

        <property name="cacheManager" ref="ehCacheManager"/>
    </bean>
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>


    <!-- 配置 shiro 的 ehcache 缓存相关,这个缓存只和 Realm 相关 -->
    <bean id="ehCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"></bean>

    <!-- 配置 Spring 的 EhCacheManagerFactoryBean ,须要 spring-context-support 的支持 -->
    <bean id="ehCacheManagerFactoryBean" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 配置 Spring 的 EhCacheCacheManager,须要 spring-context-support 的支持 -->
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehCacheManagerFactoryBean"/>
    </bean>

</beans>

猜你喜欢

转载自blog.csdn.net/zzzgd_666/article/details/80475707