base-framework 工程文檔收錄

1 shrio 實用功能說明

apache shiro 是功能強大而且容易集成的開源權限框架,它可以完成認證、受權、加密、會話管理等功能。認證和受權爲權限控制的核心,簡單來講,「認證」就是證實「你是誰?」 Web 應用程序通常作法是經過表單提交的用戶名及密碼達到認證目的。「受權」便是"你能作什麼?",不少系統經過資源表的形式來完成用戶能作什麼。關於 shiro 的一系列特徵及優勢,不少文章已有列舉,這裏再也不逐一贅述,本文首先會簡單的講述 shiro 和spring該如何集成,重點介紹 shiro 的幾個實用功能和一些 shiro 的擴展知識。html

1.1 shiro 集成 spring

因爲 spring 在 java web 應用裏普遍使用,在項目中使用 spring 給項目開發帶來的好處有不少,spring 框架自己就是一個很是靈活的東西,而 shrio 的設計模式,讓 shiro 集成 spring 並不是難事。java

首先在web.xml裏,經過 spring 的 org.springframework.web.filter.DelegatingFilterProxy 定義一個 filter ,讓全部可訪問的請求經過一個主要的 shiro 過濾器。該過濾器自己是極爲強大的,容許臨時的自定義過濾器鏈基於任何 URL 路徑表達式執行。git

web.xml:github

<!-- shiro security filter -->
<filter>
    <filter-name>shiroSecurityFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>shiroSecurityFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

接下來在你的 applicationContext.xml 文件中定義 web 支持的 SecurityManager 和剛剛在 web.xml 定義的 shiroSecurityFilter 便可完成 shiro 和 spring 的集成。web

applicationContext.xml:正則表達式

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- 默認的鏈接攔截配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = authc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <...>
</bean>

提示: org.apache.shiro.spring.web.ShiroFilterFactoryBean 的 id 名稱必須和 web.xml 的 filter-name 一致spring

ShiroFilterFactoryBean 相似預初始化shiro的一個 java bean,主要做用是配置一些東西讓 shiro 知道要作些什麼動做,好比,經過以上的配置,要訪問http://localhost:port/porject/index,必須當前用戶擁有 permission 爲 security:index 才能訪問,不然將會跳轉到指定的loginUrl。當登陸時使用 FormAuthenticationFilter 來作登陸等等。sql

ShiroFilterFactoryBean 的 filterChainDefinitions 是對系統要攔截的連接url作配置,好比,我係統中有一條連接爲 http://localhost:prot/project/add ,須要當前用戶存在角色爲admin或者擁有 permission 爲 system:add 的才能訪問該連接。須要配置以下:數據庫

/add = role[admin], perms[security:index]

提示:Shiro支持了權限(permissions)概念。權限是功能的原始表述,如:開門、建立一個博文、刪除jsmith用戶等。經過讓權限反映應用的原始功能,在改變應用功能時,你只須要改變權限檢查。進而,你能夠在運行時按需將權限分配給角色或用戶。express

若是不配置任何東西在裏面的話,shiro會起不到安全框架的做用。但若是將整個系統的全部連接配置到 filterChainDefinitions 裏面會有不少,這樣做的作法會不靠譜。因此,應該經過動態的、可配置的形式來作 filterChainDefinitions,該功能會在動態filterChainDefinitions裏說明如何經過數據庫來建立動態的filterChainDefinitions。

1.1.1 啓用Shiro註解

在獨立應用程序和 web 應用程序中,你可能想爲安全檢查使用 shiro 的註釋(例如,@RequiresRoles,@RequiresPermissions 等等)。這須要 shiro 的 spring AOP 集成來掃描合適的註解類以及執行必要的安全邏輯。如下是如何使用這些註解的。只需添加這兩個 bean:

<aop:aspectj-autoproxy proxy-target-class="true" />

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

對於這兩個bean,在base-framework裏添加在applicationContext-mvc.xml中,這樣作是爲了啓用 shiro 註解時僅在 spring mvc 的 controller 層就能夠了,沒必要在 service 和 dao 中也使用該註解。

到這裏,shiro 和 spring 集成的關鍵點只有這麼點東西。最重要的接口在 securityManager 中。securityManager 管理了認證、受權,session 等 web 安全的重要類,首先來完成認證、受權方面的功能。

1.2 shiro 認證、受權

在 shiro 裏,認證,主要是知道「你是誰」受權,是給於你權限去「作什麼」。因此,在完成認證和受權以前,咱們要構造最經典的權限3張表去完成這些事,但在這裏畫表要圖片,總體感也很醜。因此,以 hibernate 實體的方式去說明表的結構:

首先,經典3張表須要用戶、組、資源這3個實體,而3個實體的關係爲多對多關係:

/**
 * 用戶實體
 * @author maurice
 *
 */
@Entity
@Table(name="TB_USER")
public class User extends IdEntity{

    private static final long serialVersionUID = 1L;

    //登陸名稱
    private String username;
    //登陸密碼
    private String password;

    //用戶所在的組
    @ManyToMany(fetch=FetchType.LAZY)
    @JoinTable(
        name = "TB_GROUP_USER", 
        joinColumns = { @JoinColumn(name = "FK_USER_ID") }, 
        inverseJoinColumns = { @JoinColumn(name = "FK_GROUP_ID") }
    )
    private List<Group> groupsList = new ArrayList<Group>();

    //----------------getting/setting----------------//
}
**
 * 組實體
 * 
 * @author maurice
 *
 */
@Entity
@Table(name="TB_GROUP")
public class Group extends IdEntity{

    private static final long serialVersionUID = 1L;

    //名稱
    private String name;

    //用戶成員
    @ManyToMany(fetch=FetchType.LAZY)
    @JoinTable(
        name = "TB_GROUP_USER", 
        joinColumns = { @JoinColumn(name = "FK_GROUP_ID") }, 
        inverseJoinColumns = { @JoinColumn(name = "FK_USER_ID") }
    )
    private List<User> membersList = new ArrayList<User>();

    //擁有訪問的資源
    @ManyToMany(fetch=FetchType.LAZY)
    @JoinTable(
        name = "TB_GROUP_RESOURCE", 
        joinColumns = { @JoinColumn(name = "FK_GROUP_ID") }, 
        inverseJoinColumns = { @JoinColumn(name = "FK_RESOURCE_ID") }
    )
    private List<Resource> resourcesList = new ArrayList<Resource>();
    //shiro role 字符串
    private String role;
    //shiro role連定義的值
    private String value;

    //----------------getting/setting----------------//
}
**
 * 資源實體
 * 
 * @author maurice
 *
 */
@Entity
@Table(name="TB_RESOURCE")
public class Resource extends IdEntity{

    private static final long serialVersionUID = 1L;

    //名稱
    private String name;
    //action url
    private String value;

    //資源所對應的組集合
    @ManyToMany(fetch=FetchType.LAZY)
    @JoinTable(
        name = "TB_GROUP_RESOURCE", 
        joinColumns = { @JoinColumn(name = "FK_RESOURCE_ID") }, 
        inverseJoinColumns = { @JoinColumn(name = "FK_GROUP_ID") }
    )
    private List<Group> groupsList = new ArrayList<Group>();
    //shiro permission 字符串
    private String permission;

    //----------------getting/setting----------------//
}

經過以上3個 hibernate 實體,構建出瞭如下表結構,有了這些表,作認證和受權是很是簡單的一件事:

表名 表說明
TB_USER 用戶表
TB_GROUP 組表
TB_RESOURCE 資源表
TB_GROUP_USER 用戶與組的多對多中間表
TB_GROUP_RESOURCE 組與資源的多對多中間表

初始化數據假設是這樣:

TB_USER
id username password
17909124407b8d7901407be4996c0001 admin admin
TB_GROUP
id name role value
17909124407b8d7901407be4996c0002 超級管理員
TB_RESOURCE
id name permission value
17909124407b8d7901407be4996c0003 添加用戶 perms[user:add] /user/add/**
TB_GROUP_USER
FK_USER_ID FK_GROUP_ID
17909124407b8d7901407be4996c0001 17909124407b8d7901407be4996c0002
TB_GROUP_RESOURCE
FK_GROUP_ID FK_RESOURCE_ID
17909124407b8d7901407be4996c0002 17909124407b8d7901407be4996c0003

首先要認識的第一個對象是securityManager所管理的 org.apache.shiro.realm.Realm 接口,realm 擔當 shiro 和你的應用程序的安全數據之間的「橋樑」或「鏈接器」。但它實際上與安全相關的數據(如用來執行認證及受權)的用戶賬戶交互時,shiro 從一個或多個爲應用程序配置的 realm 中尋找許多這樣的東西。

在這個意義上說,realm 本質上是一個特定安全的 dao:它封裝了數據源的鏈接詳細信息,使 shiro 所需的相關數據可用。當配置 shiro 時,你必須指定至少一個 realm 用來進行身份驗證和受權。securityManager 可能配置多個 realms,但至少必須有一個

shiro 提供了當即可用的 realms 來鏈接一些安全數據源(即目錄),如LDAP、關係數據庫(JDBC)、文本配置源等。若是默認地 realm 不符合你的需求,你能夠插入你本身的 realm 實現來表明自定義的數據源。

base-framework裏就使用了本身的realm來完成受權和認證工做,realm接口有不少實現類,包括緩存、JdbcRealm、JndiLdapRealm,而JdbcRealm、JndiLdapRealm都是繼承AuthorizingRealm類,AuthorizingRealm類有兩個抽象方法:

/**
 * 
 * 訪問連接時的受權方法
 * 
 */
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

/**
 * 用戶認證方法
 * 
 */
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
throws AuthenticationException;

doGetAuthenticationInfo方法的做用是在用戶進行登陸時經過該方法去作認證工做(你是誰),doGetAuthenticationInfo 方法裏的 AuthenticationToken 參數是一個認證令牌,裝載着表單提交過來的數據,因爲 shiro 的認證 filter 默認爲 FormAuthenticationFilter,經過 filter 建立的令牌爲 UsernamePasswordToken類,該類裏面包含了表單提交上來的username、password、remeberme等信息。

doGetAuthorizationInfo方法的做用是在用戶認證完成後(登陸完成後),對要訪問的連接作受權工做。好比剛剛在上面配置的 spring xml 文件裏有那麼一句話:

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <...>
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接 -->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- 默認的鏈接攔截配置 -->
    <property name="filterChainDefinitions">
        <value>
            ...
            /index = perms[security:index]
        </value>
    </property>
</bean>

當用戶登陸成功後會跳轉到 successUrl 這個連接,即:http://localhost:port/index。那麼這個index又要當前用戶存在permission  security:index 才能進入,因此,當登陸完成跳轉 successUrl 時,會進入到 doGetAuthorizationInfo 方法裏進行一次受權,讓 shiro 瞭解該連接在當前認證的用戶裏是否能夠訪問,若是能夠訪問,那就執行接入到index,不然就會跳轉到unauthorizedUrl。

瞭解以上狀況,首先咱們建立UserDao和ResourceDao類來作數據訪問工做:

@Repository
public class UserDao extends BasicHibernateDao<User, String> {

    /**經過登陸賬號獲取用戶實體**/
    public User getUserByUsername(String username) {
        return findUniqueByProperty("username", username);
    }

}
@Repository
public class ResourceDao extends BasicHibernateDao<Resource, String> {

