1. 程式人生 > >shiro採坑指南—基礎概念與實戰

shiro採坑指南—基礎概念與實戰

說明

  程式碼及部分相關資料根據慕課網Mark老師的視訊進行整理。

  其他資料:

  • shiro官網

基礎概念

Authenticate/Authentication(認證)

  認證是指檢查使用者身份合法性,通過校驗使用者輸入的密碼是否正確,判斷使用者是否為本人。

  有幾個概念需要理解:

  • Principals (主體標識)
    任何可以唯一地確定一個使用者的屬性都可以充當principal,例如:郵箱、手機號、使用者ID等,這些都是與使用者一一對應的,可以唯一地確定一個使用者。

  • credentials (主體憑證)
    credentials是能確認使用者身份的東西,可以是證書(Certificate),也可以是密碼(password)。

  • token(令牌)
    這裡的token和api裡的token有一點兒差別,這裡token是principal和credential的結合體或者說容器。這裡先講一部分,剩下的放到"Subject"講解。

Authorize/Authorization(授權)

  shiro中的“授權”,更貼切說法是“鑑權”,即判定使用者是否擁有某些許可權,至於擁有該許可權在業務上有何意義,則是由業務本身來決定。

  關於“授權”,shiro引入了兩種概念:

  • Role (角色)
    角色用來區分使用者的類別。角色與使用者間是多對多的關係,一個使用者可以擁有多個角色,如Bob可以同時是admin(管理員)和user(普通使用者)。

  • Permission (許可權)
    許可權是對角色的具體的描述,用於說明角色在業務上的特殊性。如admin(管理員)可以擁有user:delete(刪除使用者)、user:modify(修改使用者資訊)等的許可權。同樣的,角色與許可權是多對多的數量關係。
    shiro許可權可以分級,使用":"分割,如delete、user:delete、user:info:delete。可以使用"*"作萬用字元,例如可以給admin賦予操作使用者的所有許可權,可以配置為"user:*",這樣在授權時,isPermitted("user:123")、isPermitted("user:123:abc")都是返回true;如果配置為"*:*:delete",想要返回true,則需要類似這樣的許可權: isPermitted("123:abc:delete")、isPermitted("hello:321:delete")。

Subject(主體)

  Subject物件用於應用程式與shiro的相關元件進行互動,可以把它看作應用程式中“使用者”的代理,也可以將其視為shiro中的“使用者”。譬如在一個應用中,User物件作為業務上以及程式中的“使用者”,在實現shiro的認證和授權時,並不直接使用User物件與shiro元件進行互動,而是把User物件的資訊(使用者名稱和密碼)交給Subject,Subject呼叫自己的方法,向shiro元件發起身份認證或授權。
  如下是Subject介面提供的方法,包括登入(login)、退出(logout)、認證(isAuthenticated)、授權(checkPermission)等:


  接著上面繼續講Token,在圖片中可以看到,login方法需要傳入一個AuthenticationToken型別引數,這是一個介面,點進去看是醬紫的:

這個介面有兩個方法,分別用來返回principal和credential。由此可見Token就是principal和credential的容器,用於Subject提交“登入”請求時傳遞資訊。
  需要提醒,Subject的這些方法呼叫SecurityManager進行實際上的認證和授權過程。Subject只是業務程式與SecurityManager通訊的門面。

SecurityManager

  顧名思義,SecurityManager是用來manage(管理)的,管理shiro認證授權的流程,管理shiro元件、管理shiro的一些資料、管理Session等等。
  如下是SecurityManager介面的繼承關係:

  SecurityManager繼承了Authorizer(授權)、Authenticator(認證)、和SessionManager(Session管理)。需要注意,此處Session是指Subject與SecurityManager通訊的Session,不能狹隘地理解為WebSession。SecurityManager繼承了了這幾個介面後,又另外提供了login、logout和createSubject方法。

Realm(域)

  與Subject和SecurityManager一樣,Realm是shiro中的三大核心元件之一。Realm相當於DAO,用於獲取與使用者安全相關的資料(使用者密碼、角色、許可權等)。當Subject發起認證和授權時,實際上是呼叫其對應的SecurityManager的認證和授權的方法,而SecurityManager則又是呼叫Authenticator和Authorizer的方法,這兩個類,最後是通過Realm來獲取主體的認證和授權資訊。
  shiro的認證和授權過程如下所示:

使用shiro的基本流程

