Spring MVC 中使用 Apache Shiro 安全框架詳解

GitBook發表於2017-01-05

我們在這裡將對一個整合了Spring MVC+Hibernate+Apache Shiro的專案進行了一個簡單說明。這個專案將展示如何在Spring MVC 中使用Apache Shiro來構建我們的安全框架。

閱讀文章前,您需要做以下準備:

  • Maven 3環境
  • Mysql-5.6+
  • JDK1.7+
  • git環境
  • git.oschina.net帳號
  • Apache Tomcat 7+
  • 您熟練掌握的編輯工具,推薦使用InterlliJ IDEA 14+

開始

專案地址git.oschina.net

專案地址github.com

安全管理框架資料結構

首先,我們在mysql資料庫中建立schema,命名為shirodemo。我們在建立兩個使用者shiroDemo@localhostshiroDemo@%,這裡我們將使用者的密碼簡單設定成123456。

然後,我們將專案從Git伺服器上clone到本地後,我們可以在專案根目錄下的resources中發現db.sql檔案。這個檔案是專案的資料庫結構檔案,你可以將db.sql匯入到資料庫shirodemo中。

我們這裡的許可權結構設計比較簡單,我們以表格的形式說明主要資料庫結構:

**Table:t_user**
Name Type Length Describ
id int 11 使用者表的主鍵
password varchar 255 密碼
username varchar 255 使用者名稱,全域性唯一,shiro將使用使用者名稱來鎖定安全資料中的使用者資料。

**Table:t_role**
Name Type Length Describ
id int 11 主鍵
rolename varchar 255 角色名稱,全域性唯一。shiro將通過角色名來進行鑑權

**Table:t_permission**
Name Type Length Describ
id int 11 主鍵
role_id int 11 關聯role的外來鍵
dataDomain varchar 255 系統資料模型的域(自己定義的概念,下面我們將會介紹)
dataType varchar 255 permission對應的系統例項的型別
operation varchar 255 permession許可的操作,例如add,del等等

**Table:t_authc_map**
Name Type Length Describ
id int 11 主鍵
authcType varchar 255 驗證型別,列舉:anon,authc,perms,roles
url varchar 255 系統資源的url
val varchar 255 具體的許可權字串,例如:user:query

t_user和t_role表就不用詳細介紹了,就是系統的使用者表和角色表。它與t_role角色表的關係是多對多的關係,即一個使用者可以有多個角色,一個角色可以包含多個使用者。

那麼我們介紹一下t_permission表,這個表存放的資料是角色擁有的permission(這裡我們就用shiro的permission概念,不翻譯了。因為翻譯過來是許可,但是許可二字還不能完全闡釋permission的概念)。每一個role會對應一個permission,即一對一的關係。

表t_authc_map儲存的是Shiro filter需要的配置資料,這些資料組合起來,定義了訪問控制(Access Controll)的規則,即定義了哪些url可以被擁有哪些permission或者擁有哪些role的使用者訪問。在我們這個例子中,其實這張表用處不大。當初設計這樣一張表的目的是,能夠動態管理訪問控制的規則,但是並不能。

提示: 訪問控制規則的資料是在Spring bean初始化時就載入給了訪問控制的filter。我們試想一下,在你的webapp執行時(runtime),我們可以通過一些手段來修改系統的訪問控制規則,那麼勢必會造成使用者提交事務時的處理變得非常複雜。例如,使用者正在訪問一個url連線,我們通過後臺修改了url的訪問控制許可權,這時這個使用者已經提交了一次事務操作,那麼怎麼判斷這次提交是否合法呢?要把這個問題處理清楚就很複雜。那麼你可能會問,如果我為系統增加了一個模組,模組中有一些新建的url需要提供給使用者訪問,但是我不想重啟我的應用,直接在資料庫中配置完成,怎麼辦?我想,既然增加了模組,當然需要重新部署,那麼僅通過配置資料完成部署,我感覺在現在Spring MVC下,很難實現。所以個人看法,訪問控制規則資料使用配置檔案還是持久化到資料庫,沒有什麼區別。但是本文中還是會介紹如何將訪問控制規則持久化到資料庫中。

