其實我不只會 Spring Security,Shiro 也略懂一二!

和你們分享一個鬆哥原創的 Shiro 教程吧,還沒寫完,先整一部分,剩下的敬請期待。html

1.Shiro簡介

Apache Shiro是一個開源安全框架,提供身份驗證、受權、密碼學和會話管理。Shiro框架具備直觀、易用等特性,同時也能提供健壯的安全性,雖然它的功能不如SpringSecurity那麼強大,可是在普通的項目中也夠用了。java

1.1 由來

Shiro的前身是JSecurity,2004年,Les Hazlewood和Jeremy Haile創辦了Jsecurity。當時他們找不到適用於應用程序級別的合適Java安全框架,同時又對JAAS很是失望。2004年到2008年期間,JSecurity託管在SourceForge上,貢獻者包括Peter Ledbrook、Alan Ditzel和Tim Veil。2008年,JSecurity項目貢獻給了Apache軟件基金會(ASF),並被接納成爲Apache Incubator項目,由導師管理,目標是成爲一個頂級Apache項目。期間,Jsecurity曾短暫改名爲Ki,隨後因商標問題被社區改名爲「Shiro」。隨後項目持續在Apache Incubator中孵化,並增長了貢獻者Kalle Korhonen。2010年7月,Shiro社區發佈了1.0版,隨後社區建立了其項目管理委員會,並選舉Les Hazlewood爲主席。2010年9月22日,Shrio成爲Apache軟件基金會的頂級項目(TLP)。mysql

1.2 有哪些功能

Apache Shiro是一個強大而靈活的開源安全框架,它乾淨利落地處理身份認證,受權,企業會話管理和加密。Apache Shiro的首要目標是易於使用和理解。安全有時候是很複雜的,甚至是痛苦的,但它沒有必要這樣。框架應該儘量掩蓋複雜的地方,露出一個乾淨而直觀的API,來簡化開發人員在應用程序安全上所花費的時間。git

如下是你能夠用Apache Shiro 所作的事情:github

  1. 驗證用戶來覈實他們的身份
  2. 對用戶執行訪問控制,如:判斷用戶是否被分配了一個肯定的安全角色;判斷用戶是否被容許作某事
  3. 在任何環境下使用Session API,即便沒有Web容器
  4. 在身份驗證,訪問控制期間或在會話的生命週期,對事件做出反應
  5. 彙集一個或多個用戶安全數據的數據源,並做爲一個單一的複合用戶「視圖」
  6. 單點登陸(SSO)功能
  7. 爲沒有關聯到登陸的用戶啓用"Remember Me"服務web

    等等

Apache Shiro是一個擁有許多功能的綜合性的程序安全框架。下面的圖表展現了Shiro的重點:算法

p306

Shiro中有四大基石——身份驗證,受權,會話管理和加密。spring

  1. Authentication:有時也簡稱爲「登陸」,這是一個證實用戶是誰的行爲。
  2. Authorization:訪問控制的過程,也就是決定「誰」去訪問「什麼」。
  3. Session Management:管理用戶特定的會話,即便在非Web 或EJB 應用程序。
  4. Cryptography:經過使用加密算法保持數據安全同時易於使用。

除此以外,Shiro也提供了額外的功能來解決在不一樣環境下所面臨的安全問題,尤爲是如下這些:sql

  1. Web Support:Shiro的web支持的API可以輕鬆地幫助保護Web應用程序。
  2. Caching:緩存是Apache Shiro中的第一層公民,來確保安全操做快速而又高效。
  3. Concurrency:Apache Shiro利用它的併發特性來支持多線程應用程序。
  4. Testing:測試支持的存在來幫助你編寫單元測試和集成測試。
  5. "Run As":一個容許用戶假設爲另外一個用戶身份(若是容許)的功能,有時候在管理腳本頗有用。
  6. "Remember Me":在會話中記住用戶的身份,這樣用戶只須要在強制登陸時候登陸。

2.從一個簡單的案例開始身份認證

2.1 shiro下載

要學習shiro,咱們首先需求去shiro官網下載shiro,官網地址地址https://shiro.apache.org/,截... 在 2017-2019 曾經停更了兩年,我一度覺得覺得這個項目 gg 了),本文將採用這個版本。固然,shiro咱們也能夠從github上下載到源碼。兩個源碼下載地址以下:數據庫

1.apache shiro
2.github-shiro

上面我主要是和小夥伴們介紹下源碼的下載,並無涉及到jar包的下載,jar包咱們到時候直接使用maven便可。

2.2 建立演示工程

這裏咱們先不急着寫代碼,咱們先打開剛剛下載到的源碼,源碼中有一個samples目錄,以下:

p307

這個samples目錄是官方給咱們的一些演示案例,其中有一個quickstart項目,這個項目是一個maven項目,參考這個quickstart,咱們來建立一個本身的演示工程。

1.首先使用maven建立一個JavaSE工程
工程建立成功後在pom文件中添加以下依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-all</artifactId>
    <version>RELEASE</version>
</dependency>

2.配置用戶

參考quickstart項目中的shiro.ini文件,咱們來配置一個用戶,配置方式以下:首先在resources目錄下建立一個shiro.ini文件,文件內容以下:

[users]
sang=123,admin
[roles]
admin=*

以上配置表示咱們建立了一個名爲sang的用戶,該用戶的密碼是123,該用戶的角色是admin,而admin具備操做全部資源的權限。

3.執行登陸

OK,作完上面幾步以後,咱們就能夠來看看如何實現一次簡單的登陸操做了。這個登陸操做咱們依然是參考quickstart項目中的類來實現,首先咱們要經過shiro.ini建立一個SecurityManager,再將這個SecurityManager設置爲單例模式,以下:

Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

如此以後,咱們就配置好了一個基本的Shiro環境,注意此時的用戶和角色信息咱們配置在shiro.ini這個配置文件中,接下來咱們就能夠獲取一個Subject了,這個Subject就是咱們當前的用戶對象,獲取方式以下:

Subject currentUser = SecurityUtils.getSubject();

拿到這個用戶對象以後,接下來咱們能夠獲取一個session了,這個session和咱們web中的HttpSession的操做基本上是一致的,不一樣的是,這個session不依賴任何容器,能夠隨時隨地獲取,獲取和操做方式以下:

//獲取session
Session session = currentUser.getSession();
//給session設置屬性值
session.setAttribute("someKey", "aValue");
//獲取session中的屬性值
String value = (String) session.getAttribute("someKey");

說了這麼多,咱們的用戶到如今尚未登陸呢,Subject中有一個isAuthenticated方法用來判斷當前用戶是否已經登陸,若是isAuthenticated方法返回一個false,則表示當前用戶未登陸,那咱們就能夠執行登錄,登陸方式以下:

