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地址