shiro的使用其實是比較簡單的,只要熟記這幾個步驟,然後在程式碼中實現即可。
1. 建立Realm
  Realm是一個介面,其實現類有SimpleAccountRealm, IniRealm, JdbcRealm等,實際應用中一般需要自定義實現Realm,自定義的Realm通常繼承自抽象類AuthorizingRealm,這是一個比較完善的Realm,提供了認證和授權的方法。
2. 建立SecurityManager並配置環境
  配置SecurityManager環境實際上是配置Realm、CacheManager、SessionManager等元件,最基本的要配置Realm,因為安全資料是通過Realm來獲取。用SecurityManager的setRealm(xxRealm)方法即可給SecurityManager設定Realm。可以為SecurityManager設定多個Realm。
3. 建立Subject
  可以使用SecurityUtils建立Subject。SecurityUtils是一個抽象工具類,其提供了靜態方法getSubject(),用來建立一個與執行緒繫結的Subject。創建出來的Subject用ThreadContext類來儲存,該類也是一個抽象類,它包含一個Map<Object, Object>型別的ThreadLocal靜態變數,該變數儲存該執行緒對應的SecurityManager物件和Subject物件。在SecurityUtils呼叫getSubject方法時,實際上是呼叫SecurityManager的CreateSubject()方法,既然如此,為什麼還要通過SecurityUtils建立Subject?因為SecurityUtils不僅僅建立了Subject還將其與當前執行緒繫結,而且,使用SecurityManager的CreateSubject()方法還要構建一個SubjectContext型別的引數。
4. Subject提交認證和授權
  Subject的login(Token token)方法可以提交“登入”(或者說認證),token就是待驗證的使用者資訊(使用者名稱和密碼等)。登入(認證)成功後,使用Subject的ckeckRole()、checkPermission等方法判斷主體是否擁有某些角色、許可權,以達到授權的目的。再次提醒,Subject不實現實際上的認證和授權過程,而是交給SecurityManager處理。

shiro認證授權示例

  Realm用的是SimpleAccountRealm,SimpleAccountRealm直接把使用者認證資料存到例項中, SecurityManager使用DefaultSecurityManager, 使用SecurityUtils建立Subject, Token用UsernamePasswordToken。 用Junit進行測試。 maven依賴如下:

<!--單元測試-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13-beta-3</version>
</dependency>
<!--shiro核心包-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

shiro認證示例

AuthenticationTest.java:

package com.lifeofcoding.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class AuthenticationTest {
    @Test
    public void testAuthentication(){
        //1.建立Realm並新增資料
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        simpleAccountRealm.addAccount("java","123");

        //2.建立SecurityManager並配置環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        //3.建立subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通過Token提交認證
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);

        //驗證認證情況
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        //退出登入subject.logout();
    }
}

shiro授權示例

  SimpleAccountRealm新增使用者角色和許可權的方法比較簡單,可以自己琢磨。此處的Realm改用IniRealm,iniRealm需要編寫ini檔案儲存使用者的資訊,ini檔案放在resource資料夾下。程式碼如下:
AuthorizationTest.java

package com.lifeofcoding.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
import java.util.ArrayList;

public class AuthorizationTest {
    @Test
    public void testAuthorization() {
        //1.建立Realm並新增資料
        IniRealm iniRealm = new IniRealm("classpath:UserData.ini");

        //2.建立SecurityManager並配置環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(iniRealm);

        //3.建立Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.主體提交認證
        UsernamePasswordToken token = new UsernamePasswordToken("java", "123");
        subject.login(token);

        /*以下為授權的幾種方法*/
        //①.直接判斷是否有某許可權,isPermitted方法返回boolean,不拋異常
        System.out.println("user:login isPermitted: " + subject.isPermitted("user:login"));

        //②.通過角色進行授權,方法有返回值,不拋異常
        subject.hasRole("user");//判斷主體是否有某角色
        subject.hasRoles(new ArrayList<String>() {//返回boolean陣列,陣列順序與引數Roles順序一致,接受List<String>引數
                             {
                                 add("admin");
                                 add("user");
                             }
                         });
        subject.hasAllRoles(new ArrayList<String>() {//返回一個boolean,Subject包含所有Roles時才返回true,接受Collection<String>引數
                               {
                                   add("admin");
                                   add("user");
                               }
                            });

        //③.通過角色授權,與上面大體相同,不過這裡的方法無返回值,授權失敗會丟擲異常,需做好異常處理
        subject.checkRole("user");
        subject.checkRoles("user", "admin");//變參

        //④.通過許可權授權,無返回值,授權失敗丟擲異常
        subject.checkPermission("user:login");
        //ini檔案配置了test角色擁有"prefix:*"許可權,也就是所有以"prefix"開頭的許可權
        subject.checkPermission("prefix:123:456:......");
        //ini檔案配置了test角色擁有"*:*:suffix"許可權,意味著其擁有所有以"suffix"結尾的,一共有三級的許可權
        subject.checkPermission("1:2:suffix");
        subject.checkPermission("abc:123:suffix");
        subject.checkPermissions("user:login", "admin:login");//變參
        //subject.checkPermission(Permission permission); 需要Permission介面的實現類物件作引數
        //subject.checkPermissions(Collection<Permission> permissions);
    }
}