if (!currentUser.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken("sang", "123");
    try {
        currentUser.login(token);
    } catch (UnknownAccountException uae) {
        log.info("There is no user with username of " + token.getPrincipal());
    } catch (IncorrectCredentialsException ice) {
        log.info("Password for account " + token.getPrincipal() + " was incorrect!");
    } catch (LockedAccountException lae) {
        log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                "Please contact your administrator to unlock it.");
    }
    catch (AuthenticationException ae) {
    }
}

首先構造UsernamePasswordToken,兩個參數就是咱們的用戶名和密碼,而後調用Subject中的login方法執行登陸,當用戶名輸錯,密碼輸錯、或者帳戶鎖定等問題出現時,系統會經過拋異常告知調用者這些問題。

當登陸成功以後,咱們能夠經過以下方式獲取當前登錄用戶的用戶名:

log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

咱們也能夠經過調用Subject中的hasRole和isPermitted方法來判斷當前用戶是否具有某種角色或者某種權限,以下:

if (currentUser.hasRole("admin")) {
    log.info("May the Schwartz be with you!");
} else {
    log.info("Hello, mere mortal.");
}
if (currentUser.isPermitted("lightsaber:wield")) {
    log.info("You may use a lightsaber ring.  Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

最後,咱們能夠經過logout方法註銷本次登陸,以下:

currentUser.logout();

OK,至此,咱們經過官方案例給小夥伴們簡單介紹了Shiro中的登陸操做,完整案例你們能夠參考官方的demo。

3. 聊一聊Shiro中的Realm

3.1 登陸流程是什麼樣的

首先咱們來看shiro官方文檔中這樣一張登陸流程圖:

p308

參照此圖,咱們的登陸一共要通過以下幾個步驟:

  1. 應用程序代碼調用Subject.login方法,傳遞建立好的包含終端用戶的Principals(身份)和Credentials(憑證)的AuthenticationToken實例(即上文例子中的UsernamePasswordToken)。
  2. Subject實例,一般是DelegatingSubject(或子類)委託應用程序的SecurityManager經過調用securityManager.login(token)開始真正的驗證工做(在DelegatingSubject類的login方法中打斷點便可看到)。
  3. SubjectManager做爲一個基本的「保護傘」的組成部分,接收token以及簡單地委託給內部的Authenticator實例經過調用authenticator.authenticate(token)。這一般是一個ModularRealmAuthenticator實例,支持在身份驗證中協調一個或多個Realm實例。ModularRealmAuthenticator本質上爲Apache Shiro 提供了PAM-style 範式(其中在PAM 術語中每一個Realm 都是一個'module')。
  4. 若是應用程序中配置了一個以上的Realm,ModularRealmAuthenticator實例將利用配置好的AuthenticationStrategy來啓動Multi-Realm認證嘗試。在Realms 被身份驗證調用以前,期間和之後,AuthenticationStrategy被調用使其可以對每一個Realm的結果做出反應。若是隻有一個單一的Realm 被配置,它將被直接調用,由於沒有必要爲一個單一Realm的應用使用AuthenticationStrategy。
  5. 每一個配置的Realm用來幫助看它是否支持提交的AuthenticationToken。若是支持,那麼支持Realm的getAuthenticationInfo方法將會伴隨着提交的token被調用。

OK,經過上面的介紹,相信小夥伴們對整個登陸流程都有必定的理解了,小夥伴能夠經過打斷點來驗證咱們上文所說的五個步驟。那麼在上面的五個步驟中,小夥伴們看到了有一個Realm承擔了很重要的一部分工做,那麼這個Realm究竟是個什麼東西,接下來咱們就來仔細看一看。

3.2 什麼是Realm

根據Realm文檔上的解釋,Realms擔當Shiro和你的應用程序的安全數據之間的「橋樑」或「鏈接器」。當它實際上與安全相關的數據如用來執行身份驗證(登陸)及受權(訪問控制)的用戶賬戶交互時,Shiro從一個或多個爲應用程序配置的Realm 中尋找許多這樣的東西。在這個意義上說,Realm 本質上是一個特定安全的DAO:它封裝了數據源的鏈接詳細信息,使Shiro 所需的相關的數據可用。當配置Shiro 時,你必須指定至少一個Realm 用來進行身份驗證和/或受權。SecurityManager可能配置多個Realms,但至少有一個是必須的。Shiro 提供了當即可用的Realms 來鏈接一些安全數據源(即目錄),如LDAP,關係數據庫(JDBC),文本配置源,像INI 及屬性文件,以及更多。你能夠插入你本身的Realm 實現來表明自定義的數據源,若是默認地Realm不符合你的需求。

看了上面這一段解釋,可能還有小夥伴雲裏霧裏,那麼接下來咱們來經過一個簡單的案例來看看Realm到底扮演了一個什麼樣的做用,注意,本文的案例在上文案例的基礎上完成。首先自定義一個MyRealm,內容以下:

public class MyRealm implements Realm {
    public String getName() {
        return "MyRealm";
    }
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String password = new String(((char[]) token.getCredentials()));
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("用戶不存在");
        }
        if (!"123".equals(password)) {
            throw new IncorrectCredentialsException("密碼不正確");
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

自定義Realm實現Realm接口,該接口中有三個方法,第一個getName方法用來獲取當前Realm的名字,第二個supports方法用來判斷這個realm所支持的token,這裏我假設值只支持UsernamePasswordToken類型的token,第三個getAuthenticationInfo方法則進行了登錄邏輯判斷,從token中取出用戶的用戶名密碼等,進行判斷,固然,我這裏省略掉了數據庫操做,當登陸驗證出現問題時,拋異常便可,這裏拋出的異常,將在執行登陸那裏捕獲到(注意,因爲我這裏定義的MyRealm是實現了Realm接口,因此這裏的用戶名和密碼都須要我手動判斷是否正確,後面的文章我會介紹其餘寫法)。

OK,建立好了MyRealm以後還不夠,咱們還須要作一個簡單配置,讓MyRealm生效,將shiro.ini文件中的全部東西都註釋掉,添加以下兩行:

MyRealm= org.sang.MyRealm
securityManager.realms=$MyRealm

第一行表示定義了一個realm,第二行將這個定義好的交給securityManger,這裏實際上會調用到RealmSecurityManager類的setRealms方法。OK,作好這些以後,小夥伴們能夠在MyRealm類中的一些關鍵節點打上斷點,再次執行main方法,看看整個的登陸流程。

4. 再來聊一聊Shiro中的Realm

4.1 Realm的繼承關係

經過查看類的繼承關係,咱們發現Realm的子類實際上有不少種,這裏咱們就來看看有表明性的幾種:

  1. IniRealm

可能咱們並不知道,實際上這個類在咱們第二篇文章中就已經用過了。這個類一開始就有以下兩行定義:

public static final String USERS_SECTION_NAME = "users";
public static final String ROLES_SECTION_NAME = "roles";

這兩行配置表示shiro.ini文件中,[users]下面的表示表用戶名密碼還有角色,[roles]下面的則是角色和權限的對應關係。

  1. PropertiesRealm

PropertiesRealm則規定了另一種用戶、角色定義方式,以下:

user.user1=password,role1
role.role1=permission1

  1. JdbcRealm

這個顧名思義,就是從數據庫中查詢用戶的角色、權限等信息。打開JdbcRealm類,咱們看到源碼中有以下幾行:

protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";

根據這幾行預設的SQL咱們就能夠大體推斷出數據庫中表的名稱以及字段了,固然,咱們也能夠自定義SQL。JdbcRealm其實是AuthenticatingRealm的子類,關於AuthenticatingRealm咱們在後面還會詳細說到,這裏先不展開。接下來咱們就來詳細說說這個JdbcRealm。

4.2 JdbcRealm

  1. 準備工做

使用JdbcRealm,涉及到數據庫操做,要用到數據庫鏈接池,這裏我使用Druid數據庫鏈接池,所以首先添加以下依賴:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>RELEASE</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
  1. 數據庫建立

想要使用JdbcRealm,那我首先要建立數據庫,根據JdbcRealm中預設的SQL,我定義的數據庫表結構以下:

p309

這裏爲了你們可以直觀的看到表的關係,我使用了外鍵,實際工做中,視狀況而定。而後向表中添加幾條測試數據。數據庫腳本小夥伴能夠在github上下載到(https://github.com/lenve/shir...

  1. 配置文件處理

而後將shiro.ini中的全部配置註釋掉,添加以下配置:

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm

這裏的配置文件都很簡單,不作過多贅述,小夥伴惟一須要注意的是permissionsLookupEnabled須要設置爲true,不然一會JdbcRealm就不會去查詢權限用戶權限。

  1. 測試

OK,作完上面幾步就能夠測試了,測試方式和第二篇文章中同樣,咱們能夠測試下用戶登陸,用戶角色和用戶權限。

  1. 自定義查詢SQL

小夥伴們看懂了上文,對於自定義查詢SQL就沒什麼問題了。我這裏舉一個簡單的例子,好比我要自定義authenticationQuery對對應的SQL,查看JdbcRealm源碼,咱們發現authenticationQuery對應的SQL原本是select password from users where username = ?,若是須要修改的話,好比說個人表名不是users而是employee,那麼在shiro.ini中添加以下配置便可:

jdbcRealm.authenticationQuery=select password from employee where username = ?

OK,這個小夥伴下來本身作嘗試,我這裏就不演示了。

5. Shiro中多Realm的認證策略問題

5.1 多Realm認證策略

不知道小夥伴們是否還記得這張登陸流程圖:

p308

從這張圖中咱們能夠清晰看到Realm是能夠有多個的,不過到目前爲止,咱們全部的案例都仍是單Realm,那麼咱們先來看一個簡單的多Realm狀況。

前面的文章咱們本身建立了一個MyRealm,也用過JdbcRealm,但都是單獨使用的,如今我想將兩個一塊兒使用,只須要修改shiro.ini配置便可,以下:

MyRealm= org.sang.MyRealm

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm,$MyRealm

可是此時我數據庫中用戶的信息是sang/123,MyRealm中配置的信息也是sang/123,我把MyRealm中的用戶信息修改成江南一點雨/456,此時,個人MyRealm的getAuthenticationInfo方法以下:

public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String password = new String(((char[]) token.getCredentials()));
    String username = token.getPrincipal().toString();
    if (!"江南一點雨".equals(username)) {
        throw new UnknownAccountException("用戶不存在");
    }
    if (!"456".equals(password)) {
        throw new IncorrectCredentialsException("密碼不正確");
    }
    return new SimpleAuthenticationInfo(username, password, getName());
}

這個時候咱們就配置了兩個Realm,仍是使用咱們一開始的測試代碼進行登陸測試,這個時候咱們發現我既可使用江南一點雨/456進行登陸,也可使用sang/123進行登陸,用sang/123登陸成功以後用戶的角色信息和以前是同樣的,而用江南一點雨/456登陸成功以後用戶沒有角色,這個也很好理解,由於咱們在MyRealm中沒有給用戶配置任何權限。總而言之,就是當我有了兩個Realm以後,如今只須要這兩個Realm中的任意一個認證成功,就算我當前用戶認證成功。

5.2 原理追蹤

好了,有了上面的問題後,接下來咱們在Subject的login方法上打斷點,跟隨程序的執行步驟,咱們來到了ModularRealmAuthenticator類的doMultiRealmAuthentication方法中,以下:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken);
}

在這個方法中,首先會獲取當前一共有多少個realm,若是隻有一個則執行doSingleRealmAuthentication方法進行處理,若是有多個realm,則執行doMultiRealmAuthentication方法進行處理。doSingleRealmAuthentication方法部分源碼以下:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    ...
    ...
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if(info == null) {
        String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    } else {
        return info;
    }
}

