極簡入門,Shiro的認證與受權流程解析

小Hub領讀:

接下來的幾天,咱們開講Shiro,從入門到分析、集成、單點登陸整合等幾篇。今天咱們先來認識一下Shiro吧~html


其實Shiro框架並不難,我梳理了一下,你只須要學會如下內容基本就足夠了:git

  • 登錄、受權流程
  • shiro過濾器鏈
  • 整合Springboot、redis作共享會話
  • 結合xxl-sso實現單點登陸

接下來我會分爲幾篇文章分別去介紹,這篇咱們先來了解一下shiro的一些基礎知識,以及登陸受權邏輯。github

Shiro簡介

在Web系統中咱們常常要涉及到權限問題,例如不一樣角色的人登陸系統,他操做的功能、按鈕、菜單是各不相同的,這就是所謂的權限。web

而構建一個互聯網應用,權限校驗管理是很重要的安全措施,這其中主要包含:面試

  • 用戶認證 - 用戶身份識別,即登陸
  • 用戶受權 - 訪問控制
  • 密碼加密 - 加密敏感數據防止被偷窺
  • 會話管理 - 與用戶相關的時間敏感的狀態信息

Shiro對以上功能都進行了很好的支持,它能夠很是容易的開發出足夠好的應用。Shiro能夠幫助咱們完成:認證、受權、加密、會話管理、與Web集成、緩存等。並且Shiro的API也是很是簡單。redis

官方源碼:https://github.com/apache/shiroapache

總體結構與重要組件

圖片

從上圖能夠看出,Security Manager是Shiro的核心管理器,認證受權會話緩存等都是在其內部完成,而後會委託給具體的組件來處理,好比認證過程委託給Authenticator,受權委託給Authorizer組件。因此,整理仍是比較清晰,源代碼也容易追蹤。緩存

咱們來具體聊聊全部的組件:安全

Subject: 主體,能夠看到主體能夠是任何能夠與應用交互的「用戶」;session

SecurityManager: Shiro的心臟;全部具體的交互都經過SecurityManager進行控制;負責全部Subject、且負責進行認證和受權、及會話、緩存的管理。

  • Authenticator:認證器,判斷用戶是否正常登錄
  • Authorizer:受權器,判斷用戶是否有權限操做資源

Realm: 能夠有1個或多個Realm,主要提供認證和受權的數據;

Session: Shiro提供一個權限的企業級Session解決方案,session的生命週期都在SessionManager中進行管理。

SessionManager: shiro的會話管理器;

SessionDAO: 用於會話的CRUD,好比存儲到ehcache或者redis中的會話增刪改查;

CacheManager: 緩存控制器,來管理如用戶、角色、權限等的緩存的;由於這些數據基本上不多去改變,放到緩存中後能夠提升訪問的性能

Cryptography: 密碼模塊,Shiro提升了一些常見的加密組件用於如密碼加密/解密的。

官方簡單示例

官網例子:http://shiro.apache.org/tutorial.html

剛入門Shiro的同窗,真的須要去看看這個官方例子,你能夠更加深刻了解Shiro的權限校驗流程。我仍是貼一下代碼吧,一些同窗比較懶:

  • shiro.ini
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

上面代碼中,root = secret, admin表示,用戶名root,密碼secret,角色是admin;schwartz = lightsaber:*表示角色schwartz擁有權限lightsaber:*。你其實能夠把這個文件當作一個Realm,其實就是shiro默認的IniRealm。

  • 測試類Tutorial
public class Tutorial {

    private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

    public static void main(String[] args) {
        log.info("My First Apache Shiro Application");

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

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            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 more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        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.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}

從上面的實例中,咱們能夠總結一下經常使用的API:

經常使用API

#獲取當前用戶
Subject currentUser = SecurityUtils.getSubject(); 
#判斷用戶已經認證
currentUser.isAuthenticated() 
#用戶登陸憑證
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); 
#記住我
token.setRememberMe(true); 
#登錄校驗
currentUser.login(token); 
#判斷是否有角色權限
currentUser.hasRole("schwartz") 
#判斷是否有資源操做權限
currentUser.isPermitted("lightsaber:wield") 
#登出
currentUser.logout();

其實稍微梳理一下,能夠發現上面代碼主要有兩個步驟:

  • 認證:
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
currentUser.login(token);
currentUser.logout();
  • 判斷權限
currentUser.hasRole("schwartz")
currentUser.isPermitted("winnebago:drive:eagle5")

