shiro 學習筆記-編程思維

1. 權限管理

1.1 什麼是權限管理?

權限管理實現對用戶訪問系統的控制,按照安全規則或者安全策略,可以控制用戶隻能訪問自己被授權的資源

權限管理包括用戶身份認證和授權兩部分,簡稱認證授權

1.2 什麼是身份認證?

身份認證就是判斷一個用戶是否為合法用戶的處理過程,最常用的方式就是通過核對用戶輸入和用戶名和口令是否與系統中存儲的一緻,來判斷用戶身份是否正确

1.3 什麼是授權?

授權,即訪問控制,控制誰能訪問哪些資源,主體進行身份認證後需要分配權限方可訪問系統資源,對于某些資源沒有權限是無法訪問的


2. 什麼是 shiro?

shiro 是一個功能強大且易于使用的 Java 安全框架,能夠完成身份認證、授權、加密和會話管理等功能

2.1 shiro 的核心架構

  • Subject

    主體,外部應用與 subject 進行交互,subject 記錄了當前操作用戶,将用戶的概念理解為當前操作的主體,可能是一個通過浏覽器請求的用戶,也可能是一個運行的程序。Subject 在 shiro 中是一個接口,接口中定義了很多認證授相關的方法,外部程序通過 subject 進行認證授,而 subject 是通過 SecurityManager 安全管理器進行認證授權

  • SecurityManager

    安全管理器,對全部的 subject 進行安全管理,是 shiro 的核心。通過 SecurityManager 可以完成 subject 的認證、授權等,實質上 SecurityManager 是通過 Authenticator 進行認證,通過 Authorizer 進行授權,通過 SessionManager 進行會話管理。SecurityManager 是一個接口,繼承了 Authenticator,Authorizer,SessionManager 這三個接口

  • Authenticator

    認證器,對用戶身份進行認證,Authenticator 是一個接口,shiro 提供 ModularRealmAuthenticator 實現類,通過 ModularRealmAuthenticator 基本上可以滿足大多數需求,也可以自定義認證器

  • Authorizer

    授權器,用戶通過認證器認證通過,在訪問功能時需要通過授權器判斷用戶是否有此功能的操作權限

  • Realm

    領域,相當于 datasource 數據源,securityManager 進行安全認證需要通過 Realm 獲取用戶權限數據,比如:如果用戶身份數據在數據庫,那麼 realm 就需要從數據庫獲取用戶身份信息

  • SessionManager

    會話管理,shiro 框架定義了一套會話管理,它不依賴 web 容器的 session,所以 shiro 可以使用在非 web 應用上,也可以将分布式應用的會話集中在一點管理,此特性可使它實現單點登錄

  • SessionDAO

    會話 dao,是對 session 會話操作的一套接口,比如要将 session 存儲到數據庫,可以通過 jdbc 将會話存儲到數據庫

  • CacheManager

    CacheManager 即緩存管理,将用戶權限數據存儲在緩存,這樣可以提高性能

  • Cryptography

    密碼管理,shiro 提供了一套加解密的組件,方便開發,比如提供常用的散列、加解密等功能


3. shiro 中的認證

3.1 認證

身份認證,就是判斷一個用戶是否為合法用戶的處理過程。最常用的身份認證方式是系統通過核對用戶輸入的用戶名和口令,看其是否與系統中存儲的該用戶的用戶名和口令一緻,來判斷用戶身份是否正确

3.2 shiro 認證中的關鍵對象

  • Subject:主體

    訪問系統的用戶,主體可以是用戶、程序等,進行認證的都稱為主體

  • Principal:身份信息

    是主體(subject)進行身份認證的标識,标識必須具有唯一性, 如用戶名、手機号、郵箱地址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)

  • credential:憑證信息

    是隻有主體自己知道的安全信息,如密碼、證書等

3.3 認證流程

3.4 認證開發

3.4.1 引入依賴

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>

3.4.2 創建配置文件

該配置文件用來書寫系統中相關的權限數據,主要用于學習使用