小夥伴們看到這裏就明白了,這裏調用了realm的getAuthenticationInfo方法,這個方法實際上就是咱們本身實現的MyRealm中的getAuthenticationInfo方法。

那若是有多個Realm呢?咱們來看看doMultiRealmAuthentication方法的實現,部分源碼以下:

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    AuthenticationStrategy strategy = this.getAuthenticationStrategy();
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    Iterator var5 = realms.iterator();
    while(var5.hasNext()) {
        Realm realm = (Realm)var5.next();
        aggregate = strategy.beforeAttempt(realm, token, aggregate);
        if(realm.supports(token)) {
            AuthenticationInfo info = null;
            Throwable t = null;
            try {
                info = realm.getAuthenticationInfo(token);
            } catch (Throwable var11) {
            }
            aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
        } else {
            log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
        }
    }
    aggregate = strategy.afterAllAttempts(token, aggregate);
    return aggregate;
}

我這裏主要來講下這個方法的實現思路:

  1. 首先獲取多Realm認證策略
  2. 構建一個AuthenticationInfo用來存放一會認證成功以後返回的信息
  3. 遍歷Realm,調用每一個Realm中的getAuthenticationInfo方法,看是否可以認證成功
  4. 每次獲取到AuthenticationInfo以後,都調用afterAttempt方法進行結果合併
  5. 遍歷完全部的Realm以後,調用afterAllAttempts進行結果合併,這裏主要判斷下是否一個都沒匹配上

5.3 自由配置認證策略

