shiro框架的使用及擴展

jopen 10年前發布 | 110K 次閱讀 Shiro 安全相關

       shiro的優點是:相對Spring Security較為輕巧,使用起來自由度大,和Spring框架結合的方式也很成熟。缺點是:shiro本身沒實現緩存,需要自己定義緩存實現,更新比較慢,有的功能需要自己拓展。

        shiro文檔:http://shiro.apache.org/static/1.2.3/apidocs/  

        十分鐘入門:http://shiro.apache.org/10-minute-tutorial.html

        以下總結在項目中使用shiro的方法和管理后臺項目中對shiro的拓展。

 

一、使用shiro管理權限

        1. 引入shiro需要的包。使用maven的項目中,在pom.xml增加以下依賴:

               <!-- shiro權限管理 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!-- shiro權限管理 end -->

       2. 在項目中增加shiro配置。

         在spring配置文件目錄下新建spring-shiro.xml。內容如下:

  <!-- Shiro權限管理配置 -->
    <bean id="shiroFilter"
        class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- shiro通過一個filter控制權限-->
         <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login/execute.do" />  <!-- 登陸頁 -->
         <property name="successUrl" value="/index.jsp" />  <!-- 登陸成功之后跳轉的頁面 -->
        <property name="unauthorizedUrl" value="/login/execute.do" /> <!-- 用戶在請求無權限的資源時,跳轉到這個url -->
        <property name="filters">
            <util:map>
                 <entry key="authc">
                      <bean class="org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter" />
                </entry>
              </util:map>
         </property>
         <property name="filterChainDefinitions"> <!-- 配置訪問url資源需要用戶擁有什么權限 配置的優先級由上至下-->
             <value>
                /=anon
                /template/main.jsp=user
                <!-- api用戶信息 -->
                /api/createApiUser**=perms[api:user:create]  <!-- 權限可以用":"分級,如擁有api權限相當于擁有api*權限(父權限涵蓋子權限) -->
                /api/updateApiUser**=perms[api:user:update]
                /api/*User*=perms[api:user:view]
                /template/apiUserManage/**=perms[api:user:view] 
                <!-- api接口管理 -->
                /api/*Interface*=perms[api:user:interface]
                <!-- api統計數據 -->
                /api/querySummaryData**=perms[api:data]
                /template/apiSumData/**=perms[api:data]

                         /api/**=perms[api:*]
                                ...

                 /**=anon
             </value>
         </property>
     </bean>
     <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="permissionsRealm" />  <!-- 自定義登陸及獲取權限的源 -->
     </bean>

    <!-- shiro權限管理配置 end -->
       在web.xml中增加shiro filter的配置:

   <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath*:applicationContext.xml,
            classpath*:spring-*.xml //此處引入了spring-shiro
        </param-value>
    </context-param>
    ...
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    </filter>

    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

     3.  定義登陸及獲取權限的源。

/**
 * 認證實現類
 * 
 * @author kexm
 * 
 */
@Service("permissionsRealm")
public class PermissionsRealm extends AuthorizingRealm {
    @Autowired
    private AccountDao accountDao;

    @Autowired
    private GroupDao groupDao;

    private Account acc;

    private static LogUtil log = LogUtil.getLogger(PermissionsRealm.class);

