Shiro的實現機制(源碼解析)

什麼是shiro?

Apache Shiro官網上對Shiro的解釋如下:

Apache Shiro (pronounced “shee-roh”, the Japanese word for ‘castle’) is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management and can be used to secure any application - from the command line applications, mobile applications to the largest web and enterprise applications.
----Shiro provides the application security API to perform the following aspects (I like to call these the 4 cornerstones of application security):
* Authentication - proving user identity, often called user ‘login’.
* Authorization - access control
* Cryptography - protecting or hiding data from prying eyes
* Session Management - per-user time-sensitive state

大概意思就是說:
shiro是一個功能強大且易於使用的Java安全框架,它的認證,授權,加密和會話管理可以用於保護任何應用程序——來自從命令行應用程序、移動應用程序到最大的web和企業應用程序。
shiro爲以下幾個方面提供應用程序的安全API(應用程序安全的4大基石):

  • Authentication - 提供用戶身份認證,俗稱登錄
  • Authorization - 訪問權限控制
  • Cryptography - 使用加密算法保護或者隱藏數據
  • Session Management - 用戶的會話管理

Login

這部分我會結合自己項目中的一些實例來做出分析:
我的LoginController中對登錄操作所做的處理:

@RequestMapping("/doLogin")
    public String doLogin(String loginName, String password, HttpServletRequest request){
        String result = "redirect:/index";
        String fail = "/login";
        String errorMsg = "";

        // shiro認證
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginName, password);
        try {
            subject.login(token);
            User user = userService.getUserByLogiName(loginName);
            if(user != null){
                user.setPassword("");
                //userCache.put(user);
                redisTemplate.opsForValue().set(Constant.cacheName,user);
            }
        } catch (UnknownAccountException e) {
            result = fail;
            errorMsg = "賬戶不存在";
        } catch (DisabledAccountException e) {
            result = fail;
            errorMsg = "賬戶存在問題";
        } catch (AuthenticationException e) {
            result = fail;
            errorMsg = "密碼錯誤";
        } catch (Exception e) {
            result = fail;
            errorMsg = "登陸異常";
            log.error(e.getMessage(),e);
        }

        request.setAttribute("error",errorMsg);
        return result;
    }

在進行登錄操作的方法doLogin()裏首先要做的是shiro認證這個過程。

shiro認證

這個過程主要分爲三個部分:

  • 1:獲取客戶端輸入放入用戶名,密碼。
  • 2:獲取數據源中存放的數據即相應的用戶名,密碼。
  • 3:進行兩者的比對,判斷是否登錄操作成功。
    先看一下shiro中是如何實現這個認證的過程的吧:
    在這裏插入圖片描述
    這裏的意思就是說,通過當前的用戶對象Subject執行login()方法將用戶信息傳給Shiro的SecurityManager,而這個SecurityManager會將用戶信息委託給內部登錄模塊,由內部登錄模塊來調用Realm中的方法來進行數據比對進而判斷是否登錄成功。

那麼我們就有疑問了,這裏怎麼有出現了三個關鍵詞Subject,SecurityManager和Realm這又有什麼用呢?

Subject,SecurityManager和Realm

Subject

對於subject官網上是這樣定義的:

Subject
When you’re securing your application, probably the most relevant questions to ask yourself are, “Who is the current user?” or “Is the current user allowed to do X”? It is common for us to ask ourselves these questions as we're writing code or designing user interfaces: applications are usually built based on user stories, and you want functionality represented (and secured) based on a per-user basis. So, the most natural way for us to think about security in our application is based on the current user. Shiro’s API fundamentally represents this way of thinking in its Subject concept.

The word Subject is a security term that basically means "the currently executing user". It's just not called a 'User' because the word 'User' is usually associated with a human being. In the security world, the term 'Subject' can mean a human being, but also a 3rd party process, daemon account, or anything similar. It simply means 'the thing that is currently interacting with the software'. For most intents and purposes though, you can think of this as Shiro’s ‘User’ concept. You can easily acquire the Shiro Subject anywhere in your code as shown in Listing 1 below.

大意就是:subject指的是當前用戶,因爲我們人的思維更傾向於某個用戶有某個角色,因此可以理解爲基於當前用戶。(不過在安全領域,術語“Subject”可以指一個人,也可以指第三方進程、守護進程帳戶或任何類似的東西。)
它的獲取方法在官方文檔中定義爲:相信在我上面的部分項目代碼中大家也已經看到了,這塊用法是固定的:

import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();

一旦您獲得了subject,就可以立即訪問當前用戶使用Shiro想要做的90%的事情,比如登錄、註銷、訪問他們的會話、執行授權檢查,等等.

SecurityManager