OK,通過上面的簡單解析,小夥伴們對認證策略應該有一個大體的認識了,那麼在Shiro中,一共支持三種不一樣的認證策略,以下:

  1. AllSuccessfulStrategy,這個表示全部的Realm都認證成功纔算認證成功
  2. AtLeastOneSuccessfulStrategy,這個表示只要有一個Realm認證成功就算認證成功,默認即此策略
  3. FirstSuccessfulStrategy,這個表示只要第一個Realm認證成功,就算認證成功

配置方式也很簡單,在shiro.ini中進行配置,在上面配置的基礎上,增長以下配置:

authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

此時,咱們再進行登陸測試,則會要求每一個Realm都認證經過纔算認證經過。

6. Shiro中密碼加密

6.1 密碼爲何要加密

2011年12月21日,有人在網絡上公開了一個包含600萬個CSDN用戶資料的數據庫,數據所有爲明文儲存,包含用戶名、密碼以及註冊郵箱。事件發生後CSDN在微博、官方網站等渠道發出了聲明,解釋說此數據庫系2009年備份所用,因不明緣由泄露,已經向警方報案。後又在官網網站發出了公開道歉信。在接下來的十多天裏,金山、網易、京東、噹噹、新浪等多家公司被捲入到此次事件中。整個事件中最觸目驚心的莫過於CSDN把用戶密碼明文存儲,因爲不少用戶是多個網站共用一個密碼,所以一個網站密碼泄露就會形成很大的安全隱患。因爲有了這麼多前車可鑑,咱們如今作系統時,密碼都要加密處理。

密碼加密咱們通常會用到散列函數,又稱散列算法、哈希函數,是一種從任何一種數據中建立小的數字「指紋」的方法。散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來。該函數將數據打亂混合,從新建立一個叫作散列值的指紋。散列值一般用一個短的隨機字母和數字組成的字符串來表明。好的散列函數在輸入域中不多出現散列衝突。在散列表和數據處理中,不抑制衝突來區別數據,會使得數據庫記錄更難找到。咱們經常使用的散列函數有以下幾種:

  1. MD5消息摘要算法

MD5消息摘要算法是一種被普遍使用的密碼散列函數,能夠產生出一個128位(16字節)的散列值,用於確保信息傳輸完整一致。MD5由美國密碼學家羅納德·李維斯特設計,於1992年公開,用以取代MD4算法。這套算法的程序在 RFC 1321中被加以規範。將數據(如一段文字)運算變爲另外一固定長度值,是散列算法的基礎原理。1996年後被證明存在弱點,能夠被加以破解,對於須要高度安全性的數據,專家通常建議改用其餘算法,如SHA-2。2004年,證明MD5算法沒法防止碰撞,所以不適用於安全性認證,如SSL公開密鑰認證或是數字簽名等用途。

  1. 安全散列算法

安全散列算法(Secure Hash Algorithm)是一個密碼散列函數家族,是FIPS所認證的安全散列算法。能計算出一個數字消息所對應到的,長度固定的字符串(又稱消息摘要)的算法。且若輸入的消息不一樣,它們對應到不一樣字符串的機率很高。SHA家族的算法,由美國國家安全局所設計,並由美國國家標準與技術研究院發佈,是美國的政府標準,其分別是:SHA-0:1993年發佈,是SHA-1的前身;SHA-1:1995年發佈,SHA-1在許多安全協議中廣爲使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被視爲是MD5的後繼者。但SHA-1的安全性在2000年之後已經不被大多數的加密場景所接受。2017年荷蘭密碼學研究小組CWI和Google正式宣佈攻破了SHA-1;SHA-2:2001年發佈,包括SHA-22四、SHA-25六、SHA-38四、SHA-5十二、SHA-512/22四、SHA-512/256。雖然至今還沒有出現對SHA-2有效的攻擊,它的算法跟SHA-1基本上仍然類似;所以有些人開始發展其餘替代的散列算法;SHA-3:2015年正式發佈,SHA-3並非要取代SHA-2,由於SHA-2目前並無出現明顯的弱點。因爲對MD5出現成功的破解,以及對SHA-0和SHA-1出現理論上破解的方法,NIST感受須要一個與以前算法不一樣的,可替換的加密散列算法,也就是如今的SHA-3。

6.2 Shiro中如何加密

Shiro中對以上兩種散列算法都提供了支持,對於MD5,Shiro中生成消息摘要的方式以下:

Md5Hash md5Hash = new Md5Hash("123", null, 1024);

第一個參數是要生成密碼的明文,第二個參數密碼的鹽值,第三個參數是生成消息摘要的迭代次數。

Shiro中對於安全散列算法的支持以下(支持多種算法,這裏我舉一個例子):

Sha512Hash sha512Hash = new Sha512Hash("123", null, 1024);

這裏三個參數含義與上文基本一致,再也不贅述。shiro中也提供了通用的算法,以下:

SimpleHash md5 = new SimpleHash("md5", "123", null, 1024);
SimpleHash sha512 = new SimpleHash("sha-512", "123", null, 1024);

當用戶註冊時,咱們能夠經過上面的方式對密碼進行加密,將加密後的字符串存入數據庫中。我這裏爲了簡單,就不寫註冊功能了,就把昨天數據庫中用戶的密碼123改爲sha512所對應的字符串,以下:

cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b8725fabc783

同時,爲了不其餘Realm的干擾,數據庫中我只配置一個JdbcRealm。

此時若是我不作其餘修改的話,登陸必然會失敗,緣由很簡單:我登陸時輸入的密碼是123,可是數據庫中的密碼是一個很長的字符串,因此登陸確定不會成功。經過打斷點,咱們發現最終的密碼比對是在SimpleCredentialsMatcher類中的doCredentialsMatch方法中進行密碼比對的,比對的方式也很簡單,直接使用了對用戶輸入的密碼和數據庫中的密碼生成byte數組而後進行比較,最終的比較在MessageDigest類的isEqual方法中。部分邏輯以下:

protected boolean equals(Object tokenCredentials, Object accountCredentials) {
        ...
        ...
        //獲取用戶輸入密碼的byte數組
        byte[] tokenBytes = this.toBytes(tokenCredentials);
        //獲取數據庫中密碼的byte數組
        byte[] accountBytes = this.toBytes(accountCredentials);
        return MessageDigest.isEqual(tokenBytes, accountBytes);
        ...
}

MessageDigest的isEqual方法以下:

public static boolean isEqual(byte[] digesta, byte[] digestb) {
    if (digesta == digestb) return true;
    if (digesta == null || digestb == null) {
        return false;
    }
    if (digesta.length != digestb.length) {
        return false;
    }

    int result = 0;
    // time-constant comparison
    for (int i = 0; i < digesta.length; i++) {
        result |= digesta[i] ^ digestb[i];
    }
    return result == 0;
}