[users]
xiaochen=123
zhangsan=456

3.4.3 開發認證代碼

public class TestAuthenticator {

    public static void main(String[] args) {
        // 1.創建安全管理器對象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.給安全管理器設置 realm
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        // 3.給 SecurityUtils 全局安全工具類設置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.獲取認證主體
        Subject subject = SecurityUtils.getSubject();
        // 5.創建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
        // 6.認證
        try {
            System.out.println("認證狀态:" + subject.isAuthenticated());
            subject.login(token);
            System.out.println("認證狀态:" + subject.isAuthenticated());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.5 自定義 Realm

自定義 Realm 的實現,即是将認證/授權數據來源轉為數據庫的實現

public class TestCustomerRealmAuthenticator {

    public static void main(String[] args) {
        // 1.創建安全管理器對象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.設置自定義realm
        securityManager.setRealm(new CustomerRealm());
        // 3.給 SecurityUtils 全局安全工具類設置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.獲取認證主體
        Subject subject = SecurityUtils.getSubject();
        // 5.創建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
        // 6.認證
        try {
            System.out.println("認證狀态:" + subject.isAuthenticated());
            subject.login(token);
            System.out.println("認證狀态:" + subject.isAuthenticated());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

自定義 Realm 代碼實現

public class CustomerRealm extends AuthorizingRealm {

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. 在token中獲取用戶名
        String principal = (String) authenticationToken.getPrincipal();
        // 2. 查詢數據庫,此處模拟數據庫數據
        if ("xiaochen".equals(principal)) {
            // 參數1:正确的用戶名
            // 參數2:正确的密碼
            // 參數3:提供當前realm的名字
            SimpleAuthenticationInfo simpleAuthenticationInfo
                    = new SimpleAuthenticationInfo("xianchen", "123", this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

3.6 明文加密

實際使用時,我們不可能把用戶密碼以明文形式顯示,需要做加密處理

通常的加密方式是使用 md5 + salt + hash 散列的形式,校驗過程:保存鹽和散列後的值,在 shiro 完成密碼校驗

下面是使用 shiro 提供的 api 完成加密代碼示例

public class TestShiroMD5 {

    public static void main(String[] args) {
        // 1. 使用md5加密
        // 參數1是明文密碼
        Md5Hash md5Hash1 = new Md5Hash("123");
        // 打印加密後的密文
        // 結果:202cb962ac59075b964b07152d234b70
        System.out.println(md5Hash1.toHex());
        // 2. 使用md5+salt加密
        Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps");
        // 8a83592a02263bfe6752b2b5b03a4799
        System.out.println(md5Hash2.toHex());
        // 3. 使用md5+salt+hash散列加密
        Md5Hash md5Hash3 = new Md5Hash("123", "X0*7ps", 1024);
        // e4f9bf3e0c58f045e62c23c533fcf633
        System.out.println(md5Hash3.toHex());
    }
}

自定義 CustomerMd5Realm

public class CustomerMd5Realm extends AuthorizingRealm {

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. 在token中獲取用戶名
        String principal = (String) authenticationToken.getPrincipal();
        // 2. 查詢數據庫,此處模拟數據庫數據
        if ("xiaochen".equals(principal)) {
            // 參數1:正确的用戶名
            // 參數2:正确的密碼
            // 參數3:提供當前realm的名字
            // md5
            // return new SimpleAuthenticationInfo(principal, "202cb962ac59075b964b07152d234b70", this.getName());
            // md5+salt
            /*return new SimpleAuthenticationInfo(principal, "8a83592a02263bfe6752b2b5b03a4799", ByteSource.Util.bytes("X0*7ps"), this.getName());*/
        }
        return null;
    }
}

校驗流程

public class TestCustomerMd5RealmAuthenticator {

    public static void main(String[] args) {
        // 1.創建安全管理器對象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.設置自定義realm
        CustomerMd5Realm realm = new CustomerMd5Realm();
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 使用MD5加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列次數
        hashedCredentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(hashedCredentialsMatcher);
        securityManager.setRealm(realm);
        // 3.給 SecurityUtils 全局安全工具類設置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.獲取認證主體
        Subject subject = SecurityUtils.getSubject();
        // 5.創建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
        // 6.認證
        try {
            System.out.println("認證狀态:" + subject.isAuthenticated());
            subject.login(token);
            System.out.println("認證狀态:" + subject.isAuthenticated());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. shiro 中的授權

4.1 授權

授權,即訪問控制,控制誰能訪問哪些資源。主體進行身份認證後需要分配權限方可訪問系統的資源,對于某些資源沒有權限是無法訪問的

4.2 關鍵對象

授權可簡單理解為 who 對 what(which) 進行 How 操作:

  • Who,即主體(Subject),主體需要訪問系統中的資源
  • What,即資源(Resource),如系統菜單、頁面、按鈕、類方法、系統商品信息等。資源包括資源類型和資源實例,比如商品信息為資源類型,類型為 t01 的商品為資源實例,編号為 001 的商品信息也屬于資源實例
  • How,權限/許可(Permission),規定了主體對資源的操作許可,權限離開資源沒有意義,如用戶查詢權限、用戶添加權限、某個類方法的調用權限、編号為 001 用戶的修改權限等,通過權限可知道主體對哪些資源都有哪些操作許可

4.3 授權方式

基于角色的訪問控制,以角色為中心進行訪問控制

if(subject.hasRole("admin")){
   //操作什麼資源
}

基于資源的訪問控制,以資源為中心進行訪問控制

if(subject.isPermission("user:update:01")){ //資源實例
	//對01用戶進行修改
}
if(subject.isPermission("user:update:*")){  //資源類型
	//對01用戶進行修改
}

4.4 權限字符串

權限字符串的規則是:資源标識符:操作:資源實例标識符,意思是對哪個資源的哪個實例具有什麼操作,: 是資源/操作/實例的分割符,權限字符串也可以使用 * 通配符

例子:

  • 用戶創建權限:user:create,或 user:create:*
  • 用戶修改實例 001 的權限:user:update:001
  • 用戶實例 001 的所有權限:user:*:001

4.5 授權編程實現

在之前 md5 加密的基礎上,實現授權操作

自定義 CustomerMd5Realm

public class CustomerMd5Realm extends AuthorizingRealm {

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 獲取身份信息
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        // 根據身份信息(用戶名)從數據庫獲取當前用戶的角色以及權限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 将數據庫中查詢的角色信息賦值給權限對象
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        // 将數據庫中查詢的權限信息賦值給權限對象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:create:02");
        return simpleAuthorizationInfo;
    }

    ...
}

授權邏輯

public class TestCustomerMd5RealmAuthenticator {

    public static void main(String[] args) {
        
        ...

        // 7. 認證用戶進行授權
        if (subject.isAuthenticated()) {
            // 7.1 基于角色權限控制
            boolean hasRole = subject.hasRole("admin");
            System.out.println("角色校驗:" + hasRole);
            // 7.2 基于多角色權限控制
            boolean hasAllRoles = subject.hasAllRoles(Arrays.asList("admin", "user"));
            System.out.println("多角色校驗:" + hasAllRoles);
            // 7.3 是否具有其中一個角色
            boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
            for (boolean aBoolean : booleans) {
                System.out.println(aBoolean);
            }
            // 7.4 基于權限字符串的訪問控制
            boolean permitted = subject.isPermitted("user:*:01");
            System.out.println("資源權限校驗:" + permitted);
            // 7.5 分布具有哪些資源權限
            boolean[] permitted1 = subject.isPermitted("user:*:01", "order:*:10");
            for (boolean b : permitted1) {
                System.out.println(b);
            }
            // 7.6 同時具有哪些資源權限
            boolean permittedAll = subject.isPermittedAll("user:*:01", "product:*");
            System.out.println("多資源權限校驗:" + permittedAll);
        }
    }
}