SecurityManager是shiro架構核心,協調內部安全組件(如登錄,授權,數據源等),用來管理所有的subject。這塊會後續進行補充說明。

Realm

官網定義如下:

Realms
The third and final core concept in Shiro is that of a Realm. A Realm acts as the ‘bridge’ or ‘connector’ between Shiro and your application’s security data. That is, when it comes time to actually interact with security-related data like user accounts to perform authentication (login) and authorization (access control), Shiro looks up many of these things from one or more Realms configured for an application.

In this sense a Realm is essentially a security-specific DAO: it encapsulates connection details for data sources and makes the associated data available to Shiro as needed. When configuring Shiro, you must specify at least one Realm to use for authentication and/or authorization. More than one Realm may be configured, but at least one is required.

Shiro provides out-of-the-box Realms to connect to a number of security data sources (aka directories) such as LDAP, relational databases (JDBC), text configuration sources like INI and properties files, and more. You can plug-in your own Realm implementations to represent custom data sources if the default Realms do not meet your needs. Listing 4 below is an example of configuring Shiro (via INI) to use an LDAP directory as one of the application’s Realms.

大意是指:Realm充當的是Shiro和應用程序安全數據之間的==“橋樑”或“連接器”==。也就是說,當實際需要與與安全相關的數據(如用戶帳戶)進行交互以執行身份驗證(登錄)和授權(訪問控制)時,Shiro會從一個或多個爲應用程序配置的Realm中查找這些內容。也是說Realm本質上是一個特定於安全性的DAO(邏輯處理):它封裝了數據源的連接細節,並根據需要將關聯的數據提供給Shiro。在配置Shiro時,必須指定至少一個用於身份驗證和/或授權的領域。可以配置多個域,但至少需要一個。

Shiro提供了開箱即用的領域,可以連接到許多安全數據源(即目錄),如LDAP、關係數據庫(JDBC)、文本配置源(如INI)和屬性文件,等等。也可以自定義Realm實現來表示自定義數據源。(下文我會貼出項目中自定義的Realm做爲參考)。

下面是詳細的shiro結構圖:

在這裏插入圖片描述

獲取subject

首先是第一步要獲取客戶端傳來的數據

 Subject subject = SecurityUtils.getSubject();

login()

subject.login(token);

內部調用的是subject接口聲明的方法:

void login(AuthenticationToken token) throws AuthenticationException;

我們來看一下login()的具體實現:

    public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        
        Subject subject = securityManager.login(this, token);
        
        PrincipalCollection principals;
        String host = null;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }
        if (principals == null || principals.isEmpty()) {
            String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                    "empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
        this.principals = principals;
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
        Session session = subject.getSession(false);
        if (session != null) {
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }

要注意的是內部調用的是securityManager.login(this, toke)方法。
我們再來進一步的看一下securityManager.login(this, toke)的內部實現:
首先SecurityManager中對login方法的聲明:

Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

實現類DefaultSecurityManager中對login()的實現

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }
        Subject loggedIn = createSubject(token, info, subject);
        onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

在這裏我們發現調用了authenticate(token)
這個方法是從哪裏來的呢?再來看看SecurityManager接口中的方法和它所繼承的類:

 */
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
    void logout(Subject subject);
    Subject createSubject(SubjectContext context);

}

這裏我們看到 SecurityManager 接口繼承了 Authenticator 登錄認證的接口比如登錄(Authenticator),權限驗證(Authorizer)等。
再來看一看Authenticator接口中都聲明瞭哪些方法:

public interface Authenticator {
    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;
}

也就是我們剛纔在DefaultSecurityManager中對login()的實現中調用的方法,忘了的小盆友可以回過頭去看一眼哦,O(∩_∩)O哈哈~。
AbstractAuthenticator中authenticate()的實現:

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        // 調用doAuthenticate方法
        info = doAuthenticate(token);
        if (info == null) {
            ...
        }
    } catch (Throwable t) {
        ...
    }
    ...
}

調用了doAuthenticate(token)方法。
我們再來看ModularRealmAuthenticator中doAuthenticate(token)方法的實現:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        // Realm唯一時
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

調用了doSingleRealmAuthentication(realms.iterator().next(), authenticationToken)
再往下看:doSingleRealmAuthentication的實現:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    if (!realm.supports(token)) {
        ...
    }
    // 調用Realm的getAuthenticationInfo方法獲取AuthenticationInfo信息
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if (info == null) {
        ...
    }
    return info;
}

哇,我們看到了什麼!!!!
realm.getAuthenticationInfo(token)
它調用Realm的getAuthenticationInfo(token)方法。
而在Realm中我們看一下用戶認證方法重寫:

@Service
public class MyShiroRealm extends AuthorizingRealm {