都是很容易理解的比較代碼,這裏不贅述。咱們如今之因此登陸失敗是由於沒有對用戶輸入的密碼進行加密,經過對源代碼的分析,咱們發現是由於在AuthenticatingRealm類的assertCredentialsMatch方法中獲取了一個名爲SimpleCredentialsMatcher的密碼比對器,這個密碼比對器中比對的方法就是簡單的比較,所以若是咱們可以將這個密碼比對器換掉就行了。咱們來看一下CredentialsMatcher的繼承關係:

p310

咱們發現這個恰好有一個Sha512CredentialsMatcher比對器,這個比對器的doCredentialsMatch方法在它的父類HashedCredentialsMatcher,方法內容以下:

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
}

這時咱們發現獲取tokenHashedCredentials的方式不像之前那樣簡單粗暴了,而是調用了hashProvidedCredentials方法,而hashProvidedCredentials方法最終會來到下面這個重載方法中:

protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
    String hashAlgorithmName = assertHashAlgorithmName();
    return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}

這幾行代碼似曾相識,很明顯,是系統幫咱們對用戶輸入的密碼進行了轉換。瞭解了這些以後,那我只須要將shiro.ini修改爲以下樣子便可實現登陸了:

sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher
# 迭代次數
sha512.hashIterations=1024
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
# 修改JdbcRealm中的credentialsMatcher屬性
jdbcRealm.credentialsMatcher=$sha512
securityManager.realms=$jdbcRealm

如此以後,咱們再進行登陸測試,就能夠登陸成功了。

本小節案例下載:https://github.com/lenve/shir...

7. Shiro中密碼加鹽

7.1 密碼爲何要加鹽

無論是消息摘要算法仍是安全散列算法,若是原文同樣,生成密文也是同樣的,這樣的話,若是兩個用戶的密碼原文同樣,存到數據庫中密文也就同樣了,仍是不安全,咱們須要作進一步處理,常看法決方案就是加鹽。鹽從那裏來呢?咱們可使用用戶id(由於通常狀況下,用戶id是惟一的),也可使用一個隨機字符,我這裏採用第一種方案。

7.2 Shiro中如何實現加鹽

shiro中加鹽的方式很簡單,在用戶註冊時生成密碼密文時,就要加入鹽,以下幾種方式:

Md5Hash md5Hash = new Md5Hash("123", "sang", 1024);
Sha512Hash sha512Hash = new Sha512Hash("123", "sang", 1024);
SimpleHash md5 = new SimpleHash("md5", "123", "sang", 1024);
SimpleHash sha512 = new SimpleHash("sha-512", "123", "sang", 1024)

而後咱們首先將sha512生成的字符串放入數據庫中,接下來我要配置一下個人jdbcRealm,由於我要指定個人鹽是什麼。在這裏個人鹽就是個人用戶名,每一個用戶的用戶名是不同的,所以這裏無法寫死,在JdbcRealm中,系統提供了四種不一樣的SaltStyle,以下:

SaltStyle 含義
NO_SALT 默認,密碼不加鹽
CRYPT 密碼是以Unix加密方式儲存的
COLUMN salt是單獨的一列儲存在數據庫中
EXTERNAL salt沒有儲存在數據庫中,須要經過JdbcRealm.getSaltForUser(String)函數獲取

四種不一樣的SaltStyle對應了四種不一樣的密碼處理方式,部分源碼以下:

switch (saltStyle) {
case NO_SALT:
    password = getPasswordForUser(conn, username)[0];
    break;
case CRYPT:
    // TODO: separate password and hash from getPasswordForUser[0]
    throw new ConfigurationException("Not implemented yet");
    //break;
case COLUMN:
    String[] queryResults = getPasswordForUser(conn, username);
    password = queryResults[0];
    salt = queryResults[1];
    break;
case EXTERNAL:
    password = getPasswordForUser(conn, username)[0];
    salt = getSaltForUser(username);
}

在COLUMN這種狀況下,SQL查詢結果應該包含兩列,第一列是密碼,第二列是鹽,這裏默認執行的SQL在JdbcRealm一開頭就定義好了,以下:

protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";

即系統默認的鹽是數據表中的password_salt提供的,可是我這裏是username字段提供的,因此這裏我一會要自定義這條SQL。自定義方式很簡單,修改shiro.ini文件,添加以下兩行:

jdbcRealm.saltStyle=COLUMN
jdbcRealm.authenticationQuery=select password,username from users where username=?

首先設置saltStyle爲COLUMN,而後從新定義authenticationQuery對應的SQL。注意返回列的順序很重要,不能隨意調整。如此以後,系統就會自動把username字段做爲鹽了。

不過,因爲ini文件中不支持枚舉,saltStyle的值其實是一個枚舉類型,因此咱們在測試的時候,須要增長一個枚舉轉換器在咱們的main方法中,以下:

BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() {
    @Override
    protected String convertToString(Object value) throws Throwable {
        return ((Enum) value).name();
    }

    @Override
    protected Object convertToType(Class type, Object value) throws Throwable {
        return Enum.valueOf(type, value.toString());
    }

    @Override
    protected Class getDefaultType() {
        return null;
    }
}, JdbcRealm.SaltStyle.class);

固然,之後當咱們將shiro和web項目整合以後,就不須要這個轉換器了。

如此以後,咱們就能夠再次進行登陸測試了,會發現沒什麼問題了。

7.3 非JdbcRealm如何配置鹽

OK,剛剛是在JdbcRealm中配置了鹽,若是沒用JdbcRealm,而是本身定義的普通Realm,要怎麼解決配置鹽的問題?

首先要說明一點是,咱們前面的文章在自定義Realm時都是經過實現Realm接口實現的,這種方式有一個缺陷,就是密碼比對須要咱們本身完成,通常在項目中,咱們自定義Realm都是經過繼承AuthenticatingRealm或者AuthorizingRealm,由於這兩個方法中都重寫了getAuthenticationInfo方法,而在getAuthenticationInfo方法中,調用doGetAuthenticationInfo方法獲取登陸用戶,獲取到以後,會調用assertCredentialsMatch方法進行密碼比對,而咱們直接實現Realm接口則沒有這一步,部分源碼以下:

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        //調用doGetAuthenticationInfo獲取info,這個doGetAuthenticationInfo是咱們在自定義Realm中本身實現的
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }
    if (info != null) {
        //獲取到info以後,進行密碼比對
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}

基於上面所述的緣由,這裏我先繼承AuthenticatingRealm,以下:

public class MyRealm extends AuthenticatingRealm {
    public String getName() {
        return "MyRealm";
    }
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("用戶不存在");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
        return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
    }
}

