SSM框架整合Shiro後的開發

手摸手教你SSM框架整合Shiro後的開發html

前面,咱們學習了Shiro實現權限管理之表結構設計以及JQuery-Ztree.js使用範例 ,接下來就詳細介紹一下SSM框架整合Shiro框架後的開發。一樣推薦你們參看張開濤老師的 跟我學Shiro ,或者能夠看個人筆記:Shiro實現受權Shiro實現身份認證java

若是你對SSM框架的整合不是很熟悉,你或許能夠參看個人這個項目SSM框架整合git

下面咱們就開始實現一個SSM+Shiro的權限管理項目吧!github

<!--more-->web

測試環境算法

IDEA + Tomcat8 + Mavenspring

起步

初始化數據庫,請參考/db中的代碼sql

導入依賴

導入Shiro框架須要的依賴:數據庫

shiro-core-1.3.2.jar shiro-ehcache-1.3.2.jar shiro-quartz-1.3.2.jar shiro-spring-1.3.2.jar shiro-web-1.3.2.jarapache

其餘依賴請參看項目中的pom.xml 文件

搭建SSM框架

搭建SSM框架的過程這裏再也不詳細說了,能夠參看個人SSM框架整合案例

<br/>

SSM框架整合Shiro

環境配置

1.在web.xml中配置Shiro的過濾器

與Spring集成:

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

和SpringMVC框架相似,Shiro框架也須要在web.xml中配置一個過濾器。DelegatingFilterProxy會自動到Spring容器中name爲shiroFilter的bean,而且將全部Filter的操做都委託給他管理。

這就要求在Spring配置中必須注入這樣一個這樣的Bean:

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"></bean>

此處bean的idweb.xml中Shiro過濾器的名稱<filter-name>必須是相同的,不然Shiro會找不到這個Bean。