shiroTest模組

我們可以看見專案中有一個shiroTest模組,這個模組中主要實現在單元測試時,使用的通用程式。在本例中,我們在shiroTest模組中實現一個proxool資料來源,為其他模組在單元測試時提供資料庫連線。

請注意,我們這裡配置的資料來源,僅提供給單元測試使用。而我們的webapp中將使用Spring 的JNDI資料來源。為什麼這麼做呢?主要原因是:本例中我們使用的是Tomcat做為中介軟體,但是實際專案的生產環境,可能使用商業中介軟體,例如Weblogic等等。那麼我們在遷移過程中,就不用考慮中介軟體使用的是什麼資料來源,只去呼叫中介軟體JNDI上繫結的資料來源名稱就可以了。而且這些商業中介軟體一般都有很好的資料來源管理功能。如果我們使用獨立的資料來源,那麼資料來源就脫離的中介軟體的管理,豈不是功能浪費?

我們在test中,實現一個測試用例,這個測試用例主要測試資料來源的連線:

public void testApp() throws SQLException {
        ApplicationContext cxt = new ClassPathXmlApplicationContext(
                "classpath*:conf/*-beans.xml");
        DataSource ds= (DataSource) cxt.getBean("ds-default");
        Connection con=ds.getConnection();
        con.close();
        assertTrue(true);
}

我們在shiroTest專案根目錄下執行mvn test,測試一下。

base模組

base模組主要實現的是整個專案中,各個模組公用的程式。其中包含了:

  • Hibernate Session Factory
  • Ehcache
  • POJO Class
  • BaseDao 所有dao的父類
  • Hibernte 事務管理

authmgr模組

authmgr模組實現瞭如下功能

  • 登入
  • 登出
  • 查詢訪問控制規則資料
  • 實現自定義Realm
  • 實現Shiro的SecurityManager

authmgr模組業務介面

我們來看一下介面com.ultimatech.shirodemo.authmgr.service.IAuthService

*com.ultimatech.shirodemo.authmgr.service.IAuthService*
public interface IAuthService {
    /**
     * 使用者登入介面
     * @param userName 登入使用者名稱
     * @param password 密碼
     * @throws AuthenticationException
     */
    void logIn(String userName, String password) throws AuthenticationException;

    /**
     * 使用者登出系統
     */
    void logOut();

    /**
     * 獲得資料庫中儲存的訪問控制資料
     * @return
     */
    List<AuthcMap> getFilterChainDefinitions();
}

自定義實現Realm

我們來看一下我們的Realm是如何實現:

*com.ultimatech.shirodemo.authmgr.realm.MyRealm*
......
@Component("myRealm")
public class MyRealm extends AuthorizingRealm {

    @Autowired
    public MyRealm(@Qualifier("shiroEncacheManager") CacheManager cacheManager) {
        super(cacheManager);
    }

    @Autowired
    private IAuthDao dao;

   ......

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //獲取登入時輸入的使用者名稱
        String loginName = (String) principalCollection.fromRealm(getName()).iterator().next();
        //到資料庫查是否有此物件
        User user = this.getDao().findByName(loginName);
        if (user != null) {
            //許可權資訊物件info,用來存放查出的使用者的所有的角色(role)及許可權(permission)
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            //使用者的角色集合
            info.setRoles(user.getRolesName());
            //使用者的角色對應的所有許可權,如果只使用角色定義訪問許可權,下面的四行可以不要
            List<Role> roleList = user.getRoleList();
            for (Role role : roleList) {
                info.addStringPermissions(role.getPermissionsString());
            }
            return info;
        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //UsernamePasswordToken物件用來存放提交的登入資訊
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //查出是否有此使用者
        User user = this.getDao().findByName(token.getUsername());
        if (user != null) {
            //若存在,將此使用者存放到登入認證info中
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
        }
        return null;
    }
}

Shiro的SecurityManager

我們在Spring 容器中宣告一個名叫securityManager的bean。在resources/conf/authmgr-beans.xml中,我們看見如下程式碼:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- ref對應我們寫的realm  myRealm -->
    <property name="realm" ref="myRealm"/>
    <!-- 使用下面配置的快取管理器 -->
    <property name="cacheManager" ref="shiroEncacheManager"/>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