    /**經過用戶id獲取用戶全部的資源集合**/
    public List<Resource> getUserResource(String id) {
        String h = "select rl from User u left join u.groupsList gl left join gl.resourcesList rl where u.id=?1";
        return distinct(h, id);
    }

}

而後在建立一個類,名叫JdbcAuthenticationRealm,並繼承AuthorizingRealm這個抽象類,實現它的抽象方法:

public class JdbcAuthenticationRealm extends AuthorizingRealm{

    @Autowired
    private UserDao userDao;
    @Autowired
    private ResourceDao resourceDao;

    /**
     * 用戶登陸的身份驗證方法
     * 
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;

        String username = usernamePasswordToken.getUsername();

        if (username == null) {
            throw new AccountException("用戶名不能爲空");
        }
        //經過登陸賬號獲取用戶實體
        User user = userDao.getUserByUsername(username);

        if (user == null) {
            throw new UnknownAccountException("用戶不存在");
        }

        return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
    }

    /**
     * 
     * 當用戶進行訪問連接時的受權方法
     * 
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
            throw new AuthorizationException("Principal對象不能爲空");
        }

        User user = principals.oneByType(User.class);
        List<Resource> resource = resourceDao.getUserResource(user.getId());

        //獲取用戶相應的permission
        List<String> permissions = CollectionUtils.extractToList(resource, "permission",true);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        addPermissions(info, permissions);

        return info;
    }

    /**
     * 經過資源集合,將集合中的permission字段內容解析後添加到SimpleAuthorizationInfo受權信息中
     * 
     * @param info SimpleAuthorizationInfo
     * @param authorizationInfo 資源集合
     */
    private void addPermissions(SimpleAuthorizationInfo info,List<Resource> authorizationInfo) {
        //解析當前用戶資源中的permissions
        List<String> temp = CollectionUtils.extractToList(authorizationInfo, "permission", true);
        List<String> permissions = getValue(temp,"perms\\[(.*?)\\]");

        //添加默認的permissions到permissions
        if (CollectionUtils.isNotEmpty(defaultPermission)) {
            CollectionUtils.addAll(permissions, defaultPermission.iterator());
        }

        //將當前用戶擁有的permissions設置到SimpleAuthorizationInfo中
        info.addStringPermissions(permissions);

    }

    /**
     * 經過正則表達式獲取字符串集合的值
     * 
     * @param obj 字符串集合
     * @param regex 表達式
     * 
     * @return List
     */
    private List<String> getValue(List<String> obj,String regex){

        List<String> result = new ArrayList<String>();

        if (CollectionUtils.isEmpty(obj)) {
            return result;
        }

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(StringUtils.join(obj, ","));

        while(matcher.find()){
            result.add(matcher.group(1));
        }

        return result;
    }

}

完成後修改applicationContext.xml文件:

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接 -->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
            /changePassword = perms[security:change-password]
        </value>
    </property>
</bean>

以上代碼首先從doGetAuthenticationInfo讀起,首先。假設咱們有一個表單

<form action="${base}/login" method="post">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="checkbox" name="remeberMe" />
    <input type="submit" value="提交"/>
</form>

提示: input標籤的全部name屬性不必定要寫死 username,password,remeberMe。能夠在 FormAuthenticationFilter 修改

當點擊提交時,shiro 會攔截此次的表單提交,由於在配置文件裏已經說明,/login 由 authc 作處理,就是:

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <...>
    <!-- 默認的鏈接攔截配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = authc
            ...
        </value>
    </property>
</bean>

而 authc 就是 shiro 的 FormAuthenticationFilter 。shiro 首先會判斷 /login 此次請求是否爲post請求,若是是,那麼就交給 FormAuthenticationFilter 處理,不然將不作任何處理。

當 FormAuthenticationFilter 接收到要處理時。那麼 FormAuthenticationFilter 首先會根據表單提交過來的請求參數建立一個UsernamePasswordToken,而後獲取一個 Subject 對象,由Subject去執行登陸。

提示: Subject 實質上是一個當前執行用戶的特定的安全「視圖」。鑑於「User」一詞一般意味着一我的,而一個 Subject 能夠是一我的,但它還能夠表明第三方服務,daemon account,cron job,或其餘相似的任何東西——基本上是當前正與軟件進行交互的任何東西。

全部 Subject 實例都被綁定到(且這是必須的)一個 SecurityManager 上。當你與一個 Subject 交互時,那些交互做用轉化爲與 SecurityManager 交互的特定 subject 的交互做用。

Subject執行登陸時,會將UsernamePasswordToken傳入到Subject.login方法中。在通過一些小小的處理過程後(如:是否啓用了認證緩存,若是是,獲取認證緩存,執行登陸,不在查詢數據庫),會進入到 doGetAuthenticationInfo方法裏,而在doGetAuthenticationInfo方法作的事情就是:

  1. 經過用戶名獲取當前用戶

  2. 經過當前用戶和用戶密碼建立一個 SimpleAuthenticationInfo 而後去匹配密碼是否正確

在SimpleAuthenticationInfo對象裏的密碼爲數據庫裏面的用戶密碼,返回SimpleAuthenticationInfo後 shiro 會根據表單提交的密碼和 SimpleAuthenticationInfo 的密碼去作對比,若是徹底正確,就表示認證成功,當成功後,會重定向到successUrl這個連接。

當重定向到 index 時,會進入到 perms過濾器,就是 shiro 的PermissionsAuthorizationFilter,由於配置文件裏已經說明,就是:

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <...>
    <!-- 默認的鏈接攔截配置 -->
    <property name="filterChainDefinitions">
        <value>
            ...
            /index = perms[security:index]
        </value>
    </property>
</bean>

PermissionsAuthorizationFilter的工做主要是判斷當前subject是否有足夠的權限去訪問index,判斷條件有:

  1. 判斷subject是否已認證,若是沒認證,返回登陸頁面,就是在配置文件裏指定的loginUrl

  2. 若是認證了,判斷當前用戶是否存未受權,若是沒有就去受權,當受權時,就會進入到 doGetAuthorizationInfo 方法

  3. 若是已經認證了。就判斷是否存在xx連接的permission,若是有,就進入,不然重定向到未受權頁面,就是在配置文件裏指定的unauthorizedUrl

那麼,認證咱們上面已經認證過了。就會進入到第二個判斷,第二個判斷會跑到了doGetAuthorizationInfo方法,而doGetAuthorizationInfo方法裏作了幾件事:

  1. 獲取當前的用戶

  2. 經過用戶id獲取用戶的資源集合

  3. 將資源實體集合裏的permission獲取出來造成一個List

  4. 將用戶擁有的permission放入到SimpleAuthorizationInfo對象中

doGetAuthorizationInfo 返回 SimpleAuthorizationInfo 對象的做用是讓 shiro 的 AuthorizingRealm 逐個循環裏面的 permission 和當前訪問連接的 permission 去作匹配,若是匹配到了,就表示當前用戶能夠訪問本次請求的連接,不然就重定向到未受權頁面。

實現認證和受權功能繼承AuthorizingRealm已經能夠達到效果,可是要注意幾點就是:

  1. 表單提交的action要和filterChainDefinitions的一致。

  2. filterChainDefinitions的「/login = authc」這句話的左值要和loginUrl屬性一致。

  3. 表單提交必需要post方法

完成認證和受權後如今的缺陷在於filterChainDefinitions都是要手動去一個個配置,一個系統那麼多連接都要寫上去很是不靠譜,下面將介紹如何使用資源表動態去構建filterChainDefinitions。

1.3 動態filterChainDefinitions

動態 filterChainDefinitions 是爲了可以經過數據庫的數據,將 filterChainDefinitions 構造出來,而不在是一個個手動的寫入到配置文件中,在shiro的 ShiroFilterFactoryBean 啓動時,會經過 filterChainDefinitions 的配置信息構形成一個Map,在賦值到filterChainDefinitionMap 中,shiro的源碼以下:

/**
 * A convenience method that sets the {@link #setFilterChainDefinitionMap(java.util.Map) filterChainDefinitionMap}
 * property by accepting a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs).
 * Each key/value pair must conform to the format defined by the
 * {@link FilterChainManager#createChain(String,String)} JavaDoc - each property key is an ant URL
 * path expression and the value is the comma-delimited chain definition.
 *
 * @param definitions a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs)
 *                    where each key/value pair represents a single urlPathExpression-commaDelimitedChainDefinition.
 */
public void setFilterChainDefinitions(String definitions) {
    Ini ini = new Ini();
    ini.load(definitions);
    //did they explicitly state a 'urls' section?  Not necessary, but just in case:
    Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
    if (CollectionUtils.isEmpty(section)) {
        //no urls section.  Since this _is_ a urls chain definition property, just assume the
        //default section contains only the definitions:
        section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
    }
    setFilterChainDefinitionMap(section);
}

提示:Ini.Section 該類是一個 Map 子類。

ShiroFilterFactoryBean 也提供了設置 filterChainDefinitionMap 的方法,配置 filterChainDefinitions 和 filterChainDefinitionMap 二者只需一個便可。

在實現動態 filterChainDefinitions 時,須要藉助 spring 的 FactoryBean 接口去作這件事。spring 的 FactoryBean 接口是專門暴露bean對象的接口,經過接口的 getObject() 方法獲取bean實例,也能夠經過 getObjectType() 方法去指定bean的類型,讓註解Autowired可以注入或在 spring 上下文中 getBean()方法直接經過class去獲取該bean。

那麼,繼續用上面的經典三張表的資源數據訪問去動態構造 filterChainDefinitions。 首先建立一個 ChainDefinitionSectionMetaSource 類並實現 FactoryBean 的方法和在resourceDao中添加一個獲取全部資源的方法,以下:

@Repository
public class ResourceDao extends BasicHibernateDao<Resource, String> {

    /**經過用戶id獲取用戶全部的資源集合**/
    public List<Resource> getUserResource(String id) {
        String h = "select rl from User u left join u.groupsList gl left join gl.resourcesList rl where u.id=?1";
        return distinct(h, id);
    }

    /**獲取有時有資源**/     
    public List<Resource> getAllResource() {
        return getAll();
    }

}
/**
 * 藉助spring {@link FactoryBean} 對apache shiro的premission進行動態建立
 * 
 * @author maurice
 *
 */
public class ChainDefinitionSectionMetaSource implements FactoryBean<Ini.Section>{

    @Autowired
    private ResourceDao resourceDao;

    //shiro默認的連接定義
    private String filterChainDefinitions;

    /**
     * 經過filterChainDefinitions對默認的連接過濾定義
     * 
     * @param filterChainDefinitions 默認的接過濾定義
     */
    public void setFilterChainDefinitions(String filterChainDefinitions) {
        this.filterChainDefinitions = filterChainDefinitions;
    }

    @Override
    public Section getObject() throws BeansException {
        Ini ini = new Ini();
        //加載默認的url
        ini.load(filterChainDefinitions);

        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
        if (CollectionUtils.isEmpty(section)) {
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }

        //循環數據庫資源的url
        for (Resource resource : resourceDao.getAll()) {
            if(StringUtils.isNotEmpty(resource.getValue()) && StringUtils.isNotEmpty(resource.getPermission())) {
                section.put(resource.getValue(), resource.getPermission());
            }
        }

        return section;
    }