2.spring-shiro-web.xml

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

    <!-- Shiro的Web過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的安全管理器,全部關於安全的操做都會通過SecurityManager -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 系統認證提交地址,若是用戶退出即session丟失就會訪問這個頁面 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 權限驗證失敗跳轉的頁面,須要配合Spring的ExceptionHandler異常處理機制使用 -->
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
            </util:map>
        </property>
        <!-- 自定義的過濾器鏈,從上向下執行,通常將`/**`放到最下面 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- 靜態資源不攔截 -->
                /static/** = anon
                /lib/** = anon
                /js/** = anon

                <!-- 登陸頁面不攔截 -->
                /login.jsp = anon
                /login.do = anon

                <!-- Shiro提供了退出登陸的配置`logout`,會生成路徑爲`/logout`的請求地址,訪問這個地址即會退出當前帳戶並清空緩存 -->
                /logout = logout

                <!-- user表示身份經過或經過記住我經過的用戶都能訪問系統 -->
                /index.jsp = user

                <!-- `/**`表示全部請求,表示訪問該地址的用戶是身份驗證經過或RememberMe登陸的均可以 -->
                /** = user
            </value>
        </property>
    </bean>

    <!-- 基於Form表單的身份驗證過濾器 -->
    <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
        <property name="usernameParam" value="username"/>
        <property name="passwordParam" value="password"/>
        <property name="loginUrl" value="/login.jsp"/>
    </bean>
    
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="userRealm"/>
    </bean>

    <!-- Realm實現 -->
    <bean id="userRealm" class="cn.tycoding.realm.UserRealm"></bean>
</bean>

上面配置文件的核心處就是Shiro的web過濾器的配置,固然由於Shiro的全部涉及安全的操做都要通過DefaultWebSecurityManager安全管理器,因此shiroFilter首先就要將其交給SecurityManager管理。loginUrl是帳戶退出或者session丟失就跳轉的地址;unauthorizedUrl是帳戶權限驗證失敗跳轉的地址,好比帳戶權限不夠等;而後就是過濾器鏈filterChainDefinitions的配置,他和咱們以前配置的.ini文件很是類似,其中主要就是配置資源的的攔截。Shiro提供了不少默認的攔截器,好比什麼驗證,受權等,這裏舉例幾個比較經常使用的默認攔截器:

<style> table th:first-of-type { width: 100px; } </style>

默認攔截器名 說明
authc 基於表單的攔截器,好比若用戶沒有登陸就會跳轉到loginUrl的地址,其攔截的請求必須是經過登陸驗證的,即Subject.isAuthenticated() == true的帳戶才能訪問
anon 匿名攔截器,和authc攔截器恰好做用相反。anon配置的請求容許用戶爲登陸就等訪問,通常咱們配置登陸頁面和靜態CSS等資源是容許匿名訪問
logout 退出攔截器,Shiro提供了一個退出的功能,配置了/logout = logout,Shiro就會生成一個虛擬的映射路徑,當用戶訪問了這個路徑,Shiro會自動清空緩存並跳轉到loginUrl頁面
user 用戶攔截器,和authc攔截器很相似,都是帳戶爲登陸的進行攔截並跳轉到loginUrl地址;不一樣之處在於authc容許帳戶必須是經過Subject.siAuthenticated() ==true的;而user不只容許登陸帳戶訪問,經過rememberMe登陸的用戶也能訪問

Shiro實現身份認證

身份認證的流程

若是用戶爲登陸,將跳轉到loginUrl進行登陸,登陸表單中,包含了兩個主要參數:用戶名username、密碼password(這兩個參數名稱不是固定的,可是要和FormAuthenticationFilter表單過濾器的參數配置要對應)。

  1. 用戶輸入這兩個用戶名和密碼後提交表單,經過綁定了SecurityManager的SecurityUtils獲得Subject實例,而後獲取身份驗證的UsernamePasswordToken傳入用戶名和密碼。
  2. 調用subject.login(token)進行登陸,SecurityManager會委託Authenticator把相應的token傳給Realm,從Realm中獲取身份認證信息。
  3. Realm能夠是本身實現的Realm,Realm會根據傳入的用戶名和密碼去數據庫進行校驗(提供Service層登陸接口)。
  4. Shiro從Realm中獲取安全數據(如用戶、身份、權限等),若是校驗失敗,就會拋出異常,登陸失敗;不然就登陸成功。
@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login(
            @RequestParam(value = "username", required = false) String username,
            @RequestParam(value = "password", required = false) String password,
            Model model) {
        String error = null;
        if (username != null && password != null) {
            //初始化
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            try {
                //登陸,即身份校驗,由經過Spring注入的UserRealm會自動校驗輸入的用戶名和密碼在數據庫中是否有對應的值
                subject.login(token);
                return "redirect:index.do";
            }catch (Exception e){
                e.printStackTrace();
                error = "未知錯誤,錯誤信息:" + e.getMessage();
            }
        } else {
            error = "請輸入用戶名和密碼";
        }
        //登陸失敗,跳轉到login頁面,這裏不作登陸成功的處理,由
        model.addAttribute("error", error);
        return "login";
    }
}

拓展

如上,當login()映射方法獲得用戶輸入的用戶名和密碼後調用subject.login(token)進行登陸,隨後就是經過Realm進行登陸校驗,若是登陸失敗就可能拋出一系列異常,好比UnknownAccountException用戶帳戶不存在異常、IncorrectCredentialsException用戶名或密碼錯誤異常、LockedAccountException帳戶鎖定異常... 。

可能,你也看到有些示例中在Controller層中沒有處理登陸成功,而是在ShiroFilterFactoryBean中配置successUrl,不少博文中講到:若是登陸成功Shiro會自動跳轉到登陸前訪問的地址,若是找不到登陸前訪問的地址,就會跳轉到successUrl中配置的地址;But,我在測試中並無看到這種特性,你們能夠研究一波。

認證相關的攔截器

與登陸認證相關的攔截器在前面spring-shiro-web配置文件中已經講到了。主要是使用Shiro提供的默認攔截器配置請求資源資源的攔截和驗證,如:

<!-- 靜態資源不攔截 -->
/static/** = anon
/lib/** = anon
/js/** = anon

<!-- 登陸頁面不攔截 -->
/login.jsp = anon
/login.do = anon

...

運行項目,若是用戶沒有輸入用戶名和密碼或者輸入的用戶名或密碼有誤等,將會拋出異常並從新跳轉到loginUrl地址上,若是正確輸入用戶名和密碼(數據庫中存在的)將跳轉到系統首頁index.do。那麼:咱們在Controller僅僅調用了subject.login(token),Shiro是怎樣進行登陸驗證的呢?

那咱們就要分析一下自定義的Realm了:

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 權限校驗
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    /**
     * 身份校驗
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        User user = userService.findByName(username);
        if (user == null) {
            throw new UnknownAccountException(); //沒有找到帳號
        }
        if (Boolean.TRUE.equals(user.getLocked())) {
            throw new LockedAccountException(); //帳號鎖定
        }
        //交給AuthenticationRealm使用CredentialsMatcher進行密碼匹配
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.getUsername(), //用戶名
                user.getPassword(), //密碼
                getName() //realm name
        );
        return authenticationInfo;
    }
}

Shiro從Realm中獲取安全數據,咱們能夠自定義多個Realm實現,但都要在SecurityManager中定義。通常咱們自定義實現的Realm繼承AuthorizingRealm(受權)便可,它繼承了AuthenticatingRealm(身份驗證);因此自定義Realm通常存在兩個最主要的功能:1.身份驗證;2.權限校驗。

在用戶登陸後,Controller會接收到用戶輸入的用戶名和密碼,並調用subject.login(token)進行登陸,實際上SecurityManager會委託Authenticator調用自定義的Realm進行身份驗證。要知道,調用Realm傳入的並不直接是用戶名和密碼,而是在Controller中綁定了用戶名和密碼的Token對象,那麼你首先要清楚身份驗證中兩個重要的參數:

<style> table th:first-of-type { width: 100px; } </style>

屬性名稱 做用
principals 身份,主體的惟一標識,好比用戶名、郵箱等,若是你將用戶名和密碼傳給了Token對象,那麼在Token對象中就能getPrincipal獲取這個標識
credentials 證實、憑證。好比密碼、數字證書等。可是在Shiro等安全框架中,相似於密碼這種數據通常都是通過加密處理的,它肯能不僅僅是密碼的數據,後面講

瞭解了上述兩個參數後,下面天然是從token對象中調用token.getPrincipal()獲取用戶名,而後調用Service層方法根據這個用戶名查詢數據庫中是否存在一個密碼與其對應,根據返回的User對象,最後經過Shiro提供的SimpleAuthenticationInfo進行密碼匹配。SimpleAuthenticationInfo存在多個構造方法:

public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {}

public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {}

public SimpleAuthenticationInfo(PrincipalCollection principals, Object credentials) {}

public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCredentials, ByteSource credentialsSalt) {}

SimpleAuthenticationInfo類提供了多個構造方法,可是通常而言咱們的密碼是通過加密的(後面講);如此Shiro會自動根據token中的用戶名和密碼與從數據庫中查詢到的數據進行匹配,若是匹配成功就登陸成功,否者就拋出異常。

註銷(退出)

註銷登陸就簡單不少了,在之前咱們都是手動寫一個請求映射方法,當用戶調用這個請求的時候,手動清空Session,可是在Shiro中,這些步驟都省略了,咱們只須要在配置文件Shiro的過濾器shiroFilter中過濾器鏈filterChainDefinitions中的<value>標籤中配置這一行:

/logout = logout

便可。Shiro會根據這個配置生成一個虛擬的請求映射路徑,當用戶請求localhost:8080/logout這個接口的時候,Shiro會自動清空Session,並跳轉到loginUrl指定的地址。

Shiro實現密碼加密和解密

常見的加密方式有不少,這裏咱們介紹Shiro中提供的一套散列算法加密方式。散列算法,是一種不可逆的算法(天然是要不可逆的,由於可逆的算法破解起來也很容易,因此不可逆的算法更安全),常見的散列算法如MD5,、SHA,可是咱們再網上看到不少破解MD5加密的網站,不是說散列算法是不可逆的嗎?爲何還存在那麼多破解密碼的網站?其實散列算法確實是不可逆的,即便是常見的MD5加密也是不可逆的加密方式,而網上的破解網站並非可以逆向算出這個加密密碼,而是經過大數據的方式得出來的,至關於,MD5解密的網站中存在一個很大的數據庫,裏面存放了用戶常見的加密密碼,而後當用戶再用此密碼解密時,再從數據庫中比對加密後的MD5密碼,若是存在就能獲得原密碼了。爲了不這種狀況,引入了鹽salt的概念,若是能經過大數據的方式破解MD5的加密,但若是在加密的密碼中再添加一組數據進行混淆,破解起來就至關難了,由於添加的salt只有咱們本身知道是什麼。

自定義一套散列算法:

  1. 實例化一個RandomNumberGenerator對象生成隨機數,能夠用來設置鹽值。
  2. 設定散列算法的名稱和散列迭代次數。
  3. 調用SimpleHash()構造方法,將算法名稱、用戶輸入的密碼、鹽值、迭代次數傳入。
  4. 經過SimpleHash()構造方法,Shiro能自動幫咱們對密碼進行加密,並調用實體類對象的setter方法將密碼設置進去。
@Component
public class PasswordHelper {

    //實例化RandomNumberGenerator對象,用於生成一個隨機數
    private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
    //散列算法名稱
    private String algorithName = "MD5";
    //散列迭代次數
    private int hashInterations = 2;
    //加密算法
    public void encryptPassword(User user){
        if (user.getPassword() != null){
            //對user對象設置鹽:salt;這個鹽值是randomNumberGenerator生成的隨機數,因此鹽值並不須要咱們指定
            user.setSalt(randomNumberGenerator.nextBytes().toHex());

            //調用SimpleHash指定散列算法參數:一、算法名稱;二、用戶輸入的密碼;三、鹽值(隨機生成的);四、迭代次數
            String newPassword = new SimpleHash(
                    algorithName,
                    user.getPassword(),
                    ByteSource.Util.bytes(user.getCredentialsSalt()),
                    hashInterations).toHex();
            user.setPassword(newPassword);
        }
    }

    //getter/setter ....
}

如上,在encryptPassword中進行了核心的密碼加密過程,咱們只須要調用SimpleHash()傳入須要加密的參數便可,可是在這裏你應該會注意到兩個地方:user.setSalt()user.getCredentialsSalt()。 其實,在實體類中咱們的肯定義了一個屬性private String salt;,這裏調用的setSalt()正是向其中設置RandomNumberGenerator生成的隨機數做爲鹽值;可是又矛盾了,爲何還存在一個getCredentialsSalt()方法?

那麼咱們看一下SimpleHash的構造方法:

public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {}

其中也須要一個參數salt。可是,要注意此salt非彼salt;咱們先看一下User實體類中定義的getCredentialsSalt()方法:

private String salt; //鹽

public String getCredentialsSalt() {
    return username + salt;
}

//getter/setter...

意義就是指定以後要使用的鹽值salt其實是usernamesalt的組合體,可是你確定好奇,爲何又定義getCredentialsSalt()呢? 要區分:setSalt()是爲User實體了設置salt參數的值,salt的值本就是RandomNumberGenerator生成的隨機數;可是getCredentialsSalt()獲得的鹽值是用戶名+隨機數,這個值最終成爲了SimpleHash加密密碼的一個重要組成部分,那麼最終經過指定加密方式(這裏是MD5)加密的密碼由用戶名+隨機數+密碼組合而得。

加密

上面介紹了核心的加密流程,那麼如何使用?何時須要加密呢?

當然是在建立新用戶的時候加密用戶密碼了,那麼咱們來看下建立用戶的Service層:

public void create(User user) {
  //加密密碼
  passwordHelper.encryptPassword(user);
  userDao.create(user);
}

建立用戶時,要調用passwordHelperencryptPassword()方法對傳入的User對象進行密碼加密和設定鹽值處理。那麼在數據庫中保存的數據就如:

除了建立用戶,更新用戶數據的時候也要從新加密密碼(只要更新了User表的用戶名或密碼)都必須調用encryptPassword()從新加密密碼和設置鹽值,由於最終存在數據庫表中的密碼是用戶名+密碼+鹽值

public void update(User user) {
    //加密密碼
    passwordHelper.encryptPassword(user);
    userDao.update(user);
}

解密

上面講了半天的加密過程,下面說一下解密實現。以前已經說過,散列算法是不可逆的,因此一旦密碼被加密是沒法算出來的,可是咱們能夠用另一種方式:比對。就是將散列算法的加密方式傳給Realm,當用戶登陸系統時,獲取用戶輸入的密碼根據已定義的加密方式對此密碼進行加密,而後交給SimpleAuthenticationInfo將用戶登陸輸入的加密密碼和數據庫中根據username獲得的加密密碼進行比對,若是比對成功就證實你的登陸密碼是正確的,從而實現解密。

那麼應該怎麼實現?很簡單,在Realm中咱們應該調用SimpleAuthenticationInfo的這個構造方法:

public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {}

那麼咱們要更改Realm中的SimpleAuthenticationInfo的這個實現:

SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
    user.getUsername(), //用戶名
    user.getPassword(), //密碼
    ByteSource.Util.bytes(user.getCredentialsSalt()), //salt=username+salt
    getName() //realm name
);

若是使用了散列算法進行密碼加密和驗證服務,你必須在Spring配置文件中注入credentialsMatcher來實現密碼驗證服務。

<bean id="credentialsMatcher" class="cn.tycoding.credentials.RetryLimitHashedCredentialsMatcher">
  <constructor-arg ref="cacheManager"/>
  <property name="hashAlgorithmName" value="md5"/>
  <property name="hashIterations" value="2"/>
  <property name="storedCredentialsHexEncoded" value="true"/>
</bean>

建立一個RetryLimitHashedCredentialsMatcher類,繼承HashedCredentialsMathcer

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
    public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){}

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {}
}

這樣就能獲取到加密密碼的鹽值,而後SimpleAuthenticationInfo會結合這個鹽值進行密碼比對實現解密。

<br/>

Shiro實現受權

受權,即賦予用戶必定的操做權限,這時,就該參考一下項目的表設計了: Shiro實現權限管理系統之表結構設計 。結合數據庫的表設計咱們彷佛就清楚了爲何那樣設計表,根據什麼進行權限校驗和受權,想必你也有一些思路了。

在受權中須要瞭解幾個關鍵對象:

<style> table th:first-of-type { width: 100px; } </style>

對象名稱 做用
主體(Subject) 即表明當前登陸的用戶
資源(Resource) 即用戶登陸成功後容許訪問的東西,好比某個頁面,某個文件;它能夠精確到某個按鈕等..
權限(Permission) 即表明用戶操做系統功能的權利,若是擁有了這個權限才能操做該功能,和資源關聯,有權限就意味着有訪問資源的權利
角色(Role) 表明了操做(資源)集合,能夠理解爲權限的集合,和權限關聯,角色對應的權限,權限關聯着資源

因此,咱們要清楚:用戶和角色間是一對多的關係;角色和權限是多對多的關係;權限和資源是多對多的關係。可是在咱們設計的表:Shiro實現權限管理系統之表結構的設計中,我並無設置單獨設置資源表,而是僅用了權限表。 固然你能夠再寫一個資源表(Resource),創建權限和資源間的關係,這樣權限管理能精確到對每一個按鈕的管理。

受權

實現受權前,首先,用戶得擁有權限,那麼就要創建用戶-角色的關係、角色-權限的關係;具體操做步驟請參看個人這篇博文:Shiro實現權限管理系統之表結構設計中介紹的sql。

Shiro提供了多種受權方式,好比咱們能夠看subject實例擁有的受權方法:

從方法名上就能看出subject提供了哪些受權方式;那麼這裏咱們不講用subject實例受權的方式,咱們講一種更簡便的方式:Shiro註解、Shiro-Spring註解的方式。

Shiro結合Spring提供了相應的註解用戶權限控制,咱們先來看一下都有哪些註解:

<style> table th:first-of-type { width: 400px; } </style>

註解名稱 解釋
@RequiresAuthentication 表示當前Subject已經經過login身份驗證;即Subject.isAuthenticated() == true;不然就攔截
@RequiresUser 表示當前Subject已經經過login身份驗證或經過記住我登陸;不然就攔截
@RequiresGuest 表示當前Subject沒有身份驗證或經過記住我登陸過,便是遊客身份
@RequiresRoles(value ={"admin", "user"}, logical=Logical.AND) 表示當前Subject須要同時(由Logical.AND體現)擁有admin和user角色;不然攔截
@RequiresPermissions(vale={"user:a","user:b"}, logical=Logical.OR) 表示當前Subject須要擁有user:a或者(由Logical.OR體現)user:b角色;不然攔截

由於Shiro的某些權限註解須要AOP的功能進行判斷,因此須要開啓AOP功能的支持;項目中使用了Spring AOP,Shiro提供了Spring AOP的集成用於權限註解的解析和驗證。 在SpringMVC的配置文件中開啓Shiro Spring AOP 的支持:

<aop:config proxy-target-class="true"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

在Controller映射方法上添加註解

建立用戶的方法上添加權限註解:

@ResponseBody
@RequestMapping("/create")
@RequiresRoles(value={"admin","personnel-resource"}, logical = Logical.OR)
public Result create(@RequestBody User user) {}

刪除用戶信息的方法上添加權限註解:

@ResponseBody
@RequestMapping("/delete")
@RequiresRoles(value = {"admin", "personnel-resource"}, logical = Logical.OR)
public Result delete(@RequestParam("id") Long id){}

根據用戶名查找其角色的方法上添加權限註解:

@ResponseBody
@RequestMapping("/findRoles")
@RequiresRoles(value = {"admin"}, logical = Logical.OR)
@RequiresPermissions(value = {"role:view", "role:*"}, logical = Logical.OR)
public List<Role> findRoles(String username) {}

...

JSP頁面受權

Shiro提供了JSTL標籤用於在JSP/GSP頁面進行權限控制;首先須要導入標籤庫:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<style> table th:first-of-type { width: 200px; } </style>

標籤名稱 做用
<shiro:guest> 用戶沒有身份驗證時顯示相應的信息,即遊客訪問信息
<shiro:user> 用戶已經身份驗證、記住我登陸後顯示相應的信息,未登陸用戶將會攔截
<shiro:authenticated> 用戶已經身份驗證經過,即Subject.isAuthenticated() == true;未登陸或記住我登陸的都會攔截
<shiro:notAuthenticated> 用戶已經身份驗證經過,可是Subject.isAuthenticated() == false,便可能是經過記住我登陸的
<shiro:principal> 顯示用戶身份信息,默認調用Subject.getPrincipal()獲取用戶登陸信息
<shiro:hasRole> 如:<shiro:hasRole name="admin">,若是當前Subject有admin角色就顯示數據,相似於@RequiresRoles()註解;不然就攔截
<shiro:hasAnyRole> 如:<shiro:hasAnyRole name="admin,user">,若是當前Subject有admin或user角色就顯示數據,相似於@RequireRoles(Logical=Logical.OR)註解;不然將就攔截
<shiro:lackRole> 若是當前Subject沒有角色就顯示數據
<shiro:hasPermission> 如:<shiro:hasPermission name="user:create">,若是當前Subject有user:create權限,就顯示數據;不然就攔截
<shiro:lacksPermission> 如:<shiro:lacksPermission name="user:create">,若是當前Subject沒有user:create權限,就顯示數據;不然攔截

<br/>

Shiro實現會話管理

會話:用戶登陸後直至註銷(Session丟失)前稱爲一次會話,即用戶訪問應用時保持的鏈接關係,能夠保證在屢次交互中應用可以識別出當前訪問的用戶是誰,且可在屢次交互中保存一些數據。常見的應用實例如:登陸時記住個人功能、單點登陸的功能...

Shiro提供了會話管理器:sessionManager,管理着全部會話的建立、維護、刪除、等工做。在web環境中使用Shiro的會話管理器,咱們須要在Spring的配置文件中注入DefaultWebSessionManager:

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 設置全局會話過時時間:默認30分鐘 -->
    <property name="globalSessionTimeout" value="1800000"/>
    <!-- 是否啓用sessionIdCookie,默認是啓用的 -->
    <property name="sessionIdCookieEnabled" value="true"/>
    <!-- 會話Cookie -->
    <property name="sessionIdCookie" ref="sessionIdCookie"/>    
</bean>

<!-- 會話Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="sid"/>
   <!-- 若是設置爲true,則客戶端不會暴露給服務端腳本代碼,有助於減小某些類型的跨站腳本攻擊 -->
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="-1"/><!-- maxAge=-1表示瀏覽器關閉時失效此Cookie -->
</bean>

還要將sessionManager注入到SecurityManager中:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="userRealm"/>
    <!-- 注入sessionManager -->
    <property name="sessionManager" ref="sessionManager"/>
</bean>

Shiro緩存實現

Shiro也集成了緩存機制,例如Shiro提供了CachingRealm,提供了一些基礎的緩存實現。Shiro默認是禁用緩存的,首先咱們要開啓Shiro的緩存管理,在XML中進行以下配置:

<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <property name="cacheManagerConfigFile" value="classpath:other/ehcache.xml"/>
</bean>

在自定義的Realm實現中配置緩存的實現:

<!-- Realm實現 -->
<bean id="userRealm" class="cn.tycoding.realm.UserRealm">
    <!-- 使用credentialsMatcher實現密碼驗證服務 -->
    <property name="credentialsMatcher" ref="credentialsMatcher"/>
    <!-- 是否啓用緩存 -->
    <property name="cachingEnabled" value="true"/>
    <!-- 是否啓用身份驗證緩存 -->
    <property name="authenticationCachingEnabled" value="true"/>
    <!-- 緩存AuthenticationInfo信息的緩存名稱 -->
    <property name="authenticationCacheName" value="authenticationCache"/>
    <!-- 是否啓用受權緩存,緩存AuthorizationInfo信息 -->
    <property name="authorizationCachingEnabled" value="true"/>
    <!-- 緩存AuthorizationInfo信息的緩存名稱 -->
    <property name="authorizationCacheName" value="authorizationCache"/>
</bean>

resources/other/文件夾下建立配置文件ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
    <cache name="shiro-activeSessionCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>
</ehcache>

設置SecurityManager的cacheManager:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
    ...
</bean>

實現Remember功能

在Shiro會話管理時咱們就講到會話的功能,例如:Shiro實現了RememberMe記住個人功能,當用戶在登陸頁面中勾選了記住我,再瀏覽器關閉後再次訪問系統發現是能夠直接登陸的;可是若是沒有實現這一功能,Shiro默認設置瀏覽器關閉後當即清除緩存,那麼再次打開瀏覽器要從新進行登陸。

  • 拓展

RememberMe和使用Subject.login(token)登陸是有所不一樣的,RememberMe是使用緩存Cookie的技術實現的登陸,在前面講到的一些權限註解中就說到了二者的區別。

  • RememberMe的配置實現

在配置文件中寫入:

<!-- 會話Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="sid"/>
    <!-- 若是設置爲true,則客戶端不會暴露給服務端腳本代碼,有助於減小某些類型的跨站腳本攻擊 -->
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="-1"/><!-- maxAge=-1表示瀏覽器關閉時失效此Cookie -->
</bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="rememberMe"/>
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="2592000"/><!-- 30天 -->
</bean>

<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <!-- cipherKey是加密rememberMe Cookie的密匙,默認AES算法 -->
    <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
    <property name="cookie" ref="rememberMeCookie"/>
</bean>

sessionIdCookie中設置maxAge=-1表示瀏覽器關閉後即失效此Cookie在rememberMeCookie中設置maxAge=2592000表示記住此Cookie,保存30天。

SecurityManager中設置rememberMeManager:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="rememberMeManager" ref="rememberMeManager"/>
</bean>

測試

  1. 修改login登陸頁面:

在登陸表單中添加一個checkbox:

<input type="checkbox" name="remember">請記住我

若是用戶勾選了這個複選框,點擊登陸按鈕提交後臺的參數中會多一個remember參數,且值是on(若是用戶沒有勾選,提交表單中就不存在這個參數);因此咱們修改Controller的登陸方法:

  1. 修改Controller
public String login(
            @RequestParam(value = "username", required = false) String username,
            @RequestParam(value = "password", required = false) String password,
            @RequestParam(value = "remember", required = false) String remember,
            Model model) {
  if (username != null && password != null) {
      //初始化
      Subject subject = SecurityUtils.getSubject();
      UsernamePasswordToken token = new UsernamePasswordToken(username, password);
      if (remember != null){
          if (remember.equals("on")) {
              //說明選擇了記住我
              token.setRememberMe(true);
          } else {
              token.setRememberMe(false);
          }
      }else{
          token.setRememberMe(false);
      }
      try {
          //登陸,即身份校驗,由經過Spring注入的UserRealm會自動校驗輸入的用戶名和密碼在數據庫中是否有對應的值
          subject.login(token);
          return "redirect:index.do";
      }catch (Exception e){
          e.printStackTrace();
          error = "未知錯誤,錯誤信息:" + e.getMessage();
      }
  } else {
      error = "請輸入用戶名和密碼";
  }
  //登陸失敗,跳轉到login頁面
  model.addAttribute("error", error);
  return "login";
}
  1. 建立一個測試頁面

建立一個authenticated.jsp頁面,隨便寫一段文字此頁面必須是Subject.isAuthenticated() == true才能訪問。而後在配置文件的filterChainDefinitions中定義

/authenticated.jsp = authc
  1. 測試

啓動項目,訪問localhost:8080/自動跳轉到登陸頁面,勾選登陸表單中的記住我複選框,成功登陸系統後,關閉瀏覽器。再次打開瀏覽器,直接訪問localhost:8080/index.do發現直接就能登陸系統。可是直接在瀏覽器中輸入localhost:8080/authenticated.jsp發現確是不能訪問的,而且被攔截道登陸頁面,緣由就是rememberMe登陸系統並非經過Subject.login(token)的方式,而authc攔截器攔截的資源要求必須是Subject.isAuthenticated() == true才能訪問。

從新啓動項目(或者註銷帳戶),從新進入登陸頁面,這次不勾選記住我複選框,成功進入系統後關閉瀏覽器,再次打開瀏覽器輸入localhost:8080/index.do發現會再次被攔截跳轉到loginUrl地址。

項目截圖

<br/>

交流

若是你們有興趣,歡迎你們加入個人Java交流羣:671017003 ,一塊兒交流學習Java技術。博主目前一直在自學JAVA中,技術有限,若是能夠,會盡力給你們提供一些幫助,或是一些學習方法,固然羣裏的大佬都會積極給新手答疑的。因此,別猶豫,快來加入咱們吧!

<br/>

聯繫

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

相關文章
相關標籤/搜索