說明
其他資料:
基礎概念
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🔤delete")、isPermitted("hello:321:delete")。
Subject(主體)
Subject對象用於應用程序與shiro的相關組件進行交互,可以把它看作應用程序中“用戶”的代理,也可以將其視為shiro中的“用戶”。譬如在一個應用中,User對象作為業務上以及程序中的“用戶”,在實現shiro的認證和授權時,並不直接使用User對象與shiro組件進行交互,而是把User對象的信息(用戶名和密碼)交給Subject,Subject調用自己的方法,向shiro組件發起身份認證或授權。
如下是Subject接口提供的方法,包括登錄(login)、退出(logout)、認證(isAuthenticated)、授權(checkPermission)等:
SecurityManager
顧名思義,SecurityManager是用來manage(管理)的,管理shiro認證授權的流程,管理shiro組件、管理shiro的一些數據、管理Session等等。
如下是SecurityManager接口的繼承關系:
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>
默認查詢語句
默認的查詢語句
數據庫sql語句
create database shiro; use shiro; SET FOREIGN_KEY_CHECKS=0;DROP TABLE IF EXISTS
roles_permissions;
CREATE TABLEroles_permissions(
idbigint(20) NOT NULL AUTO_INCREMENT,
role_namevarchar(100) DEFAULT NULL,
permissionvarchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEYidx_roles_permissions(role_name,permission)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO
roles_permissionsVALUES (null,'admin','user:delete');DROP TABLE IF EXISTS
users;
CREATE TABLEusers(
idbigint(20) NOT NULL AUTO_INCREMENT,
usernamevarchar(100) DEFAULT NULL,
passwordvarchar(100) DEFAULT NULL,
password_saltvarchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEYidx_users_username(username)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;INSERT INTO
usersVALUES ('1', 'java', '123', null);DROP TABLE IF EXISTS
user_roles;
CREATE TABLEuser_roles(
idbigint(20) NOT NULL AUTO_INCREMENT,
usernamevarchar(100) DEFAULT NULL,
role_namevarchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEYidx_user_roles(username,role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTOuser_rolesVALUES (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_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(授權信息)。
@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<String,String> userMap; /** 存儲用戶及其對應的角色 */ private final Map<String, Set<String>> roleMap; /** 存儲所有角色以及角色對應的權限 */ private final Map<String,Set<String>> permissionMap; /** 存儲用戶鹽值 */ private Map<String,String> 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<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<>(); //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<java.lang.String> */ private Set<String> getRolesByUserName(String userName){ return roleMap.get(userName); } /** * 自定義部分,通過用戶名獲取鹽值,可改為數據庫操作 * @return java.util.Set<java.lang.String> */ 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");}
}