    /**
     * 用戶權限源(shiro調用此方法獲取用戶權限,至于從何處獲取權限項,由我們定義。)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        log.info("method[doGetAuthorizationInfo] begin.");
        if (acc != null) {
            if(acc.getAdminType() == 2){//超級管理員 始終擁有所有權限
                info.addStringPermission("*");
                return info;
            }
            try {
                List<UserGroup> gList = accountDao.getUserGroups(acc.getLoginName());
                for (UserGroup g: gList) { //獲取用戶的組
                    log.info("method[doGetAuthorizationInfo] group<" + g.getName() + ">");
                    List<Permission> pList = groupDao.getGroupPerms(g.getId());
                    for (Permission p: pList) { //獲取組內權限
                        log.info("method[doGetAuthorizationInfo] perm<" + p.getName() + "," + p.getPermList() + ">");
                        String permList = p.getPermList();
                        if (permList != null && !"".equals(permList)) {
                            String[] perms = p.getPermList().split(",");
                            for (String perm: perms) {//分別放入容器   (權限以字符串形式呈現,如"api:data"等,和spring-shiro.xml中的配置相對應)
                                log.info("method[doGetAuthorizationInfo] add perm<" + perm + ">");
                                info.addStringPermission(perm);
                            }
                        }
                    }
                }
                return info;//將用戶權限返回給shiro
            } catch (Exception e) {
                log.error("method[doGetAuthorizationInfo] e.message<" + e.getMessage() + "> e<" + e + ">", e);
            }
        }
        return null;
    }

    /**
     * 用戶登錄驗證源(shiro調用此方法執行認證)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authtoken) throws AuthenticationException {
        log.info("method[doGetAuthenticationInfo] begin.");
        UsernamePasswordToken token = (UsernamePasswordToken) authtoken;
        SimpleAuthenticationInfo authenticationInfo = null;
        String userName = token.getUsername();
        String password = new String(token.getPassword());
        Login conf = DefaultConfigure.config.getLogin();
        String MD5pwd = MD5Util.generateSignature(conf.getSalt(), password);
        try {
            if (userName != null && !"".equals(userName)) {
                acc = accountDao.login(userName, MD5pwd);
            }
            if (acc != null) {
                doGetAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
                authenticationInfo = new SimpleAuthenticationInfo(token.getUsername(), token.getPassword(), getName());
                return authenticationInfo;
            }
        } catch (Exception e) {
            log.error("method[doGetAuthenticationInfo] acc<" + acc + "> message<" + e.getMessage() + "> e<" + e + ">",
                e);
        }
        return null;
    }

}

      3.  shiro中,使用subject管理用戶。可以把subject理解為shiro存儲用戶信息的容器和操縱用戶的工具。有了前幾步的配置,便可以使用以下代碼登入登出,并享受shiro的url權限控制了。

        //登入
        UsernamePasswordToken token = new UsernamePasswordToken(loginName, password);
        Subject user = SecurityUtils.getSubject();
        user.login(token);
        //使用shiro自帶的session存儲用戶信息 獨立于httpSession
        Session ss = user.getSession().setAttribute("userInfo", acc);
        //登出
        SecurityUtils.getSubject().logout();

      4. 在頁面中使用shiro標簽。假如我們要讓有權限的用戶看到某些菜單或按鈕,可以用以下方式。

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:hasPermission name="api:data">
        who has permission can see
</shiro:hasPermission>

         

      以上只使用了shiro的permission管理,shiro還支持對role的管理,如有進一步抽象的需求可以使用。


二、分布式環境,讓shiro與redis結合

        假如我們的web項目是分布式部署的,則需要讓shiro把session和用戶權限到放到集中緩存上去。Shiro本身不實現Cache,但是提供了接口,方便更換不同的底層Cache實現。

shiro提供的cache接口:

 

    public interface Cache<K, V> {  
        //根據Key獲取緩存中的值  
        public V get(K key) throws CacheException;  
        //往緩存中放入key-value,返回緩存中之前的值  
        public V put(K key, V value) throws CacheException;   
        //移除緩存中key對應的值,返回該值  
        public V remove(K key) throws CacheException;  
        //清空整個緩存  
        public void clear() throws CacheException;  
        //返回緩存大小  
        public int size();  
        //獲取緩存中所有的key  
        public Set<K> keys();  
        //獲取緩存中所有的value  
        public Collection<V> values();  
    }  

    觀察接口可以發現,我們需要實現一個keys方法。這個方法限制了shiro不能使用緩存集群(SharedRedis不提供這個方法,只有單臺redis可用keys方法,望找到解決方案)。

 

    我們的項目使用redis作為集中緩存,shiro和redis結合的方式可以使用一個現成的工具——shiro-redis。

    shiro-redis的github:https://github.com/alexxiyang/shiro-redis

    目前管理后臺項目正在使用此工具。這個工具有一處問題:讀取緩存時沒對讀取的對象延長有效期,修復這個BUG之后還挺好用。


三、對shiro頁面標簽拓展,增加and or not 邏輯符

    參考:http://jinnianshilongnian.iteye.com/blog/1864800

     使用過shiro的朋友應該都知道在要想實現any permission的驗證是比較麻煩。

     很多朋友剛開始接觸時以為如<shiro:hasPermission name="showcase:tree:*"> 代表驗證用戶是否擁有tree下的任何權限,但這是錯誤的。如果我們把showcase:tree:*授權給用戶,那么此時表示用戶具有showcase:tree資源的任意權限,如<shiro:hasPermission name="showcase:tree:*">或shiro:hasPermission name="showcase:tree:create">都能驗證成功。

     還有朋友認為<shiro:hasPermission name="showcase:tree:create,update"> 是或的關系,也不是,默認是且的關系。

     下載了最新的shiro1.3.0-SNAPSHOT 發現并沒有增加新的標簽或其他支持。

    因此我們需要簡單的擴展下shiro來支持像spring security 3那樣的@Secured支持表達式的強大注解。
我們擴展AuthorizingRealm,并修改:

    private static final String OR_OPERATOR = " or ";
    private static final String AND_OPERATOR = " and ";
    private static final String NOT_OPERATOR = "not ";
    /**
     * 支持or and not 關鍵詞  不支持and or混用
     * @param principals
     * @param permission
     * @return
     */
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        if(permission.contains(OR_OPERATOR)) {
            String[] permissions = permission.split(OR_OPERATOR);
            for(String orPermission : permissions) {
                if(isPermittedWithNotOperator(principals, orPermission)) {
                    return true;
                }
            }
            return false;
        } else if(permission.contains(AND_OPERATOR)) {
            String[] permissions = permission.split(AND_OPERATOR);
            for(String orPermission : permissions) {
                if(!isPermittedWithNotOperator(principals, orPermission)) {
                    return false;
                }
            }
            return true;
        } else {
            return isPermittedWithNotOperator(principals, permission);
        }
    }

    private boolean isPermittedWithNotOperator(PrincipalCollection principals, String permission) {
        if(permission.startsWith(NOT_OPERATOR)) {
            return !super.isPermitted(principals, permission.substring(NOT_OPERATOR.length()));
        } else {
            return super.isPermitted(principals, permission);
        }
    }

     如上代碼即可以實現簡單的NOT、AND、OR支持,不過缺點是不支持復雜的如AND、OR組合。

     如下標簽在拓展后可以生效:

<shiro:hasPermission name="api:data or api:user"></shiro:hasPermission>
<pre name="code" class="html"><shiro:hasPermission name="api:data and api:user"></shiro:hasPermission> 
<shiro:hasPermission name="not api:data"></shiro:hasPermission>
來自:http://blog.csdn.net/hereiskxm/article/details/41825043

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!