user.ini:

[users]
java = 123,user,admin,test
[roles]
user = user:login,user:modify
admin = user:delete,user:modify,admin:login
test = prefix:*,*:*:suffix
ini檔案的Demo

[main]
# Objects and their properties are defined here,
# Such as the securityManager, Realms and anything
# else needed to build the SecurityManager
# 此處可以用來配置shiro元件,不用編寫程式碼,如:

##--CredentialsMatcher是用來設定加密的--##
hashedCredentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
##--設定加密的演算法--##
hashedCredentialsMatcher.hashAlgorithmName = MD5
##--設定加密次數--##
hashedCredentialsMatcher.hashIterations = 1
##--給Realm配置加密器的Matcher,"$"表引用--##
iniRealm.credentialsMatcher = $hashedCredentialsMatcher
##--配置SecurityManager--##
securityManager = com.xxx.xxxManager
securityManager.realm = $iniRealm

[users]
# The 'users' section is for simple deployments
# when you only need a small number of statically-defined
# set of User accounts.
# 此處是使用者資訊,以及使用者與角色對應關係,格式為 username=password,roleName1,roleName2,roleName3,……
Java=123,user,admin
Go=123
Python=123,user

[roles]
# The 'roles' section is for simple deployments
# when you only need a small number of statically-defined
# roles.
# 角色與許可權對應關係,格式:rolename = permissionDefinition1, permissionDefinition2,……
user=user:delete,user:modify,user:login
admin=user:delete

[urls]
# The 'urls' section is used for url-based security
# in web applications.  We'll discuss this section in the
# Web documentation
#用於配置網頁過濾規則
/some/path = ssl, authc
/another/path = ssl, roles[admin]

下面介紹其他的Realm


JdbcRealm

  JdbcRealm包含預設的資料庫查詢語句,直接使用即可,但要注意建立的表結構要跟查詢語句相對應。當然也可以自己去自定義查詢語句和資料庫。
  mavern依賴:

<!--資料庫相關-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>

預設查詢語句

預設的查詢語句

預設的'users'表結構

預設的'user_roles'表結構

預設的'roles_permissions'表結構

資料庫sql語句

create database shiro;
use shiro;
SET FOREIGN_KEY_CHECKS=0;