接下來,咱們去探討一下shiro的認證與受權流程,並從源碼層去解析一下shiro各個組件之間的關係。

認證流程

圖片

上面圖片中,根據序號,其實咱們大概能猜出裏shiro的認證流程:

  1. Subject進行login操做,參數是封裝了用戶信息的token
  2. Security Manager進行登陸操做
  3. Security Manager委託給Authenticator進行認證邏輯處理
  4. 調用AuthenticationStrategy進行多Realm身份驗證
  5. 調用對應Realm進行登陸校驗,認證成功則返回用戶屬性,失敗則拋出對應異常

咱們從login方法開始debug一下流程,用簡要方式追蹤shiro源碼的認證邏輯:

currentUser.login(token);
|
Subject subject = this.securityManager.login(this, token);
|
AuthenticationInfo info = this.authenticate(token);
|
this.authenticator.authenticate(token);
|
AuthenticationInfo info = this.doAuthenticate(token);
|
Collection<Realm> realms = this.getRealms();
doSingleRealmAuthentication(realm, token);
|
AuthenticationInfo info = realm.getAuthenticationInfo(token);
|
AuthenticationInfo info = realm.doGetAuthenticationInfo(token);

ok,一條線下來,從login到委託給authenticator,再最後調用realm的doGetAuthenticationInfo方法。

因此,從源碼上來看,若是要實現shiro的認證邏輯,至少要準備一個Realm組件、和初始化securityManager組件。

常見異常

  • DisabledAccountException(禁用的賬號)
  • LockedAccountException(鎖定的賬號)
  • UnknownAccountException(錯誤的賬號)
  • ExcessiveAttemptsException(登陸失敗次數過多)
  • IncorrectCredentialsException (錯誤的憑證)
  • ExpiredCredentialsException(過時的憑證)

受權流程

圖片

從上圖中,咱們能夠知道受權流程以下:

  • 調用Subject.isPermitted/hasRole接口
  • 委託給SecurityManager
  • 而SecurityManager接着會委託給Authorizer
  • Authorizer會判斷Realm的角色/權限是否和傳入的匹配
  • 匹配如isPermitted/hasRole會返回true,不然返回false表示受權失敗

追蹤一下源碼以下:

currentUser.hasRole("schwartz")
|
this.securityManager.hasRole(this.getPrincipals(), roleIdentifier)
|
this.authorizer.hasRole(principals, roleIdentifier)
|
AuthorizationInfo info = this.getAuthorizationInfo(principal);
return info.getRoles().contains(roleIdentifier)
|
info = this.doGetAuthorizationInfo(principals);(realm)

因此shiro判斷用戶是否有權限首先會從realm中獲取用戶所擁有的權限角色信息,而後再匹配當前的角色或權限是否包含,從而斷定用戶是否有權限!

說到權限,不少人天然會想起權限系統,涉及到幾個關鍵對象:

  • 主體(Subject)
  • 資源(Resource)
  • 權限(Permission)
  • 角色(Role)

經過這幾個要素,能夠設計出比較合理的權限系統。

Shiro常見3種受權判斷方式:

  • 編碼實現
Subject subject = SecurityUtils.getSubject();  
if(subject.hasRole(「admin」)) {  
    //有權限  
} else {  
    //無權限  
}
  • 註解實現
@RequiresRoles("admin")  
public void hello() {  
    //有權限  
}
  • JSP Taglig實現,freemarker等相似
<shiro:hasRole name="admin">  
<!— 有權限 —>  
</shiro:hasRole>

jsp頁面引入shiro標籤

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags"%>

在線會話管理

獲取當前會話總人數

@Autowired
private SessionDAO sessionDAO;
//獲取會話數量
int size = sessionDAO.getActiveSessions().size()

強制下線

//強制退出
Session session = sessionDAO.readSession(subject.getSession().getId());
sessionDAO.delete(session);
// logout,可做爲強制退出
subject.logout();
Assert.isTrue(!subject.isAuthenticated());

結束語

ok,感受是高度極簡的一篇文章,主要把重要的組件和登陸、受權幾個流程搞清楚以後,其實shiro基本已經學會了,後面咱們再學一下shiro的幾個主要內置過濾器怎麼使用,如何集成SpringBoot,基本就差很少了。


推薦閱讀:

分享一套SpringBoot開發博客系統源碼,以及完整開發文檔!速度保存!

Github上最值得學習的100個Java開源項目,涵蓋各類技術棧!

2020年最新的常問企業面試題大全以及答案

相關文章
相關標籤/搜索