關於這個類,我說以下幾點:

  1. 用戶名我這裏仍是手動判斷了下,實際上這個地方要從數據庫查詢用戶信息,若是查不到用戶信息,則直接拋UnknownAccountException
  2. 返回的SimpleAuthenticationInfo中,第二個參數是密碼,正常狀況下,這個密碼是從數據庫中查詢出來的,我這裏直接寫死了
  3. 第三個參數是鹽值,這樣構造好SimpleAuthenticationInfo以後返回,shiro會去判斷用戶輸入的密碼是否正確

上面的核心步驟是第三步,系統去自動比較密碼輸入是否正確,在比對的過程當中,須要首先對用戶輸入的密碼進行加鹽加密,既然加鹽加密,就會涉及到credentialsMatcher,這裏咱們要用的credentialsMatcher實際上和在JdbcRealm中用的credentialsMatcher同樣,只須要在配置文件中增長以下一行便可:

MyRealm.credentialsMatcher=$sha512

sha512和咱們上文定義的一致,這裏就再也不重複說了。

本小節案例下載:https://github.com/lenve/shir...

8. Shiro中自定義帶角色和權限的Realm

密碼加密加鹽小夥伴們應該沒有問題了,可是前面幾篇文章又給咱們帶來了一個新的問題:咱們前面IniRealm、JdbcRealm以及自定義的MyRealm,其中前兩個咱們都能實現用戶認證以及受權,即既能管理用戶登陸,又能管理用戶角色,而咱們自定義的MyRealm,目前還只能實現登陸,不能實現受權,本文咱們就來看看自定義Realm如何實現受權。

8.1 問題追蹤

上篇文章咱們沒有實現自定義Realm的受權操做,可是這個並不影響咱們調用hasRole方法去獲取用戶的權限,我在上文測試代碼上的currentUser.hasRole上面打斷點,經過層層追蹤,咱們發現最終來到了ModularRealmAuthorizer類的hasRole方法中,部分源碼以下:

public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
    assertRealmsConfigured();
    for (Realm realm : getRealms()) {
        if (!(realm instanceof Authorizer)) continue;
        if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
            return true;
        }
    }
    return false;
}

咱們看到在這裏會遍歷全部的realm,若是這個realm是Authorizer的實例,則會進行進一步的受權操做,若是不是Authorizer的實例,則直接跳過,而咱們只有一個自定義的MyRealm繼承自AuthenticatingRealm,很明顯不是Authorizer的實例,因此這裏必然返回false,受權失敗,因此要解決受權問題,第一步,得先讓咱們的MyRealm成爲Authorizer的實例。

8.2 解決方案

以下圖是Authorizer的繼承關係:

p311

小夥伴們看到,在Authorizer的實現類中有一個AuthorizingRealm,打開這個類,咱們發現它的繼承關係以下:

public abstract class AuthorizingRealm extends AuthenticatingRealm
        implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
            ...
        }

咱們發現,這個AuthorizingRealm不只是Authorizer的實現類,同時也是咱們上文所用的AuthenticatingRealm的實現類,既然AuthorizingRealm同時是這兩個類的實現類,那麼我把MyRealm的繼承關係由AuthenticatingRealm改成AuthorizingRealm,確定不會影響我上文的功能,修改以後的MyRealm以下(部分關鍵代碼):

public class MyRealm extends AuthorizingRealm {
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("用戶不存在");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
        return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Set<String> roles = new HashSet<String>();
        if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
            roles.add("普通用戶");
        }
        return new SimpleAuthorizationInfo(roles);
    }
}

繼承了AuthorizingRealm以後,須要咱們實現doGetAuthorizationInfo方法。在這個方法中,咱們配置用戶的權限。這裏我爲了方便,直接添加了普通用戶這個權限,實際上,這裏應該根據用戶名去數據庫裏查詢權限,查詢方式不贅述。

經過源碼追蹤,咱們發現最終受權會來到AuthorizingRealm類的以下兩個方法中:

public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
    AuthorizationInfo info = getAuthorizationInfo(principal);
    return hasRole(roleIdentifier, info);
}

protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
    return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}

這兩個方法的邏輯很簡單,第一個方法中調用的getAuthorizationInfo方法會最終調用到咱們自定義的doGetAuthorizationInfo方法,第二個hasRole方法接收的兩個參數,第一個是用戶申請的角色,第二個是用戶具有的角色集,一個簡單的contains函數就判斷出用戶是否具有某個角色了。

可是這個時候,用戶只有角色,沒有權限,咱們能夠對doGetAuthorizationInfo方法作進一步的完善,以下:

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    Set<String> roles = new HashSet<String>();
    Set<String> permiss = new HashSet<String>();
    if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
        roles.add("普通用戶");
        permiss.add("book:update");
    }
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
    info.setStringPermissions(permiss);
    return info;
}

固然,正常狀況下,權限也應當是從數據庫中查詢獲得的,我這裏簡化下。

那麼這個角色是怎麼驗證的呢?追蹤源碼咱們來到了AuthorizingRealm類的以下兩個方法中:

public boolean isPermitted(PrincipalCollection principals, Permission permission) {
    AuthorizationInfo info = getAuthorizationInfo(principals);
    return isPermitted(permission, info);
}

//visibility changed from private to protected per SHIRO-332
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
    Collection<Permission> perms = getPermissions(info);
    if (perms != null && !perms.isEmpty()) {
        for (Permission perm : perms) {
            if (perm.implies(permission)) {
                return true;
            }
        }
    }
    return false;
}

第一個isPermitted方法中調用了getAuthorizationInfo方法,而getAuthorizationInfo方法最終會調用到咱們本身定義的doGetAuthorizationInfo方法,即獲取到用戶的角色權限信息,而後在第二個方法中進行遍歷判斷,查看是否具有相應的權限,第二個isPermitted方法的第一個參數就是用戶要申請的權限。

本小節案例下載:https://github.com/lenve/shir...

9. Shiro整合Spring

9.1 Spring&SpringMVC環境搭建

Spring和SpringMVC環境的搭建,總體上來講,仍是比較容易的,由於這個不是本文的重點,所以這裏我不作詳細介紹,小夥伴能夠在文末下載源碼查看Spring+SpringMVC環境的搭建。同時,因爲MyBatis的整合相對要容易不少,這裏爲了下降項目複雜度,我也就先不引入MyBatis。

對於項目依賴,除了Spring、SpringMVC、Shiro相關的依賴,還須要加入Shiro和Spring整合的jar,以下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>RELEASE</version>
</dependency>

9.2 整合Shiro

搭建好Spring+SpringMVC環境以後,整合Shiro咱們主要配置兩個地方:

  1. web.xml中配置代理過濾器,以下:
<filter>
    <filter-name>shiroFilter</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>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

這樣以後,當DelegatingFilterProxy攔截到全部請求以後,都會委託給shiroFilter來處理,shiroFilter是咱們第二步在Spring容器中配置的一個實例。

  1. 配置Spring容器