    //用於用戶查詢
    @Reference
    private UserService userService;
    //用戶認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 獲取用戶信息
        String name = authenticationToken.getPrincipal().toString();
        User user = userService.getUserByLogiName(name);
        if(user == null){
            return null;
        }else{
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
            return simpleAuthenticationInfo;
        }
    }

    //角色權限和對應權限添加
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //獲取登錄用戶名
        String name= (String) principalCollection.getPrimaryPrincipal();
        //查詢用戶名稱
        //User user = loginService.findByName(name);
        //添加角色和權限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        /*for (Role role:user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            for (Permission permission:role.getPermissions()) {
                //添加權限
                simpleAuthorizationInfo.addStringPermission(permission.getPermission());
            }
        }*/
        return simpleAuthorizationInfo;
    }
}

主要重寫了倆個方法:
doGetAuthenticationInfo()主要是進行登錄認證
doGetAuthorizationInfo()主要是進行角色權限和對應權限的添加

Shiro 配置

要配置的是ShiroConfig類,Apache Shiro 核心通過 Filter 來實現(類似SpringMvc 通過DispachServlet 來主控制一樣)
filter主要是通過URL規則來進行過濾和權限校驗,所以我們需要定義一系列關於URL的規則和訪問權限。如下:

@Configuration
public class ShiroConfiguration {
    //將自己的驗證方式加入容器
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(new CredentialsMatcher());
        return myShiroRealm;
    }
    //權限管理,配置主要是Realm的管理認證
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    //Filter工廠,設置對應的過濾條件和跳轉條件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,String> map = new HashMap<String, String>();
        //登出
        map.put("/logout","logout");
        //對所有用戶認證
        map.put("/user/**","authc");
        map.put("/org/**","authc");
        map.put("/role/**","authc");
        map.put("/menu/**","authc");
        map.put("/log/**","authc");
        map.put("/index","authc");
        //其他資源不攔截
        map.put("/**","anon");
        map.put("/doLogin","anon");
        //登錄
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首頁
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //錯誤頁面,認證不通過跳轉
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

}

方法一:myShiroRealm()方法
主要是將我自定義的匹配器對象當做參數傳給MyShiroRealm並返回。
(也就是說把我自定義來判斷規則告訴shiro讓shiro來管理)
而在我自定義的密碼匹配器中是這樣實現的:

public class CredentialsMatcher extends SimpleCredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    	UsernamePasswordToken uToken =(UsernamePasswordToken) token;
        //獲得用戶輸入的密碼:(可以採用加鹽(salt)的方式去檢驗)
        String inPassword = new String(uToken.getPassword());
        //獲得數據庫中的密碼
        String dbPassword = (String) info.getCredentials();
        System.err.println("inPassword:" + inPassword);
        System.err.println("dbPassword:" + dbPassword);
        //進行密碼的比對
        boolean flag = PasswordHash.validatePassword(inPassword,dbPassword);
        return flag;
    }
}

這裏我使用的是將客戶端的密碼進行加鹽處理之後再和我數據庫中的數據進行比對判斷。
方法二:securityManager()
實例化了DefaultWebSecurityManager類,將上面myShiroRealm()的返回值當做參數,也就是配置Realm的管理認證。
方法三:shiroFilterFactoryBean(DefaultWebSecurityManager securityManager)
Filter工廠,設置對應的過濾條件和跳轉條件。

異常捕獲

在登錄過程中可能會出現不同的異常,對於不同的異常,我們是如何處理的呢?
當然不同的異常就要分類進行處理,比如密碼錯誤和賬戶不存在就不能一概而論,對於這些問題,我們能做的就是將不同的異常進行捕獲進行不同頁面的跳轉反饋給用戶,提高用戶體驗,比如:

try {
            subject.login(token);
            User user = userService.getUserByLogiName(loginName);
            if(user != null){
                user.setPassword("");
                //userCache.put(user);
                redisTemplate.opsForValue().set(Constant.cacheName,user);
            }
        } catch (UnknownAccountException e) {
            result = fail;
            errorMsg = "賬戶不存在";
        } catch (DisabledAccountException e) {
            result = fail;
            errorMsg = "賬戶存在問題";
        } catch (AuthenticationException e) {
            result = fail;
            errorMsg = "密碼錯誤";
        } catch (Exception e) {
            result = fail;
            errorMsg = "登陸異常";
            log.error(e.getMessage(),e);
        }

這裏要注意的是不要把父類寫在前面捕獲,否則所有的此類異常都會被父類捕獲,子類就不會進行錯誤處理,無法得到錯誤的詳細的歸類處理。

哈哈哈哈哈哈哈哈嗝,今天敲開薰,一直就很疑惑shiro的實現機制,今天終於明白啦。嗷嗷嗷開心開心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章