DROP TABLE IF EXISTS roles_permissions;
CREATE TABLE roles_permissions (
id bigint(20) NOT NULL AUTO_INCREMENT,
role_name varchar(100) DEFAULT NULL,
permission varchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_roles_permissions (role_name,permission)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO roles_permissions VALUES (null,'admin','user:delete');

DROP TABLE IF EXISTS users;
CREATE TABLE users (
id bigint(20) NOT NULL AUTO_INCREMENT,
username varchar(100) DEFAULT NULL,
password varchar(100) DEFAULT NULL,
password_salt varchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_users_username (username)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO users VALUES ('1', 'java', '123', null);

DROP TABLE IF EXISTS user_roles;
CREATE TABLE user_roles (
id bigint(20) NOT NULL AUTO_INCREMENT,
username varchar(100) DEFAULT NULL,
role_name varchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_user_roles (username,role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO user_roles VALUES (null,'java','admin');


JdbcRealmTest.java:

package com.lifeofcoding.shiro;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class JdbcRealmTest {

    DruidDataSource druidDataSource = new DruidDataSource();
    {
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("0113");
    }

    @Test
    public void testJdbcRealm(){

        //1.建立Realm並新增資料
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(druidDataSource);//配置資料來源
        jdbcRealm.setPermissionsLookupEnabled(true);//設定允許查詢許可權,否則checkPermission拋異常,預設值為false

        //2.建立SecurityManager並配置環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);

        //3.建立subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通過Token提交認證
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登入subject.logout();

        //驗證認證與授權情況
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

自定義查詢語句

自定義的查詢語句

自定義的'test_users'表結構

自定義的'test_user_roles'表結構

自定義的'test_roles_permissions'表結構

資料庫sql語句

DROP TABLE IF EXISTS test_users;
CREATE TABLE test_users (
user_name varchar(20) DEFAULT NULL,
password varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_users VALUES('java','123');

DROP TABLE IF EXISTS test_user_roles;
CREATE TABLE test_user_roles (
user_name varchar(20) DEFAULT NULL,
role varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_user_roles VALUES('java','admin');

DROP TABLE IF EXISTS test_roles_permissions;
CREATE TABLE test_roles_permissions (
role varchar(20) DEFAULT NULL,
permission varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_roles_permissions VALUES('admin','user:delete');

MyJdbcRealmTest.java:

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class MyJdbcRealmTest {

    //從資料庫獲取對應使用者密碼實現認證
    protected static final String AUTHENTICATION_QUERY = "select password from test_users where user_name = ?";
    //從資料庫中獲取對應使用者的所有角色
    protected static final String USER_ROLES_QUERY = "select role from test_user_roles where user_name = ?";
    //從資料庫中獲取角色對應的所有許可權
    protected static final String PERMISSIONS_QUERY = "select permission from test_roles_permissions where role = ?";

    DruidDataSource druidDataSource = new DruidDataSource();
    {
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("0113");
    }

    @Test
    public void testMyJdbcRealm(){

        //1.建立Realm並設定資料庫查詢語句
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(druidDataSource);//配置資料來源
        jdbcRealm.setPermissionsLookupEnabled(true);//設定允許查詢許可權,否則checkPermission拋異常,預設值為false
        jdbcRealm.setAuthenticationQuery(AUTHENTICATION_QUERY);//設定使用者認證資訊查詢語句
        jdbcRealm.setUserRolesQuery(USER_ROLES_QUERY);//設定使用者角色資訊查詢語句
        jdbcRealm.setPermissionsQuery(PERMISSIONS_QUERY);//設定角色許可權資訊查詢語句

        //2.建立SecurityManager並配置環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);

        //3.建立subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通過Token提交認證
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登入subject.logout();

        //驗證認證與授權情況
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

自定義Realm

  自定義Realm,可以繼承抽象類AuthorizingRealm,實現其兩個方法——doGetAuthenticationInfo和doGetAuthorizationInfo,分別用來返回AuthenticationInfo(認證資訊)和AuthorizationInfo(授權資訊)。

如上所示,AuthenticationInfo包含principal和crendential,和token一樣,不同的是,前者是切切實實的在資料庫或其他資料來源中的資料,而後者是使用者輸入的,待校驗的資料。

AuthorizationInfo也是類似的,包含使用者的角色和許可權資訊。
可以用SimpleAuthenticationInfo和SimpleAuthorizationInfo來實現這兩個方法:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.獲取主體中的使用者名稱
    String userName = (String) authenticationToken.getPrincipal();
    //2.通過使用者名稱獲取密碼,getPasswordByName自定義實現
    String password = getPasswordByUserName(userName);
    if(null == password){
        return null;
    }
    //構建AuthenticationInfo返回
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
    return authenticationInfo;
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //1.獲取使用者名稱。principal為Object型別,是使用者唯一標識,可以是使用者名稱,使用者郵箱,資料庫主鍵等,能唯一確定一個使用者的資訊。
    String userName = (String) principalCollection.getPrimaryPrincipal();
    //2.獲取角色資訊,getRoleByUserName自定義
    Set<String> roles = getRolesByUserName(userName);
    //3.獲取許可權資訊,getPermissionsByRole方法同樣自定義,也可以通過使用者名稱查詢許可權資訊
    Set<String> permissions = getPermissionsByUserName(userName);
    //4.構建認證資訊並返回。
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.setStringPermissions(permissions);
    simpleAuthorizationInfo.setRoles(roles);
    return simpleAuthorizationInfo;
}

完整的,包含新增使用者功能的自定義Realm
MyRealm.java:

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyRealm extends AuthorizingRealm {

    /**儲存使用者名稱和密碼*/
    protected final Map<String,String> userMap;
    /**儲存使用者及其對應的角色*/
    protected final Map<String, Set<String>> roleMap;
    /**儲存所有角色以及角色對應的許可權*/
    protected final Map<String,Set<String>> permissionMap;

    {
        //設定Realm名
        super.setName("MyRealm")  ;
    }

    public MyRealm(){
        userMap = new ConcurrentHashMap<>(16);
        roleMap = new ConcurrentHashMap<>(16);
        permissionMap = new ConcurrentHashMap<>(16);
    }

    /**
     * 身份認證必須實現的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.獲取主體中的使用者名稱
        String userName = (String) authenticationToken.getPrincipal();
        //2.通過使用者名稱獲取密碼,getPasswordByName自定義實現
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //構建AuthenticationInfo返回
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        return authenticationInfo;
    }

    /**
     * 用於授權
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.獲取使用者名稱。principal為Object型別,是使用者唯一標識,可以是使用者名稱,使用者郵箱,資料庫主鍵等,能唯一確定一個使用者的資訊。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.獲取角色資訊,getRoleByUserName自定義
        Set<String> roles = getRolesByUserName(userName);
        //3.獲取許可權資訊,getPermissionsByRole方法同樣自定義,也可以通過使用者名稱查詢許可權資訊
        Set<String> permissions = getPermissionsByUserName(userName);
        //4.構建認證資訊並返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 自定義部分,通過使用者名稱獲取許可權資訊
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getPermissionsByUserName(String userName) {
        //1.先通過使用者名稱獲取角色資訊
        Set<String> roles = getRolesByUserName(userName);
        //2.通過角色資訊獲取對應的許可權
        Set<String> permissions = new HashSet<String>();
        //3.新增每個角色對應的所有許可權
        roles.forEach(role -> {
            if(null != permissionMap.get(role)) {
                permissions.addAll(permissionMap.get(role));
            }

        });
        return permissions;
    }

    /**
     * 自定義部分,通過使用者名稱獲取密碼,可改為資料庫操作
     * @param userName 使用者名稱
     * @return java.lang.String
     */
    private String getPasswordByUserName(String userName){
        return userMap.get(userName);
    }

    /**
     * 自定義部分,通過使用者名稱獲取角色資訊,可改為資料庫操作
     * @param userName 使用者名稱
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getRolesByUserName(String userName){
        return roleMap.get(userName);
    }

    /**
     * 往realm新增賬號資訊,變參不傳值會接收到長度為0的陣列。
     */
    public void addAccount(String userName,String password) throws UserExistException{
        addAccount(userName,password,(String[]) null);
    }

    /**
     * 往realm新增賬號資訊
     * @param userName 使用者名稱
     * @param password 密碼
     * @param roles 角色(變參)
     */
    public void addAccount(String userName,String password,String... roles) throws UserExistException{
        if( null != userMap.get(userName) ){
            throw new UserExistException("user \""+ userName +" \" exists");
        }
        userMap.put(userName, password);
        roleMap.put(userName, CollectionUtils.asSet(roles));
    }

    /**
     * 從realm刪除賬號資訊
     * @param userName 使用者名稱
     */
    public void delAccount(String userName){
        userMap.remove(userName);
        roleMap.remove(userName);
    }

    /**
     * 新增角色許可權,變參不傳值會接收到長度為0的陣列。
     * @param roleName 角色名
     */
    public void addPermission(String roleName,String...permissions){
        permissionMap.put(roleName,CollectionUtils.asSet(permissions));
    }

    /**使用者已存在異常*/
    public class UserExistException extends Exception{
        public UserExistException(String message){super(message);}
    }
}

測試程式碼
MyRealmTest.java:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class MyRealmTest {

    @Test
    public void testMyRealm(){

        //1.建立Realm並新增資料
        MyRealm myRealm = new MyRealm();
        try {
            myRealm.addAccount("java", "123", "admin");
        }catch (Exception e){
            e.printStackTrace();
        }
        myRealm.addPermission("admin","user:delete");

        //2.建立SecurityManager並配置環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        //3.建立subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通過Token提交認證
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登入subject.logout();

        //驗證認證與授權情況
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

加密

  使用加密,只需要在Realm返回的AuthenticationInfo新增使用者密碼對應的鹽值:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.獲取主體中的使用者名稱
    String userName = (String) authenticationToken.getPrincipal();
    //2.通過使用者名稱獲取密碼,getPasswordByName自定義實現
    String password = getPasswordByUserName(userName);
    if(null == password){
        return null;
    }
    //3.構建authenticationInfo認證資訊
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
    //設定鹽值
    String salt = getSaltByUserName(userName);
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
    return authenticationInfo;
}

  並且在測試程式碼中給Realm設定加密:

//設定加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");//設定加密演算法
matcher.setHashIterations(3);//設定加密次數
myEncryptedRealm.setCredentialsMatcher(matcher);


  CredentialMatcher用來匹配使用者密碼。shiro通過Realm獲取AuthenticationInfo,AuthenticationInfo裡面包含該使用者的principal、credential、salt。principal就是使用者名稱或手機號或其他,credential就是密碼(加鹽加密後,存到資料來源中的密碼),salt就是密碼對應的“鹽”。shiro獲取到這些資訊之後,利用CredentialMatcher中配置的資訊(加密演算法,加密次數等),對token中使用者輸入的、待校驗的密碼,進行加鹽加密,然後比對結果是否和AuthenticationInfo中的credential一致,若一致,則使用者通過認證。
  “加密”演算法一般用的是hash演算法,hash演算法並不是用來加密的,而是用來生成資訊摘要,該過程是不可逆的,不能從結果逆推得出使用者的密碼的原文。下文這一段話也是相對於hash演算法而言,其他演算法不在考慮範圍內。shiro的CredentialMatcher並沒有“解密”這個概念,因為不能解密,不能把資料庫中“加密”後的密碼還原,只能對使用者輸入的密碼,進行一次相同的“加密”,然後比對資料庫的“加密”後的密碼,從而判斷使用者輸入的密碼是否正確。

  必要地,在存密碼時,要儲存加鹽加密後的密碼:

public void addAccount(String userName,String password,String... roles) throws UserExistException {
    if(null != userMap.get(userName)){
        throw new UserExistException("user \""+ userName +"\" exist");
    }
    //如果配置的加密次數大於0,則進行加密
    if(iterations > 0){
        //使用隨機數作為密碼,可改為UUID或其它
        String salt = String.valueOf(Math.random()*10);
        saltMap.put(userName,salt);
        password = doHash(password, salt);
    }
    userMap.put(userName, password);
    roleMap.put(userName, CollectionUtils.asSet(roles));
}
MyEncryptedRealm.java

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class MyEncryptedRealm extends AuthorizingRealm {

    /** 加密次數 */
    private int iterations;
    /** 演算法名 */
    private String algorithmName;

    /** 儲存使用者名稱和密碼 */
    private final Map userMap;
    /** 儲存使用者及其對應的角色 */
    private final Map> roleMap;
    /** 儲存所有角色以及角色對應的許可權 */
    private final Map> permissionMap;
    /** 儲存使用者鹽值 */
    private Map saltMap;

    {
        //設定Realm名
        super.setName("MyRealm");
    }

    public MyEncryptedRealm(){
        this.iterations = 0;
        this.algorithmName = "MD5";
        this.userMap = new ConcurrentHashMap<>(16);
        this.roleMap = new ConcurrentHashMap<>(16);
        this.permissionMap  = new ConcurrentHashMap<>(16);
        this.saltMap = new ConcurrentHashMap<>(16);
    }

    /**
     * 身份認證必須實現的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.獲取主體中的使用者名稱
        String userName = (String) authenticationToken.getPrincipal();
        //2.通過使用者名稱獲取密碼,getPasswordByName自定義實現
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //3.構建authenticationInfo認證資訊
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        //設定鹽值
        String salt = getSaltByUserName(userName);
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return authenticationInfo;
    }


    /**
     * 用於授權
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.獲取使用者名稱。principal為Object型別,是使用者唯一憑證,可以是使用者名稱,使用者郵箱,資料庫主鍵等,能唯一確定一個使用者的資訊。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.獲取角色資訊,getRoleByUserName自定義
        Set roles = getRolesByUserName(userName);
        //3.獲取許可權資訊,getPermissionsByRole方法同樣自定義,也可以通過使用者名稱查詢許可權資訊
        Set permissions = getPermissionsByUserName(userName);
        //4.構建認證資訊並返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 自定義部分,通過使用者名稱獲取許可權資訊
     * @return java.util.Set
     */
    private Set getPermissionsByUserName(String userName) {
        //1.先通過使用者名稱獲取角色資訊
        Set roles = getRolesByUserName(userName);
        //2.通過角色資訊獲取對應的許可權
        Set permissions = new HashSet<>();
        //3.新增每個角色對應的所有許可權
        roles.forEach(role -> {
            if (null != permissionMap.get(role)) {
                permissions.addAll(permissionMap.get(role));
            }
        });
        return permissions;
    }

    /**
     * 自定義部分,通過使用者名稱獲取密碼,可改為資料庫操作
     * @return java.lang.String
     */
    private String getPasswordByUserName(String userName){
        return userMap.get(userName);
    }

    /**
     * 自定義部分,通過使用者名稱獲取角色資訊,可改為資料庫操作
     * @return java.util.Set
     */
    private Set getRolesByUserName(String userName){
        return roleMap.get(userName);
    }

    /**
     * 自定義部分,通過使用者名稱獲取鹽值,可改為資料庫操作
     * @return java.util.Set
     */
    private String getSaltByUserName(String userName) {
        return saltMap.get(userName);
    }

    /**
     * 往realm新增賬號資訊,變參不傳值會接收到長度為0的陣列。
     */
    public void addAccount(String userName,String password) throws UserExistException {
        addAccount(userName,password,(String[]) null);
    }

    /**
     * 往realm新增賬號資訊
     */
    public void addAccount(String userName,String password,String... roles) throws UserExistException {
        if(null != userMap.get(userName)){
            throw new UserExistException("user \""+ userName +"\" exist");
        }
        //如果配置的加密次數大於0,則進行加密
        if(iterations > 0){
            String salt = String.valueOf(Math.random()*10);
            saltMap.put(userName,salt);
            password = doHash(password, salt);
        }
        userMap.put(userName, password);
        roleMap.put(userName, CollectionUtils.asSet(roles));
    }

    /**
     * 從realm刪除賬號資訊
     * @param userName 使用者名稱
     */
    public void deleteAccount(String userName){
        userMap.remove(userName);
        roleMap.remove(userName);
    }


    /**
     * 新增角色許可權,變參不傳值會接收到長度為0的陣列。
     * @param roleName 角色名
     */
    public void addPermission(String roleName,String...permissions){
        permissionMap.put(roleName, CollectionUtils.asSet(permissions));
    }

    /**
     * 設定加密次數
     * @param iterations 加密次數
     */
    public void setHashIterations(int iterations){
        this.iterations = iterations;
    }

    /**
     * 設定演算法名
     * @param algorithmName 演算法名
     */
    public void setAlgorithmName(String algorithmName){
        this.algorithmName = algorithmName;
    }

    /**
     * 計算雜湊值
     * @param str 要進行"加密"的字串
     * @param salt 鹽
     * @return String
     */
    private String doHash(String str,String salt){
        salt = null==salt ? "" : salt;
        return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString();
    }


    /**
     * 註冊時,使用者已存在的異常
     */
    public class UserExistException extends Exception{
        public UserExistException(String message) {super(message);}
    }

}
MyEncryptedRealmTest.java
 
package com.lifeofcoding.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

/**
 * @Classname MyEncryptionRealmTest
 * @Description TODO
 * @Date 2019-11-2019-11-20-17:41
 * @Created by yo
 */
public class MyEncryptedRealmTest {

    private MyEncryptedRealm myEncryptedRealm;

    @Test
    public void testShiroEncryption() {
        //1.建立Realm並新增資料
        MyEncryptedRealm myEncryptedRealm = new MyEncryptedRealm();
        myEncryptedRealm.setHashIterations(3);
        myEncryptedRealm.setAlgorithmName("MD5");
        try {
            myEncryptedRealm.addAccount("java", "123456", "admin");
        }catch (Exception e){
            e.printStackTrace();
        }
        myEncryptedRealm.addPermission("admin","user:create","user:delete");

        //2.建立SecurityManager物件
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(myEncryptedRealm);

        //3.設定加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");//設定加密演算法
        matcher.setHashIterations(3);//設定加密次數
        myEncryptedRealm.setCredentialsMatcher(matcher);

        //4.建立主體並提交認證
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken("java","123456");
        subject.login(token);
        System.out.println(subject.getPrincipal()+" isAuthenticated: "+subject.isAuthenticated());
        subject.checkRole("admin");
        subject.checkPermission("user:delete");
    }
}

檔案傳送門

github地址