在Spring容器中至少有兩個Bean須要咱們配置,一個就是第一步中的shiroFilter,還有一個就是SecurityManager,完整配置以下:

<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
</bean>
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>

這是一個很是簡單的配置,咱們在之後的文章中還會繼續完善它,關於這個配置我說以下幾點:

  1. 首先咱們須要配置一個securityManager,到時候咱們的realm要配置在這裏。
  2. 還要配置一個名爲shiroFilter的bean,這個名字要和web.xml中代理過濾器的名字一致。
  3. shiroFilter中,loginUrl表示登陸頁面地址。
  4. successUrl表示登陸成功地址。
  5. unauthorizedUrl表示受權失敗地址。
  6. filterChainDefinitions中配置的/**=authc表示全部的頁面都須要認證(登陸)以後才能訪問。
  7. authc其實是一個過濾器,這個咱們在後文還會再詳細說到。
  8. 匹配符遵循Ant風格路徑表達式,這裏能夠配置多個,匹配順序從上往下匹配到了就再也不匹配了。好比下面這個寫法:
/a/b/*=anon
/a/**=authc

假設個人路徑是/a/b/c那麼就會匹配到第一個過濾器anon,而不會匹配到authc,因此這裏的順序很重要。

OK,這些配置寫完後,在webpap目錄下建立對應的jsp文件,以下:

p312

此時,啓動項目去瀏覽器中訪問,不管咱們訪問什麼地址,最後都會回到login.jsp頁面,由於全部的頁面(即便不存在的地址)都須要認證後才能夠訪問。

本小節案例:https://github.com/lenve/shir...

10. Shiro處理登陸的三種方式

10.1 準備工做

很明顯,無論是那種登陸,都離不開數據庫,這裏數據庫我採用咱們前面的數據庫,這裏不作贅述(文末能夠下載數據庫腳本),可是我這裏須要首先配置JdbcRealm,在applicationContext.xml中首先配置數據源,以下:

<context:property-placeholder location="classpath:db.properties"/>
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
    <property name="username" value="${db.username}"/>
    <property name="password" value="${db.password}"/>
    <property name="url" value="${db.url}"/>
</bean>

有了數據源以後,接下來配置JdbcRealm,以下:

<bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm">
    <property name="dataSource" ref="dataSource"/>
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="sha-512"/>
            <property name="hashIterations" value="1024"/>
        </bean>
    </property>
    <property name="saltStyle" value="COLUMN"/>
    <property name="authenticationQuery" value="select password, username from users where username = ?"/>
</bean>

JdbcRealm中這幾個屬性和咱們本系列第七篇文章基本是一致的,首先咱們配置了密碼比對器爲HashedCredentialsMatcher,相應的算法爲sha512,密碼加密迭代次數爲1024次,而後咱們配置了密碼的鹽從數據表的列中來,username列就是咱們的鹽,這些配置和前文都是一致的,不清楚的小夥伴能夠參考咱們本系列第七篇文章。

10.2 自定義登陸邏輯

自定義登陸邏輯比較簡單,首先咱們把login.jsp頁面進行簡單改造:

<form action="/login" method="post">
    <table>
        <tr>
            <td>用戶名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="登陸"></td>
        </tr>
    </table>
</form>

而後建立咱們的登陸處理Controller,以下:

@PostMapping("/login")
public String login(String username, String password) {
    Subject currentUser = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        currentUser.login(token);
        return "success";
    } catch (AuthenticationException e) {
    }
    return "login";
}

登陸成功咱們就去success頁面,登陸失敗就回到登陸頁面。作完這兩步以後,咱們還要修改shiroFilter中的filterChainDefinitions屬性,要設置/login接口能夠匿名訪問,以下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /login=anon
            /**=authc
        </value>
    </property>
</bean>

作完這些以後,就能夠去login.jsp頁面測試登陸了。

上面中方式是咱們本身寫登陸邏輯,shiro也給咱們提供了兩種不用本身寫登陸邏輯的登陸方式,請繼續往下看。

10.3 基於HTTP的認證

shiro中也提供了基於http協議的認證,固然,這種認證也得有數據庫的輔助,數據配置和前文同樣,咱們只須要修改一個配置便可,以下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authcBasic
        </value>
    </property>
</bean>

這個表示全部的頁面都要通過基於http的認證。此時咱們打開任意一個頁面,認證方式以下:

p313

10.4 表單登陸

表單登陸和基於HTTP的登陸相似,都是不須要咱們本身寫登陸邏輯的登陸,可是出錯的邏輯仍是要稍微處理下,首先修改shiroFilter:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>

配置登陸頁面,也配置登陸成功後的跳轉頁面,同時設置全部頁面都要登陸後才能訪問。

配置登陸頁面請求,以下:

@RequestMapping("/login")
public String login(HttpServletRequest req, Model model) {
    String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure");
    if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
        model.addAttribute("error", "帳戶不存在!");
    }
    if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
        model.addAttribute("error", "密碼不正確!");
    }
    return "login";
}

若是登陸失敗,那麼在request中會有一個shiroLoginFailure的屬性中保存了登陸失敗的異常類名,經過判斷這個類名,咱們就能夠知道是什麼緣由致使了登陸失敗。

OK,配置好這兩步以後,就能夠去登陸頁面測試了。

10.5 註銷登陸

註銷登陸比較簡單,就一個過濾器,按以下方式配置:

<property name="filterChainDefinitions">
    <value>
        /logout=logout
        /**=authc
    </value>
</property>

經過get請求訪問/logout便可註銷登陸。

本小節有三個案例,下載地址以下:

11. Shiro中的受權問題

11.1 配置角色

本文的案例在上文的基礎上完成,所以Realm這一塊我依然採用JdbcRealm,相關的受權就沒必要配置了。可是這裏的數據庫腳本有更新,小夥伴須要下載從新執行(https://github.com/lenve/shir...

先來介紹下目前數據庫中用戶的狀況,數據庫中有兩個用戶,sang具備admin的角色,同時具備book:*author:create兩個權限,lisi具備user的角色,同時具備user:infouser:delete兩個權限。修改shiroFilter,以下:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /admin.jsp=authc,roles[admin]
            /user.jsp=authc,roles[user]
            /logout=logout
            /**=authc
        </value>
    </property>
</bean>

關於這裏的配置,我說以下幾點:

  1. unauthorizedUrl表示受權失敗時展現的頁面
  2. filterChainDefinitions中咱們配置了admin.jsp頁面必須登陸後才能訪問,同時登陸的用戶必須具備admin角色,user.jsp也是必須登陸後才能訪問,同時登陸的用戶必須具備user角色

11.2 測試

測試時咱們分別用sang/123和lisi/123進行登陸,登陸成功後分別訪問user.jsp和admin.jsp就能看到效果。

11.3 配置權限

上面的方式是配置角色,可是尚未配置權限,要配置權限,首先要在jdbcRealm中添加容許權限信息的查詢:

<property name="permissionsLookupEnabled" value="true"/>

而後配置下shiroFilter:

<property name="filterChainDefinitions">
    <value>
        /admin.jsp=authc,roles[admin]
        /user.jsp=authc,roles[user]
        /userinfo.jsp=authc,perms[user:info]
        /bookinfo.jsp=authc,perms[book:info]
        /logout=logout
        /**=authc
    </value>
</property>

這裏假設訪問userinfo.jsp須要user:info權限,訪問bookinfo.jsp須要book:info權限。

OK,作完這些以後就能夠測試了,分別用sang/123和lisi/123進行登陸,登陸成功後分別訪問bookinfo.jsp和userinfo.jsp就能夠看到不一樣效果了。

本小節案例下載:https://github.com/lenve/shir...

12. Shiro中的JSP標籤

12.1 緣起

上篇文章中,咱們在success.jsp中寫了不少像下面這種超連接:

<h1>登陸成功!</h1>
<h3><a href="/logout">註銷</a></h3>
<h3><a href="/admin.jsp">admin.jsp</a></h3>
<h3><a href="/user.jsp">user.jsp</a></h3>
<h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
<h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>

可是對於不一樣身份的用戶,並非每個連接都是有效的,點擊無效的連接會進入到未受權的頁面,這樣用戶體驗並很差,最好可以把不可達的連接自動隱藏起來,同時,我也但願可以方便獲取當前登陸用戶的信息等,考慮到這些需求,咱們來聊聊shiro中的jsp標籤。

12.2 標籤介紹

shiro中的標籤並很少,主要有以下幾種:

  1. shiro:guest

shiro:guest標籤只有在當前未登陸時顯示裏邊的內容,以下:

<shiro:guest>
    歡迎【遊客】訪問!
</shiro:guest>
  1. shiro:user

shiro:user是在用戶登陸以後顯示該標籤中的內容,不管是經過正常的登陸仍是經過Remember Me登陸,以下:

<shiro:user>
    歡迎【<shiro:principal/>】訪問!
</shiro:user>
  1. shiro:principal

shiro:principal用來獲取當前登陸用戶的信息,顯示效果以下:

p314

4.shiro:authenticated

和shiro:user相比,shiro:authenticated的範圍變小,當用戶認證成功且不是經過Remember Me認證成功,這個標籤中的內容纔會顯示出來:

<shiro:authenticated>
    用戶【<shiro:principal/>】身份認證經過,不是經過Remember Me認證!
</shiro:authenticated>
  1. shiro:notAuthenticated

shiro:notAuthenticated也是在用戶未認證的狀況下顯示內容,和shiro:guest不一樣的是,對於經過Remember Me方式進行的認證,shiro:guest不會顯示內容,而shiro:notAuthenticated會顯示內容(由於此時並非遊客,可是又確實未認證),以下:

<shiro:notAuthenticated>
    用戶未進行身份認證
</shiro:notAuthenticated>
  1. shiro:lacksRole

當用戶不具有某個角色時候,顯示內容,以下:

<shiro:lacksRole name="admin">
    用戶不具有admin角色
</shiro:lacksRole>
  1. shiro:lacksPermission

當用戶不具有某個權限時顯示內容:

<shiro:lacksPermission name="book:info">
    用戶不具有book:info權限
</shiro:lacksPermission>
  1. shiro:hasRole

當用戶具有某個角色時顯示的內容:

<shiro:hasRole name="admin">
    <h3><a href="/admin.jsp">admin.jsp</a></h3>
</shiro:hasRole>
  1. shiro:hasAnyRoles

當用戶具有多個角色中的某一個時顯示的內容:

<shiro:hasAnyRoles name="user,aaa">
    <h3><a href="/user.jsp">user.jsp</a></h3>
</shiro:hasAnyRoles>
  1. shiro:hasPermission

當用戶具有某一個權限時顯示的內容:

<shiro:hasPermission name="book:info">
    <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
</shiro:hasPermission>

本小節案例下載:https://github.com/lenve/shir...

13.Shiro 中的緩存機制

13.1 添加依賴

使用緩存,首先須要添加相關依賴,以下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

13.2 添加配置文件

ehcache的配置文件主要參考官方的配置,在resources目錄下建立ehcache.xml文件,內容以下:

<ehcache>
    <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           eternal="true"
           overflowToDisk="true"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600"/>
    <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
           maxElementsInMemory="100"
           eternal="false"
           timeToLiveSeconds="600"
           overflowToDisk="false"/>
</ehcache>

這些都是ehcache緩存中常規的配置,含義我就不一一解釋了,文末下載源碼有註釋。

13.3 緩存配置

接下來咱們只須要在applicationContext中簡單配置下緩存便可,配置方式以下:

<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager">
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
    <property name="realm" ref="jdbcRealm"/>
    <property name="cacheManager" ref="cacheManager"/>
</bean>

首先配置EhCacheManager類,指定緩存位置,而後在DefaultWebSecurityManager中引入cacheManager便可,如此以後,咱們的緩存就應用上了。

13.4 測試

因爲我這裏使用了JdbcRealm,若是使用了自定義Realm那麼能夠經過打日誌看是否使用了緩存,使用了JdbcRealm以後,咱們能夠經過打斷點來查看是否應用了緩存,好比我執行以下代碼:

subject.checkRole("admin");
subject.checkPermission("book:info");

經過斷點跟蹤,發現最終會來到AuthorizingRealm的getAuthorizationInfo方法中,在該方法中,首先會去緩存中檢查數據,若是緩存中有數據,則不會執行doGetAuthorizationInfo方法(數據庫操做就在doGetAuthorizationInfo方法中進行),若是緩存中沒有數據,則會執行doGetAuthorizationInfo方法,而且在執行成功後將數據保存到緩存中(前提是配置了緩存,cache不爲null),此時咱們經過斷點,發現執行了緩存而沒有查詢數據庫中的數據,部分源碼以下:

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
    AuthorizationInfo info = null;
    Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
    if (cache != null) {
        Object key = getAuthorizationCacheKey(principals);
        info = cache.get(key);
    }
    if (info == null) {
        info = doGetAuthorizationInfo(principals);
        if (info != null && cache != null) {
            Object key = getAuthorizationCacheKey(principals);
            cache.put(key, info);
        }
    }
    return info;
}

OK,總體來講shiro中的緩存配置仍是很是簡單的。

That's all.

本小節案例下載地址:https://github.com/lenve/shir...

待續。。。

相關文章
相關標籤/搜索