    @Override
    public Class<?> getObjectType() {
        return Section.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}

ChainDefinitionSectionMetaSource 類,重點在 getObject() 中,返回了一個 shiro 的 Ini.Section 首先Ini類加載了filterChainDefinitions的配置信息(因爲有些連接不必定要放到數據庫裏,也能夠經過直接寫在配置文件中)。經過ini.load(filterChainDefinitions);一話構形成了/login key = authc value等信息。那麼shiro就知道了login這個url須要使用authc這個filter去攔截。完成以後,經過resourceDao的getAll()方法將全部數據庫的信息再次疊加到Ini.Section中(在tb_resource表中的數據爲:/user/add/** = perms[user:add]),造成了最後的配置。

完成該以上工做後,修改 spring 的 applicationContext.xml,當項目啓動時,你會發如今容器加載spring內容時,會進入到ChainDefinitionSectionMetaSource,若是使用maven的朋友,進入到shiro的源碼放一個斷點,你會看到tb_resource表的/user/add/** = perms[user:add]已經構造到了filterChainDefinitionMap裏。

applicationContext.xml修改成:

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = authc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

經過修改和添加以上三個文件,完成了動態 filterChainDefinitions 具體的過程在base-framework的showcase的base-curd項目下有例子,若是看不懂。能夠根據例子去理解。

1.4 擴展 shiro 的 filter 實現驗證碼登陸

驗證碼登陸在web開發中最多見,shiro對於驗證碼登陸的功能沒有支持,但shiro的設計模式讓開發人員自定義一個小小的驗證碼登陸不會很難。base-framework的showcase的base-curd項目所擴展的驗證碼登陸需求是:當用戶登陸失敗次數達到指標時,纔出現驗證碼。

經過該需求,咱們回到上面提到的 FormAuthenticationFilter,該filter是專門作認證用的filter,因此本人第一時間想到擴展它,若是有更好的實現方式但願可以分享。

實現驗證碼登陸,咱們首先建立一個CaptchaAuthenticationFilter類,並繼承FormAuthenticationFilter。FormAuthenticationFilter最須要重寫的方法有:

/**執行登陸**/
protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception

/**當登陸失敗時所響應的方法**/
protected boolean onLoginFailure(AuthenticationToken token,
                                 AuthenticationException e, 
                                 ServletRequest request,
                                 ServletResponse response);

/**當登陸成功時所響應的方法**/
protected boolean onLoginSuccess(AuthenticationToken token,
                                 Subject subject, 
                                 ServletRequest request, 
                                 ServletResponse response) throws Exception;

因此CaptchaAuthenticationFilter類應該這樣實現:

/**
 * 驗證碼登陸認證Filter
 * 
 * @author maurice
 *
 */
@Component
public class CaptchaAuthenticationFilter extends FormAuthenticationFilter{

    /**
     * 默認驗證碼參數名稱
     */
    public static final String DEFAULT_CAPTCHA_PARAM = "captcha";

    /**
     * 默認在session中存儲的登陸錯誤次數的名稱
     */
    private static final String DEFAULT_LOGIN_INCORRECT_NUMBER_KEY_ATTRIBUTE = "incorrectNumber";

    //驗證碼參數名稱
    private String captchaParam = DEFAULT_CAPTCHA_PARAM;
    //在session中的存儲驗證碼的key名稱
    private String sessionCaptchaKeyAttribute = DEFAULT_CAPTCHA_PARAM;
    //在session中存儲的登陸錯誤次數名稱
    private String loginIncorrectNumberKeyAttribute = DEFAULT_LOGIN_INCORRECT_NUMBER_KEY_ATTRIBUTE;
    //容許登陸錯誤次數,當登陸次數大於該數值時,會在頁面中顯示驗證碼
    private Integer allowIncorrectNumber = 1;

    /**
     * 重寫父類方法,在shiro執行登陸時先對比驗證碼,正確後在登陸,不然直接登陸失敗
     */
    @Override
    protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception {

        Session session = SystemVariableUtils.createSessionIfNull();
        //獲取登陸錯誤次數
        Integer number = (Integer) session.getAttribute(getLoginIncorrectNumberKeyAttribute());

        //首次登陸,將該數量記錄在session中
        if (number == null) {
            number = new Integer(1);
            session.setAttribute(getLoginIncorrectNumberKeyAttribute(), number);
        }

        //若是登陸次數大於allowIncorrectNumber,須要判斷驗證碼是否一致
        if (number > getAllowIncorrectNumber()) {
            //獲取當前驗證碼
            String currentCaptcha = (String) session.getAttribute(getSessionCaptchaKeyAttribute());
            //獲取用戶輸入的驗證碼
            String submitCaptcha = getCaptcha(request);
            //若是驗證碼不匹配,登陸失敗
            if (StringUtils.isEmpty(submitCaptcha) || !StringUtils.equals(currentCaptcha,submitCaptcha.toLowerCase())) {
                return onLoginFailure(this.createToken(request, response), 
                                      new AccountException("驗證碼不正確"), 
                                      request, 
                                      response);
            }

        }

        return super.executeLogin(request, response);
    }


    /**
     * 重寫父類方法,當登陸失敗將異常信息設置到request的attribute中
     */
    @Override
    protected void setFailureAttribute(ServletRequest request,AuthenticationException ae) {
        if (ae instanceof IncorrectCredentialsException) {
            request.setAttribute(getFailureKeyAttribute(), "用戶名密碼不正確");
        } else {
            request.setAttribute(getFailureKeyAttribute(), ae.getMessage());
        }
    }

    /**
     * 重寫父類方法,當登陸失敗次數大於allowIncorrectNumber(容許登陸錯誤次數)時,將顯示驗證碼
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token,
                                     AuthenticationException e, 
                                     ServletRequest request,
                                     ServletResponse response) {

        Session session = SystemVariableUtils.getSession();
        Integer number = (Integer) session.getAttribute(getLoginIncorrectNumberKeyAttribute());
        session.setAttribute(getLoginIncorrectNumberKeyAttribute(),++number);

        return super.onLoginFailure(token, e, request, response);
    }

    /**
     * 重寫父類方法,當登陸成功後,將allowIncorrectNumber(容許登錯誤錄次)設置爲0,重置下一次登陸的狀態
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, 
                                     Subject subject, 
                                     ServletRequest request, 
                                     ServletResponse response) throws Exception {

        Session session = SystemVariableUtils.getSession();
        session.removeAttribute(getLoginIncorrectNumberKeyAttribute());
        session.setAttribute("sv", subject.getPrincipal());

        return super.onLoginSuccess(token, subject, request, response);
    }

    //---------------------------------getter/setter方法----------------------------------//
}

CaptchaAuthenticationFilter類重點的代碼在executeLogin方法和onLoginFailure方法中。當執行登陸時,會在session中建立一個"登陸錯誤次數"屬性,當該屬性大於指定的值時纔去匹配驗證碼,不然繼續調用FormAuthenticationFilter的executeLogin方法執行登陸。

當登陸失敗時(onLoginFailure方法)會獲取"登陸錯誤次數",而且加1。直到登陸成功後,將"登陸錯誤次數"屬性從session中移除。

提示setFailureAttribute方法的做用是當出現用戶名密碼錯誤時提示中文出去,這樣會友好些。

因此,在登陸界面的html中用freemarker的話就這樣寫:

<form action="${base}/login" method="post">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="checkbox" name="remeberMe" />
    <!--當登陸錯誤次數大於1時,出現驗證碼 -->       
    <#if Session.incorrectNumber?? && Session.incorrectNumber gte 1>
      <input type="text" name="captcha" id="captcha" >
      <img id="captchaImg" src="get-captcha" />
    </#if>
    <input type="submit" value="提交"/>
</form>

完成後在修改applicationContext.xml便可:

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

提示: applicationContext.xml修改了ShiroFilterFactoryBean的filters屬性,在filters屬性裏添加了一個自定義的 captchaAuthenticationFilter , 名字叫 captchaAuthc 在 filterChainDefinitions 裏將 /login = authc 該爲 /login = captchaAuthc。

經過添加CaptchaAuthenticationFilter類和修改applicationContext.xml文件,完成了驗證碼,具體的過程在base-framework的showcase的base-curd項目下有例子,若是看不懂。能夠根據例子去理解。

1.5 定義 AuthorizationRealm 抽象類,讓多 realms 的受權獲得統一

1.2 shiro 認證、受權中提到,realm 擔當 shiro 和你的應用程序的安全數據之間的「橋樑」或「鏈接器」。必需要存在一個。當一個應用程序配置了兩個或兩個以上的 realm 時,ModularRealmAuthenticator 依靠內部的 AuthenticationStrategy 組件來肯定這些認證嘗試的成功或失敗條件。如:若是隻有一個 realm 驗證成功,但全部其餘的都失敗,這被認爲是成功仍是失敗?又或者必須全部的 realm 驗證成功才被認爲樣子成功?又或者若是一個 realm 驗證成功,是否有必要進一步調用其餘 realm ? 等等。

AuthenticationStrategy: 是一個無狀態的組件,它在身份驗證嘗試中被詢問4 次(這4 次交互所需的任何須要的狀態將被做爲方法參數):

  1. 在任何Realm 被調用以前被詢問。

  2. 在一個單獨的Realm 的getAuthenticationInfo 方法被調用以前當即被詢問。

  3. 在一個單獨的Realm 的getAuthenticationInfo 方法被調用以後當即被詢問。

  4. 在全部的Realm 被調用後詢問。

另外,AuthenticationStrategy 負責從每個成功的 realm 彙總結果並將它們「捆綁」到一個單一的 AuthenticationInfo 再現。這最後彙總的 AuthenticationInfo 實例就是從 Authenticator 實例返回的值以及 shiro 所用來表明 Subject 的最終身份ID 的值(即Principals(身份))。

shiro 有 3 個具體的AuthenticationStrategy 實現:

AuthenticationStrategy 類 描述
AtLeastOneSuccessfulStrategy 若是一個(或更多)Realm 驗證成功,則總體的嘗試被認爲是成功的。若是沒有一個驗證成功,則總體嘗試失敗。
FirstSuccessfulStrategy 只有第一個成功驗證的Realm 返回的信息將被使用。全部進一步的Realm 將被忽略。若是沒有一個驗證成功,則總體嘗試失敗。
AllSucessfulStrategy 爲了總體的嘗試成功,全部配置的Realm 必須驗證成功。若是沒有一個驗證成功,則總體嘗試失敗。

ModularRealmAuthenticator 默認的是AtLeastOneSuccessfulStrategy 實現,由於這是最常所需的方案。若是你不喜歡,你能夠配置一個不一樣的方案:

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="authenticator">
        <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
            <!-- 認證策略使用FirstSuccessfulStrategy策略 -->
            <property name="authenticationStrategy">
                <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy" />
            </property>
            <!-- 多realms配置 -->
            <property name="realms">
                <list>
                    <value>
                        <ref bean="jdbcAuthenticationRealm" />
                        <ref bean="otherAuthenticationRealm" />
                    </value>
                </list>
            </property>
        </bean>
    </property>
</bean>

那麼如今存在這樣的需求,讓shiro 的多 realms 就有了發揮餘地:

  1. 在用戶登陸時,先從本系統數據庫獲取用戶信息。若是獲取獲得用戶,就執行進行認證。

  2. 若是獲取不到用戶,去第三方應用接口或者其餘數據庫獲取用戶。

  3. 若是是從第三方應用接口或者其餘數據庫獲取的用戶,將用戶插入到本系統的用戶表中,並賦給它一些本系統的權限。

但問題是:受權和認證的接口AuthorizingRealm須要實現兩個方法,但這個需求提到的是認證而已,受權卻要統一進行受權。因此,在多realms都繼承AuthorizingRealm會出現不少複製粘貼的受權代碼。因此,寫一個公用的受權抽象類會比較好些。固然,這個看需求而定。

那麼定義 AuthorizationRealm 抽象類讓多realms繼承它,完成各各realms本身的受權:

/**
 * apache shiro 的公用受權類
 * 
 * @author maurice
 *
 */
public abstract class AuthorizationRealm extends AuthorizingRealm{

    @Autowired
    private ResourceDao resourceDao;

    private List<String> defaultPermission = Lists.newArrayList();

    /**
     * 設置默認permission
     * 
     * @param defaultPermissionString permission 若是存在多個值,使用逗號","分割
     */
    public void setDefaultPermissionString(String defaultPermissionString) {
        String[] perms = StringUtils.split(defaultPermissionString,",");
        CollectionUtils.addAll(defaultPermission, perms);
    }

    /**
     * 設置默認permission
     * 
     * @param defaultPermission permission
     */
    public void setDefaultPermission(List<String> defaultPermission) {
        this.defaultPermission = defaultPermission;
    }

    /**
     * 
     * 當用戶進行訪問連接時的受權方法
     * 
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
            throw new AuthorizationException("Principal對象不能爲空");
        }

        User user = (User)principals.fromRealm(getName()).iterator().next();
        List<Resource> resource = resourceDao.getUserResource(user.getId());

        //獲取用戶相應的permission
        List<String> permissions = CollectionUtils.extractToList(resource, "permission",true);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //添加用戶擁有的permission
        addPermissions(info,authorizationInfo);

        return info;
    }

    /**
     * 經過資源集合,將集合中的permission字段內容解析後添加到SimpleAuthorizationInfo受權信息中
     * 
     * @param info SimpleAuthorizationInfo
     * @param authorizationInfo 資源集合
     */
    private void addPermissions(SimpleAuthorizationInfo info,List<Resource> authorizationInfo) {
        //解析當前用戶資源中的permissions
        List<String> temp = CollectionUtils.extractToList(authorizationInfo, "permission", true);
        List<String> permissions = getValue(temp,"perms\\[(.*?)\\]");

        //添加默認的permissions到permissions
        if (CollectionUtils.isNotEmpty(defaultPermission)) {
            CollectionUtils.addAll(permissions, defaultPermission.iterator());
        }

        //將當前用戶擁有的permissions設置到SimpleAuthorizationInfo中
        info.addStringPermissions(permissions);

    }

    /**
     * 經過正則表達式獲取字符串集合的值
     * 
     * @param obj 字符串集合
     * @param regex 表達式
     * 
     * @return List
     */
    private List<String> getValue(List<String> obj,String regex){

        List<String> result = new ArrayList<String>();

        if (CollectionUtils.isEmpty(obj)) {
            return result;
        }

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(StringUtils.join(obj, ","));

        while(matcher.find()){
            result.add(matcher.group(1));
        }

        return result;
    }
}

本系統的認證:

/**
 * 
 * apache shiro 的身份驗證類
 * 
 * @author maurice
 *
 */
public class JdbcAuthenticationRealm extends AuthorizationRealm{

    @Autowired
    private UserDao userDao;

    /**
     * 用戶登陸的身份驗證方法
     * 
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;

        String username = usernamePasswordToken.getUsername();

        if (username == null) {
            throw new AccountException("用戶名不能爲空");
        }
        //經過登陸賬號獲取用戶實體
        User user = userDao.getUserByUsername(username);

        if (user == null) {
            throw new UnknownAccountException("用戶不存在");
        }

        return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
    }


}

第三方應用和其餘數據庫的認證:

public class otherAuthenticationRealm extends AuthorizationRealm{

    /**
     * 用戶登陸的身份驗證方法
     * 
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //獲取用戶
        //插入到本系統
        //返回SimpleAuthenticationInfo.
    }


}

完成後在修改applicationContext.xml便可:

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="otherAuthenticationRealm" class="domain.OtherAuthenticationRealm" />

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="authenticator">
        <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
            <!-- 多realms配置 -->
            <property name="realms">
                <list>
                    <value>
                        <ref bean="jdbcAuthenticationRealm" />
                        <ref bean="otherAuthenticationRealm" />
                    </value>
                </list>
            </property>
        </bean>
    </property>
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

base-framework中沒有多realms例子,若是存在什麼問題。能夠到這裏提問。

1.6 更好的性能 shiro + cache

在許多應用程序中性能是相當重要的。緩存是從第一天開始第一個創建在 shiro 中的一流功能,以確保安全操做保持儘量的快。然而,緩存做爲一個概念是 shiro 的基本組成部分,實現一個完整的緩存機制是安全框架核心能力以外的事情。爲此,shiro 的緩存支持基本上是一個抽象的(包裝)API,它將「坐」在一個基本的緩存機制產品(例如,Ehcache,OSCache,Terracotta,Coherence,GigaSpaces,JBossCache 等)之上。這容許 shiro 終端用戶配置他們喜歡的任何緩存機制。

shiro 有三個重要的緩存接口:

  1. CacheManager - 負責全部緩存的主要管理組件,它返回 Cache 實例。

  2. Cache - 維護key/value 對。

  3. CacheManagerAware - 經過想要接收和使用 CacheManager 實例的組件來實現。

CacheManager 返回 Cache 實例,各類不一樣的 shiro 組件使用這些 Cache 實例來緩存必要的數據。任何實現了 CacheManagerAware 的 shiro 組件將會自動地接收一個配置好的 CacheManager,該 CacheManager 可以用來獲取 Cache 實例。

shiro 的 SecurityManager 實現及全部 AuthorizingRealm 實現都實現了 CacheManagerAware 。若是你在 SecurityManager 上設置了 CacheManger,它反過來也會將它設置到實現了 CacheManagerAware 的各類不一樣的Realm 上(OO delegation)。

那麼爲了方便,本節就使用先在比較流行的 ehcache 來作講解,將spring的cache和shiro的cache結合起來用,經過spring的緩存註解來「緩存數據」,「清除緩存」等操做。

具體配置文件以下,applicationContext.xml:

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
    <!-- cacheManager,集合spring緩存工廠 -->
    <property name="cacheManager" ref="cacheManager" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

<!-- spring對ehcache的緩存工廠支持 -->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml" />
</bean>

<!-- spring對ehcache的緩存管理 -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="ehCacheManagerFactory"></property>
</bean>

<!-- 使用緩存annotation 配置 -->
<cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />

經過以上修改將spring cache和shiro cache整合了起來。完成這個以後先解決一個session集羣同步的問題。

在不少web應用中,session共享是很是必要的一件事。而shiro提供了SessionManager給開發人員去管理和維護當前的session。固然,shiro所寫的sessionDao 本人認爲已經夠用了。若是想使用nosql來作存儲的話。能夠實現SessionManager接口去作你本身的業務邏輯。

提示:

SessionManager(org.apache.shiro.session.SessionManager)知道如何去建立及管理用戶 Session 生命週期來爲全部環境下的用戶提供一個強健的 Session 體驗。這在安全框架界是一個獨有的特點 shiro 擁有可以在任何環境下本地化管理用戶 Session 的能力,即便沒有可用的 Web/Servlet 或 EJB 容器,它將會使用它內置的企業級會話管理來提供一樣的編程體驗。SessionDAO 的存在容許任何數據源可以在持久會話中使用。

SesssionDAO表明SessionManager 執行Session 持久化(CRUD)操做。這容許任何數據存儲被插入到會話管理的基礎之中。SessionDAO 的權力是你可以實現該接口來與你想要的任何數據存儲進行通訊。這意味着你的會話數據能夠駐留在內存/緩存中,文件系統,關係數據庫或NoSQL 的數據存儲,或其餘任何你須要的位置。你得控制持久性行爲。

EHCache SessionDAO 默認是沒有啓用的,但若是你不打算實現你本身的SessionDAO,那麼強烈地建議你爲 shiro 的 SessionManagerment 啓用EHCache 支持。EHCache SessionDAO 將會在內存中保存會話,並支持溢出到磁盤,若內存成爲制約。這對生產程序確保你在運行時不會隨機地「丟失」會話是很是好的。

那麼修改applicationContext.xml相關的配置:

<!-- 使用EnterpriseCacheSessionDAO,將session放入到緩存,經過同步配置,將緩存同步到其餘集羣點上,解決session同步問題。 -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
    <!-- 活動session緩存名稱 -->
    <property name="activeSessionsCacheName" value="shiroActiveSessionCache" />
</bean>

<!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 使用EnterpriseCacheSessionDAO,解決session同步問題 -->
    <property name="sessionDAO" ref="sessionDAO" />
</bean>

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
    <!-- cacheManager,集合spring緩存工廠 -->
    <property name="cacheManager" ref="cacheManager" />
    <!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
    <property name="sessionManager" ref="sessionManager" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

<!-- spring對ehcache的緩存工廠支持 -->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml" />
</bean>

<!-- spring對ehcache的緩存管理 -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="ehCacheManagerFactory"></property>
</bean>

<!-- 使用緩存annotation 配置 -->
<cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />

注意sessionDAO這個bean 裏面的屬性activeSessionsCacheName就是ehcache的緩存名稱。經過該名稱能夠配置ehcache的緩存性質。

ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>  

    <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" 
                                            properties="peerDiscovery=automatic,
                                            multicastGroupAddress=230.0.0.1,
                                            multicastGroupPort=4446,
                                            timeToLive=32" />

    <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> 

    <defaultCache maxElementsInMemory="10000" 
                  eternal="false" 
                  timeToIdleSeconds="300" 
                  timeToLiveSeconds="600" 
                  overflowToDisk="true"/>

    <!-- shiro的活動session緩存名稱 -->
    <cache name="shiroActiveSessionCache" 
                 maxElementsInMemory="10000" 
                 timeToLiveSeconds="1200" 
                 memoryStoreEvictionPolicy="LRU">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" 
                                   properties="replicateAsynchronously=false"/>
    </cache>

</ehcache>

在shiroActiveSessionCache緩存裏。集羣的配置爲同步緩存。做用是subject的getSession可以在全部集羣的服務器上共享數據。

完成session共享後在發揮shiro的緩存功能,以1.6 shiro 認證、受權章節爲例子,將認證緩存  受權緩存 一塊兒解決。

認證緩存的用做至關於"熱用戶"的概念,意思就是說:

  1. 當一個用戶進行登陸成功後,將該用戶記錄到緩存中,當下次登陸時,不在去查數據庫,而是直接在緩存中獲取用戶信息,

  2. 當緩存滿了。而就將緩存裏最少使用的用戶踢出去。

shiro 的 realm就能實現這個需求,shiro 的 realm 自己就支持緩存。而緩存的踢出規則,ehcache 就能夠配置該規則。可是當用戶修改信息時,須要將緩存清除。否則下次登陸時,登陸密碼用之前舊的密碼同樣可以登陸,新的密碼就不起做用。

具體applicationContext.xml配置以下:

<!-- 使用EnterpriseCacheSessionDAO,將session放入到緩存,經過同步配置,將緩存同步到其餘集羣點上,解決session同步問題。 -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
    <!-- 活動session緩存名稱 -->
    <property name="activeSessionsCacheName" value="shiroActiveSessionCache" />
</bean>

<!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 使用EnterpriseCacheSessionDAO,解決session同步問題 -->
    <property name="sessionDAO" ref="sessionDAO" />
</bean>

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
    <!-- 啓用認證緩存,當用戶登陸一次後將不在查詢數據庫來獲取用戶信息,直接在從緩存獲取 -->
    <property name="authenticationCachingEnabled" value="true" />
    <!-- 認證緩存名稱 -->
    <property name="authenticationCacheName" value="shiroAuthenticationCache" />
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
    <!-- cacheManager,集合spring緩存工廠 -->
    <property name="cacheManager" ref="cacheManager" />
    <!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
    <property name="sessionManager" ref="sessionManager" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

<!-- spring對ehcache的緩存工廠支持 -->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml" />
</bean>

<!-- spring對ehcache的緩存管理 -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="ehCacheManagerFactory"></property>
</bean>

<!-- 使用緩存annotation 配置 -->
<cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />

在jdbcAuthenticationRealm這個bean裏啓用了認證緩存,而這個緩存的名稱是shiroAuthenticationCache。

提示:shiro默認不啓動認證緩存,若是須要啓用,必須在realm裏將authenticationCachingEnabled設置成true

ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>  

    <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" 
                                            properties="peerDiscovery=automatic,
                                            multicastGroupAddress=230.0.0.1,
                                            multicastGroupPort=4446,
                                            timeToLive=32" />

    <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> 

    <defaultCache maxElementsInMemory="10000" 
                  eternal="false" 
                  timeToIdleSeconds="300" 
                  timeToLiveSeconds="600" 
                  overflowToDisk="true"/>

    <!-- shiro的活動session緩存名稱 -->
    <cache name="shiroActiveSessionCache" 
                 maxElementsInMemory="10000" 
                 timeToLiveSeconds="1200" 
                 memoryStoreEvictionPolicy="LRU">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" 
                                   properties="replicateAsynchronously=false"/>
    </cache>

    <!-- shiro認證的緩存名稱 -->
    <cache name="shiroAuthenticationCache" 
                 maxElementsInMemory="10000" 
                 timeToLiveSeconds="1200" 
                 memoryStoreEvictionPolicy="LRU">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory 
                               properties="replicateAsynchronously=false""/>
    </cache>

</ehcache>

在ehcache.xml中添加了shiroAuthenticationCache緩存。而且memoryStoreEvictionPolicy屬性爲LRU,LRU就是當緩存滿了將「最近最少訪問」的緩存踢出。

那麼,經過以上配置完成了「熱用戶」,還有一步就是當修改用戶時,將緩存清除,讓下次這個用戶登陸時,從新去數據庫加載新的數據進行認證。

shiro在存儲受權用戶緩存時,會將用戶登陸帳戶作鍵,實體作值的方式進行存儲到緩存中。因此,當修改用戶時,經過用戶的登陸賬號,和spring的緩存註解,將該緩存清空。具體代碼以下:

@Repository
public class UserDao extends BasicHibernateDao<User, String> {

    /**經過登陸賬號獲取用戶實體**/
    public User getUserByUsername(String username) {
        return findUniqueByProperty("username", username);
    }


    /**經過用戶實體信息修改用戶**/
    //當更新後將shiro的認證緩存也更新,保證shiro和當前的用戶一致
    @CacheEvict(value="shiroAuthenticationCache",key="#entity.getUsername()")
    public void updateUser(User entity) {
        update(entity);
    }

    /**經過用戶實體刪除用戶**/
    //當更新後將shiro的認證緩存也更新,保證shiro和當前的用戶一致
    @CacheEvict(value="shiroAuthenticationCache",key="#entity.getUsername()")
    public void deleteUser(User entity) {
        delete(entity);
    }

}

經過以上代碼,當調用updateUser或deletUser方法完成後,spring cache 會將 shiroAuthenticationCache緩存塊裏key爲當前用戶的登陸賬號的緩存進行清除。

受權緩存的做用大部分是快速獲取用戶的認證信息,若是存在兩個集羣點,能夠直接使用同步的功能將緩存同步到其餘服務器裏,當下次訪問服務器時,當出現某臺服務器沒有進行受權工做時,不在進行受權的工做。具體配置以下:

applicationContext.xml:

<!-- 使用EnterpriseCacheSessionDAO,將session放入到緩存,經過同步配置,將緩存同步到其餘集羣點上,解決session同步問題。 -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
    <!-- 活動session緩存名稱 -->
    <property name="activeSessionsCacheName" value="shiroActiveSessionCache" />
</bean>

<!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 使用EnterpriseCacheSessionDAO,解決session同步問題 -->
    <property name="sessionDAO" ref="sessionDAO" />
</bean>

<!-- 自定義shiro的realm數據庫身份驗證 -->
<bean id="jdbcAuthenticationRealm" class="domain.JdbcAuthenticationRealm">
    <property name="name" value="jdbcAuthentication" />
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="MD5" />
        </bean>
    </property>
    <!-- 受權緩存名稱 -->
    <property name="authorizationCacheName" value="shiroAuthorizationCache" />
    <!-- 啓用認證緩存,當用戶登陸一次後將不在查詢數據庫來獲取用戶信息,直接在從緩存獲取 -->
    <property name="authenticationCachingEnabled" value="true" />
    <!-- 認證緩存名稱 -->
    <property name="authenticationCacheName" value="shiroAuthenticationCache" />
</bean>

<!-- 使用默認的WebSecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- realm認證和受權,從數據庫讀取資源 -->
    <property name="realm" ref="jdbcAuthenticationRealm" />
    <!-- cacheManager,集合spring緩存工廠 -->
    <property name="cacheManager" ref="cacheManager" />
    <!-- 考慮到集羣,使用DefaultWebSessionManager來作sessionManager -->
    <property name="sessionManager" ref="sessionManager" />
</bean>

<!-- 自定義對 shiro的鏈接約束,結合shiroSecurityFilter實現動態獲取資源 -->
<bean id="chainDefinitionSectionMetaSource" class="domian.ChainDefinitionSectionMetaSource">
    <!-- 默認的鏈接配置 -->
    <property name="filterChainDefinitions">
        <value>
            /login = captchaAuthc
            /logout = logout
            /index = perms[security:index]
        </value>
    </property>
</bean>

<!-- 將shiro與spring集合 -->
<bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="filters">
        <map>
            <entry key="captchaAuthc" value-ref="captchaAuthenticationFilter" />
        </map>
    </property>
    <!-- shiro的核心安全接口 -->
    <property name="securityManager" ref="securityManager" />
    <!-- 要求登陸時的連接 -->
    <property name="loginUrl" value="/login" />
    <!-- 登錄成功後要跳轉的鏈接 -->
    <property name="successUrl" value="/index" />
    <!-- 沒有權限要跳轉的連接-->
    <property name="unauthorizedUrl" value="/unauthorized" />
    <!-- shiro鏈接約束配置,在這裏使用自定義的動態獲取資源類 -->
    <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
</bean>

<!-- spring對ehcache的緩存工廠支持 -->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml" />
</bean>

<!-- spring對ehcache的緩存管理 -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="ehCacheManagerFactory"></property>
</bean>

<!-- 使用緩存annotation 配置 -->
<cache:annotation-driven cache-manager="ehCacheManager" proxy-target-class="true" />

在applicationContext.xml裏的jdbcAuthenticationRealm bean 添加了authorizationCacheName,值爲:shiroAuthorizationCache

提示:shiro默認啓動受權緩存,若是不想使用受權緩存,將會每次訪問到有perms的url都會受權一次。

ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>  

    <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" 
                                            properties="peerDiscovery=automatic,
                                            multicastGroupAddress=230.0.0.1,
                                            multicastGroupPort=4446,
                                            timeToLive=32" />

    <cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory" /> 

    <defaultCache maxElementsInMemory="10000" 
                  eternal="false" 
                  timeToIdleSeconds="300" 
                  timeToLiveSeconds="600" 
                  overflowToDisk="true"/>

    <!-- shiro的活動session緩存名稱 -->
    <cache name="shiroActiveSessionCache" 
                 maxElementsInMemory="10000" 
                 timeToLiveSeconds="1200" 
                 memoryStoreEvictionPolicy="LRU">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" 
                                   properties="replicateAsynchronously=false"/>
    </cache>

    <!-- shiro認證的緩存名稱 -->
    <cache name="shiroAuthenticationCache" 
                 maxElementsInMemory="10000" 
                 timeToLiveSeconds="1200" 
                 memoryStoreEvictionPolicy="LRU">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory 
                               properties="replicateAsynchronously=false""/>
    </cache>

    <!-- shiro受權的緩存名稱 -->
    <cache name="shiroAuthorizationCache" 
           maxElementsInMemory="10000" 
           timeToLiveSeconds="1200">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory 
                                   properties="replicateAsynchronously=false""/>

    </cache>

</ehcache>

在ehcache中添加shiroAuthorizationCache緩存。將完成受權緩存同步。當修改或刪除某些角色時,記得要清楚全部緩存,讓用戶下次訪問時受權一次,否則修改了角色須要重啓服務器後才能生效。

@Repository
public class GroupDao extends BasicHibernateDao<Group, String> {

    /**經過用戶實體信息修改用戶**/
    @CacheEvict(value="shiroAuthorizationCache",allEntries=true)
    public void updateGroup(Group entity) {
        update(entity);
    }

    /**經過用戶實體刪除用戶**/
    @CacheEvict(value="shiroAuthorizationCache",allEntries=true)
    public void deleteGroup(Group entity) {
        delete(entity);
    }

}

具體的過程在base-framework的showcase的base-curd項目下有例子,若是看不懂。能夠根據例子去理解。

2 dactiv orm 使用說明

dactiv orm 是對持久化層所使用到的框架進行封裝,讓使用起來不在寫如此多的繁瑣代碼,目前dactiv orm 只支持了 hibernate 4 和 spring data jpa。

dactiv orm 對 hibernate 和 spring data jpa 的修改並很少,經常使用的方法和往常同樣使用,除了 hibernate 的 save 方法更名爲insert(其實save也是起到了insert的做用,從字面上,insert更加形容了hibernate save方法的做用)其餘都和往常同樣使用。主要是添加了一些註解和一些簡單的執行某些方法先後作了一個攔截處理,以及添加一個靈活的屬性查詢功能。

2.1 使用 hibernate

在使用 hibernate 時,主要關注幾個類:

  1. BasicHibernateDao

  2. HibernateSupportDao

BasicHibernateDao:是對 hibernate 封裝的基礎類,包含對 hibernate 的基本CURD和其餘 hibernate 的查詢操做,該類是一個能夠支持泛型的CURD類,要是在寫dao時的繼承體,泛型參數中的 T 爲 orm 對象實體類型,PK爲實體的主鍵類型。在類中的 sessionFactory 已經使用了自動寫入:

@Autowired(required = false)
public void setSessionFactory(SessionFactory sessionFactory) {
    this.sessionFactory = sessionFactory;
}

只要用spring配置好sessionFactory就能夠繼承使用。

applicationContext.xml:

<!-- Hibernate配置 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="namingStrategy">
        <bean class="org.hibernate.cfg.ImprovedNamingStrategy" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${hibernate.dialect}</prop>
            <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
            <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
        </props>
    </property>
    <property name="packagesToScan" value="com.github.dactiv.showcase.entity" />
</bean>

UserDao:

public class UserDao extends BasicHibernateDao<User, String> {

}

2.2 使用 spring data jpa

在使用 spring data jpa 時,主要關注BasicJpaRepository這個接口,該接口添加了支持PropertyFilter的方法,能夠直接使用,但須要添加配置,要使用到BasicJpaRepository須要在spring data jpa配置文件中對jpa:repositories的factory-class屬性添加一個類:org.exitsoft.orm.core.spring.data.jpa.factory.BasicRepositoryFactoryBean:

<jpa:repositories base-package="你的repository包路徑" 
                              transaction-manager-ref="transactionManager" 
                              factory-class="org.exitsoft.orm.core.spring.data.jpa.factory.BasicRepositoryFactoryBean"
                              entity-manager-factory-ref="entityManagerFactory"  />

若是以爲麻煩,不配置同樣可以使用PropertyFilter來作查詢操做:

Specifications.get(Lists.newArrayList(
    PropertyFilters.get("LIKES_loginName", "m"),
    PropertyFilters.get("EQI_state", "1")
));

該方法會返回一個Specification接口,使用spring data jpa 原生的api findAll方法能夠直接使用,執行查詢操做:

repository.findAll(Specifications.get(Lists.newArrayList(
    PropertyFilters.get("LIKES_loginName", "m"),
    PropertyFilters.get("EQI_state", "1")
)));

2.3 PropertyFilter 查詢表達式說明

在 dactiv orm 裏,對 hibernate 和 spring data jpa 都擴展了一套查詢表達式,是專門用來應付一些比較簡單的查詢而不用寫語句的功能。經過該表達式,dactiv orm 可以解析出最終的查詢語句去讓 hibernate 或 spring data jpa 去執行,須要使用該表達式,若是是用 hibernate 須要集成 HibernateSupportDao 類,若是使用 spring data jpa 的話須要使用到 Specifications.get() 方法去構造 spring data jpa 的 Specification 後才能執行查詢,或者根據 2.2 使用 spring data jpa 配置完成後,集成BasicJpaRepository 接口,裏面就提供了支持 PropertyFilter 的查詢方法。

該表達式的規則爲:<約束名稱><屬性類型>_<屬性名稱>,例如如今有個用戶實體:

@Entity
@Table(name = "TB_ACCOUNT_USER")
public class User implements Serializable {
    private String id;//主鍵id
    private String username;//登陸名稱
    private String password;//登陸密碼
    private String realname;//真實名稱
    private Integer state;//狀態 
    private String email;//郵件

    //----------GETTER/SETTER-----------//
}

經過查詢表達式來查詢username等於a的用戶能夠這樣寫:

hibernate

public class UserDao extends HibernateSupportDao<User, String>{

}
List<PropertyFilter> filters = Lists.newArrayList(
    //<約束名稱><屬性類型>_<屬性名稱>
    PropertyFilters.get("EQS_username", "a")
);
userDao.findByPropertyFilter(filters);

spring data jpa

public interface UserRepository extends JpaRepository<User, String>,JpaSpecificationExecutor<User>{

}
userRepository.findAll(Specifications.get(Lists.newArrayList(
    //<約束名稱><屬性類型>_<屬性名稱>
    PropertyFilters.get("EQS_username", "a")
)));

查詢username等於a的而且realname等於c的用戶能夠經過多個條件進行and關係查詢:

hibernate

public class UserDao extends HibernateSupportDao<User, String>{

}
List<PropertyFilter> filters = Lists.newArrayList(
    //<約束名稱><屬性類型>_<屬性名稱>
    PropertyFilters.get("EQS_username", "a"),
    PropertyFilters.get("EQS_realname", "c")
);
userDao.findByPropertyFilter(filters);

spring data jpa

public interface UserRepository extends JpaRepository<User, String>,JpaSpecificationExecutor<User>{

}
userRepository.findAll(Specifications.get(Lists.newArrayList(
    //<約束名稱><屬性類型>_<屬性名稱>
    PropertyFilters.get("EQS_username", "a"),
    PropertyFilters.get("EQS_realname", "c")
)));

瞭解一些功能後,解釋一下 <約束名稱><屬性類型>_<屬性名稱> 應該怎麼寫。

約束名稱:約束名稱是表達式第一個參數的必須條件,在這裏的約束名稱是指經過什麼條件去作查詢,如等於、不等於、包含(in)、大於、小於...等等。

約束名稱描述列表:

約束名稱 描述
EQ 等於約束 (from object o where o.value = ?)若是爲"null"就是 (from object o where o.value is null)
NE 不等於約束 (from object o where o.value <> ?) 若是爲"null"就是 (from object o where o.value is not null)
IN 包含約束 (from object o where o.value in (?,?,?,?,?))
NIN 不包含約束 (from object o where o.value not in (?,?,?,?,?))
GE 大於等於約束 (from object o where o.value >= ?)
GT 大於約束 (from object o where o.value > ?)
LE 小於等於約束 ( from object o where o.value <= ?)
LT 小於約束 ( from object o where o.value < ?)
LIKE 模糊約束 ( from object o where o.value like '%?%')
LLIKE 左模糊約束 ( from object o where o.value like '%?')
RLIKE 右模糊約束 ( from object o where o.value like '?%')

屬性類型:屬性類型是表達式第二個參數的必須條件,表示表達式的屬性值是什麼類型的值。由於在使用表達式查詢時,參數都是String類型的參數,因此必須根據你指定的類型才能自動轉爲該類型的值,屬性類型的描述用一個枚舉類來表示,就是 dactiv common 下的 FieldType枚舉:

/**
 * 屬性數據類型
 * S表明String,I表明Integer,L表明Long, N表明Double, D表明Date,B表明Boolean
 * 
 * @author calvin
 * 
 */
public enum FieldType {

    /**
     * String
     */
    S(String.class),
    /**
     * Integer
     */
    I(Integer.class),
    /**
     * Long
     */
    L(Long.class),
    /**
     * Double
     */
    N(Double.class), 
    /**
     * Date
     */
    D(Date.class), 
    /**
     * Boolean
     */
    B(Boolean.class);

    //類型Class
    private Class<?> fieldClass;

    private FieldType(Class<?> fieldClass) {
        this.fieldClass = fieldClass;
    }

    /**
     * 獲取類型Class
     * 
     * @return Class
     */
    public Class<?> getValue() {
        return fieldClass;
    }
}

如,用戶對象中實體描述:

@Entity
@Table(name = "TB_ACCOUNT_USER")
public class User implements Serializable {
    private String id;//主鍵id
    private String username;//登陸名稱
    private String password;//登陸密碼
    private String realname;//真實名稱
    private Integer state;//狀態
    private String email;//郵件

    //----------GETTER/SETTER-----------//
}

假如我想查用戶狀態不等於3的能夠寫成:

PropertyFilters.get("NEI_state", "3")

假如我想查用戶的登陸名稱等於a的能夠寫成:

PropertyFilters.get("EQS_username", "a")

屬性名稱:屬性名稱就是實體的屬性名,可是要注意的是,經過表達式不能支持別名查詢如:

@Entity
@Table(name = "TB_ACCOUNT_USER")
public class User implements Serializable {
    private String id;//主鍵id
    private String username;//登陸名稱
    private String password;//登陸密碼
    private String realname;//真實名稱
    private Integer state;//狀態
    private String email;//郵件
    private List<Group> groupsList = new ArrayList<Group>();//用戶所在的組
    //----------GETTER/SETTER-----------//
}

想經過PropertyFilters.get("EQS_groupsList.name", "a")就報錯,但若是是一對多而且「多」這方的實體在User裏面能夠經過id查詢出來,如:

@Entity
@Table(name = "TB_ACCOUNT_USER")
public class User implements Serializable {
    private String id;//主鍵id
    private String username;//登陸名稱
    private String password;//登陸密碼
    private String realname;//真實名稱
    private Integer state;//狀態
    private String email;//郵件
    private Group group;//用戶所在的組
    //----------GETTER/SETTER-----------//
}
PropertyFilters.get("EQS_group.id", "1")

若是想使用別名的查詢,能夠經過重寫的方法來實現該功能,以 hibernate 爲例子,HibernateSupportDao的表達式查詢方法其實都是在用QBC形式的查詢,就是Criteria。在該類中都是經過:createCriteria建立Criteria的,因此。在UserDao中重寫該方法就能夠實現別名查詢了,如:

public class UserDao extends HibernateSupportDao<User, String>{

    protected Criteria createCriteria(List<PropertyFilter> filters,Order ...orders) {
        Criteria criteria = super.createCriteria(filters, orders);
        criteria.createAlias("groupsList", "gl");
        return criteria;
    }
}
PropertyFilters.get("EQS_groupsList.name", "mm")

表達式and與or的多值寫法:有時候會經過表達式去查詢某個屬性等於多個值或者多個屬性等於某個值的須要,在某個屬性等於多個值時,若是用and查詢的話把值用逗號","分割,若是用or查詢的話把值用橫槓"|"分割。如:

and

PropertyFilters.get("EQS_username", "1,2,3")

or:

PropertyFilters.get("EQS_username", "1|2|3")

在須要查詢多個屬性等於某個值時,使用OR分隔符隔開這些屬性:

PropertyFilters.get("EQS_username_OR_email", "xxx@xxx.xx");

2.4 頁面多條件查詢

多條件以及分頁查詢,可能每一個項目中都會使用,可是查詢條件變幻無窮,當某時客戶要求添加多一個查詢條件時,繁瑣的工做會不少,但使用 PropertyFilter 會爲你減小一些複製粘貼的動做。

以用戶實體類例:

@Entity
@Table(name = "TB_ACCOUNT_USER")
public class User implements Serializable {
    private String id;//主鍵id
    private String username;//登陸名稱
    private String password;//登陸密碼
    private String realname;//真實名稱
    private Integer state;//狀態
    private String email;//郵件
    //----------GETTER/SETTER-----------//
}

先在的查詢表單以下:

<form id="search_form" action="account/user/view" method="post">
    <label for="filter_RLIKES_username">
        登陸賬號:
    </label>
    <input type="text" id="filter_RLIKES_username" name="filter_RLIKES_username" />
    <label for="filter_RLIKES_realname">
        真實姓名:
    </label>
    <input type="text" id="filter_RLIKES_realname" name="filter_RLIKES_realname" />
    <label for="filter_RLIKES_email">
        電子郵件:
    </label>
    <input type="text" id="filter_RLIKES_email" name="filter_RLIKES_email" />
    <label for="filter_EQS_state">
        狀態:
    </label>
</form>

注意看每一個input的name屬性,在input的name裏經過filter_作前綴加查詢表達式,當表單提交過來時經過一下代碼完成查詢:

public class UserDao extends HibernateSupportDao<User, String>{

}
@RequestMapping("view")
public Page<User> view(PageRequest pageRequest,HttpServletRequest request) {

    List<PropertyFilter> filters = PropertyFilters.get(request, true);

    if (!pageRequest.isOrderBySetted()) {
        pageRequest.setOrderBy("id");
        pageRequest.setOrderDir(Sort.DESC);
    }

    return userDao.findPage(pageRequest, filters);
}

當客戶在某時想添加一個經過狀態查詢時,只須要在表單中添加多一個select便可完成查詢。

<form id="search_form" action="account/user/view" method="post">
    <...>
    <select name="filter_EQI_state" id="filter_EQS_state" size="25">
        <option value="">
            所有
        </option>
        <option value="1">
            禁用
        </option>
        <option value="2">
            啓用
        </option>
    </select>
</form>

2.5 擴展表達式的約束名稱

若是你在項目開發時以爲表達式裏面的約束名稱不夠用,能夠對錶達式作擴展處理。擴展約束名稱時 spring data jpa 和 hibernate 所關注的類不一樣。

2.5.1 hiberante擴展表達式的約束名稱

要擴展 hibernate 查詢表達式的約束名主要關注的類有 HibernateRestrictionBuilder, CriterionBuilder,CriterionSingleValueSupport 以及 CriterionMultipleValueSupport

HibernateRestrictionBuilder:HibernateRestrictionBuilder 是裝載全部 CriterionBuilder 實現類的包裝類,該類有一塊靜態局域。去初始化全部的約束條件。並提供兩個方法去建立 Hibernate 的 Criterion,該類是 HibernateSupportDao 查詢表達式查詢的關鍵類。全部經過條件建立的 Criterion 都是經過該類建立。

CriterionBuilder:CriterionBuilder是一個構造Hibernate Criterion的類,該類有一個方法專門提供根據 PropertyFilter 該如何建立 hibernate 的 Criterion,而該類有一個抽象類實現了部分方法,就是 CriterionSingleValueSupport,具體 CriterionBuilder 的接口以下:

public interface CriterionBuilder {

    /**
     * 獲取Hibernate的約束標準
     * 
     * @param filter 屬性過濾器
     * 
     * @return {@link Criterion}
     * 
     */
    public Criterion build(PropertyFilter filter);

    /**
     * 獲取Criterion標準的約束名稱
     * 
     * @return String
     */
    public String getRestrictionName();

    /**
     * 獲取Hibernate的約束標準
     * 
     * @param propertyName 屬性名
     * @param value 值
     * 
     * @return {@link Criterion}
     * 
     */
    public  Criterion build(String propertyName,Object value);
}

CriterionSingleValueSupport:該類是CriterionBuilder的子類,實現了public Criterion build(PropertyFilter filter)實現體主要是對PropertyFilter的值模型作處理。而且逐個循環調用public Criterion build(String propertyName,Object value)方法給 CriterionSingleValueSupport 實現體作處理。

public abstract class CriterionSingleValueSupport implements CriterionBuilder{

    //or值分隔符
    private String orValueSeparator = "|";
    //and值分隔符
    private String andValueSeparator = ",";

    public CriterionSingleValueSupport() {

    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.hibernate.CriterionBuilder#build(com.github.dactiv.orm.core.PropertyFilter)
     */
    public Criterion build(PropertyFilter filter) {
        String matchValue = filter.getMatchValue();
        Class<?> FieldType = filter.getFieldType();

        MatchValue matchValueModel = getMatchValue(matchValue, FieldType);

        Junction criterion = null;

        if (matchValueModel.hasOrOperate()) {
            criterion = Restrictions.disjunction();
        } else {
            criterion = Restrictions.conjunction();
        }

        for (Object value : matchValueModel.getValues()) {

            if (filter.hasMultiplePropertyNames()) {
                List<Criterion> disjunction = new ArrayList<Criterion>();
                for (String propertyName:filter.getPropertyNames()) {
                    disjunction.add(build(propertyName,value));
                }
                criterion.add(Restrictions.or(disjunction.toArray(new Criterion[disjunction.size()])));
            } else {
                criterion.add(build(filter.getSinglePropertyName(),value));
            }

        }

        return criterion;
    }


    /**
     * 獲取值對比模型
     * 
     * @param matchValue 值
     * @param FieldType 值類型
     * 
     * @return {@link MatchValue}
     */
    public MatchValue getMatchValue(String matchValue,Class<?> FieldType) {
        return MatchValue.createMatchValueModel(matchValue, FieldType,andValueSeparator,orValueSeparator);
    }

    /**
     * 獲取and值分隔符
     * 
     * @return String
     */
    public String getAndValueSeparator() {
        return andValueSeparator;
    }

    /**
     * 設置and值分隔符
     * @param andValueSeparator and值分隔符
     */
    public void setAndValueSeparator(String andValueSeparator) {
        this.andValueSeparator = andValueSeparator;
    }

}

CriterionMultipleValueSupport:該類是 CriterionSingleValueSupport 的子類。重寫了CriterionSingleValueSupport類的public Criterion build(PropertyFilter filter)  public Criterion build(String propertyName, Object value) 方法。而且添加了一個抽象方法 public abstract Criterion buildRestriction(String propertyName,Object[] values) 。該類主要做用是在多值的狀況不逐個循環,而是將全部的參數組合成一個數組傳遞給抽象方法buildRestriction(String propertyName,Object[] values)中。這種狀況在in或not in約束中就用獲得。

public abstract class CriterionMultipleValueSupport extends CriterionSingleValueSupport{

    /**
     * 將獲得值與指定分割符號,分割,獲得數組
     *  
     * @param value 值
     * @param type 值類型
     * 
     * @return Object
     */
    public Object convertMatchValue(String value, Class<?> type) {
        Assert.notNull(value,"值不能爲空");
        String[] result = StringUtils.splitByWholeSeparator(value, getAndValueSeparator());

        return  ConvertUtils.convertToObject(result,type);
    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.hibernate.restriction.CriterionSingleValueSupport#build(com.github.dactiv.orm.core.PropertyFilter)
     */
    public Criterion build(PropertyFilter filter) {
        Object value = convertMatchValue(filter.getMatchValue(), filter.getFieldType());
        Criterion criterion = null;
        if (filter.hasMultiplePropertyNames()) {
            Disjunction disjunction = Restrictions.disjunction();
            for (String propertyName:filter.getPropertyNames()) {
                disjunction.add(build(propertyName,value));
            }
            criterion = disjunction;
        } else {
            criterion = build(filter.getSinglePropertyName(),value);
        }
        return criterion;
    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.hibernate.CriterionBuilder#build(java.lang.String, java.lang.Object)
     */
    public Criterion build(String propertyName, Object value) {

        return buildRestriction(propertyName, (Object[])value);
    }


    /**
     * 
     * 獲取Hibernate的約束標準
     * 
     * @param propertyName 屬性名
     * @param values 值
     * 
     * @return {@link Criterion}
     */
    public abstract Criterion buildRestriction(String propertyName,Object[] values);
}

瞭解完以上幾個類。那麼假設如今有個需求。要寫一個模糊非約束 (from object o where o.value not like '%?%')來判斷一些值,能夠經過繼承CriterionSingleValueSupport類,實現public Criterion build(String propertyName, Object value),如:

/**
 * 模糊非約束 ( from object o where o.value not like '%?%') RestrictionName:NLIKE
 * 表達式:NLIKE_屬性類型_屬性名稱[|屬性名稱...]
 * 
 * @author vincent
 *
 */
public class NlikeRestriction extends CriterionSingleValueSupport{

    public final static String RestrictionName = "NLIKE";

    @Override
    public String getRestrictionName() {
        return RestrictionName;
    }

    @Override
    public Criterion build(String propertyName, Object value) {

        return Restrictions.not(Restrictions.like(propertyName, value.toString(), MatchMode.ANYWHERE));
    }

}

經過某種方式(如spring的InitializingBean,或serlvet)將該類添加到HibernateRestrictionBuilder的CriterionBuilder中。就可使用約束名了。

CriterionBuilder nlikeRestriction= new NlikeRestriction();
HibernateRestrictionBuilder.getCriterionMap().put(nlikeRestriction.getRestrictionName(), nlikeRestriction);
2.5.2 spring data jpa擴展表達式的約束名稱

若是使用 spring data jpa 作 orm 支持時,要擴展查詢表達式的約束名主要關注的類有 JpaRestrictionBuilder, PredicateBuilder,PredicateSingleValueSupport 以及 PredicateMultipleValueSupport

JpaRestrictionBuilder:JpaRestrictionBuilder 是裝載全部 PredicateBuilder 實現的包裝類,該類有一塊靜態局域。去初始化全部的約束條件。並提供兩個方法去建立 jpa 的 Predicate,該類是 BasicJpaRepository 查詢表達式查詢的關鍵類。全部經過 PropertyFilter 建立的 Predicate 都是經過該類建立。

PredicateBuilder:PredicateBuilder 是一個構造 jpa Predicate 的類,該類有一個方法專門提供根據 PropertyFilter 該如何建立 jpa 的 Predicate,而該類有一個抽象類實現了部分方法,就是 PredicateSingleValueSupport,具體 PredicateBuilder 的接口以下:

public interface PredicateBuilder {

    /**
     * 獲取Jpa的約束標準
     * 
     * @param filter 屬性過濾器
     * @param entity jpa查詢綁定載體
     * 
     * @return {@link Predicate}
     * 
     */
    public Predicate build(PropertyFilter filter,SpecificationEntity entity);

    /**
     * 獲取Predicate標準的約束名稱
     * 
     * @return String
     */
    public String getRestrictionName();

    /**
     * 獲取Jpa的約束標準
     * 
     * @param propertyName 屬性名
     * @param value 值
     * @param entity jpa查詢綁定載體
     * 
     * @return {@link Predicate}
     * 
     */
    public Predicate build(String propertyName, Object value,SpecificationEntity entity);
}

PredicateSingleValueSupport:該類是PredicateBuilder的子類,實現了 public Predicate build(PropertyFilter filter,SpecificationEntity entity) 方法,實現體主要是對 PropertyFilter 的值模型作處理。而且逐個循環調用 public Predicate build(String propertyName, Object value,SpecificationEntity entity) 方法給實現體作處理:

public abstract class PredicateSingleValueSupport implements PredicateBuilder{

    //or值分隔符
    private String orValueSeparator = "|";
    //and值分隔符
    private String andValueSeparator = ",";

    public PredicateSingleValueSupport() {

    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.spring.data.jpa.PredicateBuilder#build(com.github.dactiv.orm.core.PropertyFilter, javax.persistence.criteria.Root, javax.persistence.criteria.CriteriaQuery, javax.persistence.criteria.CriteriaBuilder)
     */
    public Predicate build(PropertyFilter filter,SpecificationEntity entity) {

        String matchValue = filter.getMatchValue();
        Class<?> FieldType = filter.getFieldType();

        MatchValue matchValueModel = getMatchValue(matchValue, FieldType);

        Predicate predicate = null;

        if (matchValueModel.hasOrOperate()) {
            predicate = entity.getBuilder().disjunction();
        } else {
            predicate = entity.getBuilder().conjunction();
        }

        for (Object value : matchValueModel.getValues()) {
            if (filter.hasMultiplePropertyNames()) {
                for (String propertyName:filter.getPropertyNames()) {
                    predicate.getExpressions().add(build(propertyName, value, entity));
                }
            } else {
                predicate.getExpressions().add(build(filter.getSinglePropertyName(), value, entity));
            }
        }

        return predicate;
    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.spring.data.jpa.PredicateBuilder#build(java.lang.String, java.lang.Object, com.github.dactiv.orm.core.spring.data.jpa.JpaBuilderModel)
     */
    public Predicate build(String propertyName, Object value,SpecificationEntity entity) {

        return build(Specifications.getPath(propertyName, entity.getRoot()),value,entity.getBuilder());
    }

    /**
     * 
     * 獲取Jpa的約束標準
     * 
     * @param expression 屬性路徑表達式
     * @param value 值
     * @param builder CriteriaBuilder
     * 
     * @return {@link Predicate}
     */
    public abstract Predicate build(Path<?> expression,Object value,CriteriaBuilder builder);

    /**
     * 獲取值對比模型
     * 
     * @param matchValue 值
     * @param FieldType 值類型
     * 
     * @return {@link MatchValue}
     */
    public MatchValue getMatchValue(String matchValue,Class<?> FieldType) {
        return MatchValue.createMatchValueModel(matchValue, FieldType,andValueSeparator,orValueSeparator);
    }

    /**
     * 獲取and值分隔符
     * 
     * @return String
     */
    public String getAndValueSeparator() {
        return andValueSeparator;
    }

    /**
     * 設置and值分隔符
     * @param andValueSeparator and值分隔符
     */
    public void setAndValueSeparator(String andValueSeparator) {
        this.andValueSeparator = andValueSeparator;
    }
}

PredicateMultipleValueSupport:該類是 PredicateSingleValueSupport 的子類。重寫了 PredicateSingleValueSupport 類的public Predicate build(PropertyFilter filter, SpecificationEntity entity)  public Predicate build(Path<?> expression, Object value,CriteriaBuilder builder)。而且添加了一個抽象方法 public abstract Predicate buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder)。該類主要做用是在多值的狀況不逐個循環,而是所有都將參數組合成一個數組傳遞給抽象方法 buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder) 中。這種狀況在in或not in約束中就用獲得。

public abstract class PredicateMultipleValueSupport extends PredicateSingleValueSupport{

    /**
     * 將獲得值與指定分割符號,分割,獲得數組
     *  
     * @param value 值
     * @param type 值類型
     * 
     * @return Object
     */
    public Object convertMatchValue(String value, Class<?> type) {
        Assert.notNull(value,"值不能爲空");
        String[] result = StringUtils.splitByWholeSeparator(value, getAndValueSeparator());

        return  ConvertUtils.convertToObject(result,type);
    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.spring.data.jpa.restriction.PredicateSingleValueSupport#build(com.github.dactiv.orm.core.PropertyFilter, com.github.dactiv.orm.core.spring.data.jpa.JpaBuilderModel)
     */
    public Predicate build(PropertyFilter filter, SpecificationEntity entity) {
        Object value = convertMatchValue(filter.getMatchValue(), filter.getFieldType());
        Predicate predicate = null;

        if (filter.hasMultiplePropertyNames()) {
            Predicate orDisjunction = entity.getBuilder().disjunction();
            for (String propertyName:filter.getPropertyNames()) {
                orDisjunction.getExpressions().add(build(propertyName,value,entity));
            }
            predicate = orDisjunction;
        } else {
            predicate = build(filter.getSinglePropertyName(),value,entity);
        }

        return predicate;
    }

    /*
     * (non-Javadoc)
     * @see com.github.dactiv.orm.core.spring.data.jpa.restriction.PredicateSingleValueSupport#build(javax.persistence.criteria.Path, java.lang.Object, javax.persistence.criteria.CriteriaBuilder)
     */
    public Predicate build(Path<?> expression, Object value,CriteriaBuilder builder) {
        return buildRestriction(expression,(Object[])value,builder);
    }

    /**
     * 獲取Jpa的約束標準
     * 
     * @param expression root路徑
     * @param values 值
     * @param builder CriteriaBuilder
     * 
     * @return {@link Predicate}
     */
    public abstract Predicate buildRestriction(Path<?> expression,Object[] values,CriteriaBuilder builder);
}

瞭解完以上幾個類。那麼假設如今有個需求。要寫一個模糊約束 (from object o where o.value like '%?%')來判斷一些值,能夠經過繼承PredicateSingleValueSupport類,實現build(Path<?> expression,Object value,CriteriaBuilder builder),如:

/**
 * 模糊約束 ( from object o where o.value like '%?%') RestrictionName:LIKE
 * <p>
 * 表達式:LIKE屬性類型_屬性名稱[_OR_屬性名稱...]
 * </p>
 * 
 * @author vincent
 *
 */
public class LikeRestriction extends PredicateSingleValueSupport{

    public String getRestrictionName() {
        return RestrictionNames.LIKE;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Predicate build(Path expression, Object value,CriteriaBuilder builder) {

        return builder.like(expression, "%" + value + "%");
    }



}

經過某種方式(如spring的InitializingBean,或serlvet)將該類添加到JpaRestrictionBuilder的PredicateBuilder中。就可使用約束名了。

PredicateBuilder nlikeRestriction= new NlikeRestriction();
JpaRestrictionBuilder.getCriterionMap().put(nlikeRestriction.getRestrictionName(), nlikeRestriction);

對於 hibernate 和 spring data jpa 的約束名在base-framework的 dactiv orm 都會有例子。若是不明白。能夠看例子理解。

2.6 註解

在 dactiv orm 中,擴展一些比較經常使用的註解,如:狀態刪除、防僞安全碼等。當須要執行某些特定的需求時,無需在寫繁瑣的代碼。而是直接加上註解便可。

狀態刪除:在大部分的業務系統中有某些表對於 delete 操做都有這麼一個需求,就是不物理刪除,只把狀態改爲修改狀態。dactiv orm提供了這種功能。只要在實體類加上 @StateDelete 註解後調用 orm 框架的刪除方法,將會改變實體某個字段的值作已刪除的狀態值。例若有一個User實體,在刪除時要改變 state 值爲3,看成已刪除記錄。能夠這樣寫實體:

@Entity
@Table(name = "TB_ACCOUNT_USER")
@StateDelete(propertyName = "state", value = "3")
public class User implements Serializable {
    //主鍵id
    private Integer id;
    //狀態 
    private Integer state;
    //----------GETTER/SETTER-----------//
}

hibernate

public class UserDao extends BasicHibernateDao<User, Integer> {

}

調用 deleteByEntity 方法後會生成如下sql語句:

User user = userDao.load(1);
userDao.deleteByEntity(user);

sql:update tb_account_user set state = ? where id = ?

spring data jpa

public interface UserDao extends BasicJpaRepository<User, Integer> {

}

調用 delete 方法後會生成如下sql語句:

userDao.delete(1);
sql:update tb_account_user set state = ? where id = ?

若是 orm 框架爲 spring data jpa 而且要用到這個功能,記得把.BasicRepositoryFactoryBean類配置到jpa:repositories標籤的factory-class屬性中。

安全碼:在某些業務系統中,有某些表涉及到錢的敏感字段時,爲了防止數據被串改,提到了安全碼功能。就是在表裏面加一個字段,該字段的值是表中的主鍵id和其餘比較敏感的字段值併合起來加密存儲到安全碼字段的值。當下次更新時記錄時,會先用id獲取數據庫的記錄,並再次將數據加密與安全嗎字段作對比,若是安全碼不相同。表示該記錄有問題。將丟出異常。

dactiv orm 提供了這種功能。只要在實體類加上 @SecurityCode 註解後調用 orm 框架的update方法時,將根據id獲取一次記錄並加密去對比安全嗎。若是相同執行 update 操做,不然丟出 SecurityCodeNotEqualException 異常。

記錄用上面的用戶實體

@Entity
@Table(name = "TB_ACCOUNT_USER")
@SecurityCode(value="securityCode",properties={"money"})
@StateDelete(propertyName = "state", value = "3")
public class User implements Serializable {
    //主鍵id
    private Integer id;
    //狀態 
    private Integer state;
    //帳戶餘額
    private Double money;
    //安全嗎
    private String securityCode;
    //----------GETTER/SETTER-----------//
}

經過以上代碼,在更新時,會將 id + money 的值鏈接起來,並使用md5加密的方式加密一個值,賦值到 securityCode 中:

hibernate

public class UserDao extends BasicHibernateDao<User, Integer> {

}
User user = userDao.load(1);
user.setMoney(1000000.00);
userDao.update(user);
//or userDao.save(user);

spring data jpa:

public interface UserDao extends BasicJpaRepository<User, Integer> {

}
User user = userDao.findOne(1);
user.setMoney(1000000.00);
userDao.save(user);

若是orm 框架爲 spring data jpa 而且要用到這個功能,記得把.BasicRepositoryFactoryBean類配置到jpa:repositories標籤的factory-class屬性中。

樹形實體:某些時候,在建立自身關聯一對多時,每每會出現 n + 1 的問題。就是在遍歷樹時,獲取子節點永遠都會去讀多一次數據庫。特別是用到 orm 框架時,lazy功能會主要你調用到該方法,java代理就去會執行獲取屬性的操做,同時也產生了數據庫的訪問。爲了不這些 n + 1 不少人想到了在實體中加多一個屬性去記錄該節點是否包含子節點,若是包含就去讀取,不然將不讀取。如如下實體:

@Entity
@Table(name = "TB_ACCOUNT_RESOURCE")
public class Resource implements Serializable{

    //主鍵id
    private Integer id;
    //名稱
    private String name;
    //父類
    private Resource parent;
    //是否包含葉子節點
    private Boolean leaf = Boolean.FALSE;
    //子類
    private List<Resource> children = new ArrayList<Resource>();

}

經過該實體,能夠經過leaf字段是判斷是否包含子節點,當等於true時表示有子節點,能夠調用getChildren()方法去讀取子節點信息。但如今問題是。要管理這個 leaf 字段彷佛每次插入、保存、刪除操做都會涉及到該值的修改問題。如:當前數據沒有子節點,但添加了一個子節點進去後。當前數據的 leaf 字段要改爲true。又或者:當前數據存在子節點,但子節點刪除完時,要更新 leaf 字段成 false。又或者,當錢數據存在子節點,但 update 時移除了子節點,要更新 leaf 字段成 false。這些狀況下只要調用到 update、 insert、 delete方法都要去更新一次數據。

dactiv orm 提供了這種功能。只要在實體類加上 @TreeEntity 註解後調用增、刪、改操做會幫你維護該leaf字段。

@Entity
@TreeEntity
@Table(name = "TB_ACCOUNT_RESOURCE")
public class Resource implements Serializable{

    //主鍵id
    private Integer id;
    //名稱
    private String name;
    //父類
    private Resource parent;
    //是否包含葉子節點
    private Boolean leaf = Boolean.FALSE;
    //子類
    private List<Resource> children = new ArrayList<Resource>();

}

TreeEntity註解裏面有一個屬性爲refreshHql,該屬性是一個hql語句,在調用增、刪、該方法時,會使用該語句去查一次數據,將全部的數據狀態爲:沒子節點但leaf又等於true的數據加載出來,並設置這些數據的 leaf 屬性爲 false 值。

TreeEntity註解源碼

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeEntity {

    /**
     * 是否包含葉子的標記屬性名
     * 
     * @return String
     */
    public String leafProperty() default "leaf";

    /**
     * tree實體的父類屬性名
     * 
     * @return String
     */
    public String parentProperty() default "parent";

    /**
     * 刷新的hql語句
     * 
     * @return String
     */
    public String refreshHql() default "from {0} tree " +
                                       "where tree.{1} = {2} and (" + 
                                       "    select count(c) from {0} c " + 
                                       "    where c.{3}.{4} = tree.{4} " +
                                       ") = {5}";

    /**
     * 是否包含葉子的標記屬性類型
     * 
     * @return Class
     */
    public Class<?> leafClass() default Boolean.class;

    /**
     * 若是是包含葉子節點須要設置的值
     * 
     * @return String
     */
    public String leafValue() default "1";

    /**
     * 若是不是包含葉子節點須要設置的值
     * 
     * @return String
     */
    public String unleafValue() default "0";

}

若是 orm 框架爲 spring data jpa 而且要用到這個功能,記得把.BasicRepositoryFactoryBean類配置到jpa:repositories標籤的factory-class屬性中。

對於 dactiv orm 使用所講的一切在base-framework的dactiv-orm項目中都有例子,若是不懂。能夠看例子去理解。

相關文章
相關標籤/搜索