由於我們很多模組都會用到共享快取,所以以上<property name="cacheManager" ref="shiroEncacheManager"/>中的shiroEncacheManager被定義在base模組中。

我們可以去base模組的目錄下找到resources/conf/base-beans.xml,找到如下程式碼:

<bean id="shiroEncacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <property name="cacheManager" ref="ehCacheManager"/>
</bean>

<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml"></property>
    <property name="shared" value="true"/>
</bean>

shiroWebapp模組

shiroWebapp模組是本例中的web應用。主要整合了Spring MVC框架、Hibernate框架,以及我們的安全框架Apache Shiro。

我們使用Shiro Filter來進行訪問控制,那麼在web.xml檔案中進行了如下配置:

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

我們使用Spring的DelegatingFilterProxy來建立Shiro Filter。<filter-name>shiroFilter</filter-name>這個引數要與Spring中Shiro Filter Bean的名字保持一致。在shiroWebapp下的resources/conf下的web-beans.xml檔案中,我們可以看見Shiro Filter的配置:

<bean id="filterChainDefinitions" class="com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions">
    <property name="filterChainDefinitions">
        <value>
            /html/**=anon
            /js/**=anon
            /css/**=anon
            /images/**=anon
            /authc/login=anon
            /login=anon
            <!--/user=perms[user:del]-->
            /user/add=roles[manager]
            /user/del/**=roles[admin]
            /user/edit/**=roles[manager]
            <!--/** = authc-->
        </value>
    </property>
</bean>

<!-- 配置shiro的過濾器工廠類,id- shiroFilter要和我們在web.xml中配置的過濾器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- 呼叫我們配置的許可權管理器 -->
    <property name="securityManager" ref="securityManager"/>
    <!-- 配置我們的登入請求地址 -->
    <property name="loginUrl" value="/"/>
    <!-- 配置我們在登入頁登入成功後的跳轉地址,如果你訪問的是非/login地址,則跳到您訪問的地址 -->
    <property name="successUrl" value="/user"/>
    <!-- 如果您請求的資源不再您的許可權範圍,則跳轉到/403請求地址 -->
    <property name="unauthorizedUrl" value="/html/403.html"/>
    <!-- 許可權配置 -->
    <property name="filterChainDefinitionMap" ref="filterChainDefinitions" />
</bean>

訪問控制資料

我們看見上面的filterChainDefinitions中,我們自定義了一個FacotryBean,這個bean主要實現將配置檔案中的訪問控制資料和資料庫中的訪問控制資料整合在一起。(雖然我們之前已經說了,這兩種方式沒什麼區別。)

*com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions*
public class ShiroFilterChainDefinitions implements FactoryBean<Ini.Section> {

    @Autowired
    private IAuthService authService;

    ......

    public static final String PREMISSION_STRING = "perms[{0}]";

    public static final String ROLE_STRING = "roles[{0}]";

    public Ini.Section getObject() throws Exception {
        List<AuthcMap> list = this.getAuthService().getFilterChainDefinitions();
        Ini ini = new Ini();
        ini.load(this.getFilterChainDefinitions());
        Ini.Section section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        for (AuthcMap map : list) {
            String s = null;
            switch (AuthcType.valueOf(map.getAuthcType())) {
                case roles:
                    s = MessageFormat.format(ROLE_STRING, map.getVal());
                    break;
                case perms:
                    s = MessageFormat.format(PREMISSION_STRING, map.getVal());
                    break;
                case authc:
                    s = AuthcType.authc.name();
                case anon:
                    s = AuthcType.anon.name();
                default:
                    s = AuthcType.authc.name();
            }
            section.put(map.getUrl(), s);
        }
        return section;
    }

   ......
}

關於訪問控制資料,我們要注意Shiro Filter在執行訪問控制時,是按訪問控制資料的順序來逐個驗證的,而我們將資料庫中的訪問控制資料追加到配置檔案的後面。例如上面的配置:

<bean id="filterChainDefinitions" class="com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions">
    <property name="filterChainDefinitions">
        <value>
            /html/**=anon
            /js/**=anon
            /css/**=anon
            /images/**=anon
            /authc/login=anon
            /login=anon
            /user/add=roles[manager]
            /user/del/**=roles[admin]
            /user/edit/**=roles[manager]
        </value>
    </property>
</bean>

追加上我們在資料庫中的訪問配置資料:

id authcType url val
1 perms /user user:query
2 authc /**

那麼全部訪問控制資料應該是:

/html/**=anon
/js/**=anon
/css/**=anon
/images/**=anon
/authc/login=anon
/login=anon
/user/add=roles[manager]
/user/del/**=roles[admin]
/user/edit/**=roles[manager]
/user=perms[user:query]
/**=authc

這裡我們要強調一下,如果把基於角色和基於permission的控制放在/**=authc之後的控制是不會起作用的。例如:

/html/**=anon
/js/**=anon
/css/**=anon
/images/**=anon
/authc/login=anon
/login=anon
/user/add=roles[manager]
/user/del/**=roles[admin]
/user/edit/**=roles[manager]
/**=authc
/user=perms[user:query]

這時如果使用者rose擁有user:del的permission,在他登入系統以後也是可以訪問/user路徑的。其實我們想實現只有擁有user:query的使用者才能訪問/user,而rose擁有的是user:del。我們設定了/user=perms[user:query],而rose並沒有user:query這樣的permission,為什麼rose還能訪問/user呢?這是因為,Shior Filter先使用訪問控制規則/**=authc對rose的許可權進行了驗證,那麼rose是一個已知身份的使用者,所以他可以訪問所有url,除了/**=authc之前設定的規則限制不能訪問的url。

是不是很混亂,一部分訪問控制規則在配置檔案中,一部分又在資料庫中,而且訪問控制還有順序要求,一旦我們忽略任意一部分訪問控制資料,我們的設定就很難達到我們預期的效果。所以,將訪問控制資料分開並不是一個好的實踐。

我們這裡實現了使用資料庫配置訪問控制資料,僅僅是為了開闊一下思路,並不推薦同時使用資料庫配置和配置檔案配置。

關於permission語法

我們可以參考Understanding Permissions in Apache Shiro。你可能會發現好長的一篇文章啊!

那麼下面我就我個人的理解,簡單對permission語法說明一下。

在我們設計系統時,我們經過一系列的分析過程,會得到我們要實現的系統中存在哪些實體。例如,系統中存在使用者(user),工作流(workflow)等實體。我們對這些實體進行抽象化,構成我們系統的基礎模型。那麼實體除了資料屬性,例如,使用者名稱(username),流程名稱(flowname)等,還具備一些功能(function)或者叫做方法(method)的特徵。我們使用OOP的思想來設計系統,那麼我們抽象出來的實體就是我們所說的實體類,這些實體類代表了很多實體物件,例如,user類中,實際包含了tom,jack和rose,這些使用者的一個子集就組成了一個資料域。那麼我們的permission就是由系統實體和功能,以及一個資料域組成,格式就像這樣:實體:功能:資料域。

例如: perms[user:query:*]——表示允許查詢(query)使用者(user實體類)所有(*)物件的許可。 perms[user:query,add,del,update]——表示允許查詢(query)、新增(add)、刪除(del)和更新(update)使用者(user)所有物件的許可。 perms[user:query:jack,rose]——表示允許查詢(query)使用者(user)中jack和rose資料的許可。 perms[workflow:approve:order]——表示允許操作工作流程(workflow)中一個名叫order的流程的審批許可。

試驗

我們掌握了專案中整合Apache Shiro的方法後,將專案在IDEA中run或者debug起來。使用tom、jack和rose使用者登入系統中,看看會有什麼現象。當然使用者、角色和permission資料都在db.sql檔案中,匯入資料庫時已經一起匯入了。你可以修改這些資料,以及他們之間的關係來體驗Shiro的安全框架。

總結

希望通過這個說明,能夠讓你瞭解在本專案中是如何整合Apache Shiro安全框架的。

在接下來的計劃中,我們將實現在叢集環境中使用Apache Shiro。

相關文章