說明:本文很多觀點和內容來自互聯網以及各種資料,如果侵犯了您的權益,請及時聯系我,我會刪除相關內容。
權限管理
基本上涉及到用戶參與的系統都要進行權限管理,權限管理屬于系統安全的范疇,權限管理實現對用戶訪問系統的控制,按照安全規則或者安全策略控制用戶可以訪問而且只能訪問自己被授權的資源。
權限管理包括用戶身份認證和授權兩部分,簡稱認證授權。對于需要訪問控制的資源用戶首先經過身份認證,認證通過后用戶具有該資源的訪問權限方可訪問。
- 用戶身份認證
身份認證,就是判斷一個用戶是否為合法用戶的處理過程。最常用的簡單身份認證方式是核對用戶輸入的用戶名和口令,來判斷用戶身份是否正確。對于采用指紋等系統,則出示指紋;對于硬件Key等刷卡系統,則需要刷卡。 -
用戶名密碼身份認證流程
用戶名密碼身份認證流程.png - 關鍵對象
上邊的流程圖中需要理解以下關鍵對象:
- Subject:主體
訪問系統的用戶,主體可以是用戶、程序等,進行認證的都稱為主體; - Principal:身份信息
是主體(subject)進行身份認證的標識,標識必須具有唯一性,如用戶名、手機號、郵箱地址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)。 - credential:憑證信息
只有主體自己知道的安全信息,如密碼、證書等。
-
授權
概念
授權,即訪問控制,控制誰能訪問哪些資源。主體進行身份認證后需要分配權限方可訪問系統的資源,對于某些資源沒有權限是無法訪問的。
授權流程:
下圖中橙色為授權流程
授權流程.png -
關鍵對象
授權可簡單理解為who對what(which)進行How操作:
-----Who,即主體(Subject),主體需要訪問系統中的資源。
-----What,即資源(Resource),如系統菜單、頁面、按鈕、類方法、系統商品信息等。資源包括資源類型和資源實例,比如商品信息為資源類型,類型為c01的商品為資源實例,編號為001的商品信息也屬于資源實例。
------How,權限/許可(Permission),規定了主體對資源的操作許可,權限離開資源沒有意義,如用戶查詢權限、用戶添加權限、某個類方法的調用權限、編號為001用戶的修改權限等,通過權限可知主體對哪些資源都有哪些操作許可。
權限分為粗顆粒和細顆粒,粗顆粒權限是指對資源類型的權限,細顆粒權限是對資源實例的權限。
主體、資源、權限關系如下圖:
主體、資源、權限關系圖.png
權限模型
對上節中的主體、資源、權限通過數據模型表示。 - 主體(賬號、密碼)
- 資源(資源名稱、訪問地址)
- 權限(權限名稱、資源id)
- 角色(角色名稱)
- 角色和權限關系(角色id、權限id)
- 主體和角色關系(主體id、角色id)
權限模型.png
通常企業開發中將資源和權限表合并為一張權限表,如下:
資源(資源名稱、訪問地址)
權限(權限名稱、資源id)
合并為:
權限(權限名稱、資源名稱、資源訪問地址) - 權限分配
對主體分配權限,主體只允許在權限范圍內對資源進行操作,比如:對u01用戶分配商品修改權限,u01用戶只能對商品進行修改。
權限分配的數據通常需要持久化,根據上邊的數據模型創建表并將用戶的權限信息存儲在數據庫中。 - 權限控制
用戶擁有了權限即可操作權限范圍內的資源,系統不知道主體是否具有訪問權限需要對用戶的訪問進行控制 -
基于角色的訪問控制
RBAC基于角色的訪問控制(Role-Based Access Control)是以角色為中心進行訪問控制,比如:主體的角色為總經理可以查詢企業運營報表,查詢員工工資信息等,訪問控制流程如下:
基于角色的訪問控制.png
上圖中的判斷邏輯代碼可以理解為:
if(主體.hasRole("總經理角色id")){
查詢工資
}
缺點:以角色進行訪問控制粒度較粗,如果上圖中查詢工資所需要的角色變化為總經理和部門經理,此時就需要修改判斷邏輯為“判斷主體的角色是否是總經理或部門經理”,系統可擴展性差。
修改代碼如下:
if(主體.hasRole("總經理角色id") || 主體.hasRole("部門經理角色id")){
查詢工資
}
-
基于資源的訪問控制
RBAC基于資源的訪問控制(Resource-Based Access Control)是以資源為中心進行訪問控制,比如:主體必須具有查詢工資權限才可以查詢員工工資信息等,訪問控制流程如下:
基于資源的訪問控制.png
上圖中的判斷邏輯代碼可以理解為:
if(主體.hasPermission("wage:query")){
查詢工資
}
優點:系統設計時定義好查詢工資的權限標識,即使查詢工資所需要的角色變化為總經理和部門經理也只需要將“查詢工資信息權限”添加到“部門經理角色”的權限列表中,判斷邏輯不用修改,系統可擴展性強。
權限管理解決方案
- 什么是粗顆粒度和細顆粒度
對資源類型的管理稱為粗顆粒度權限管理,即只控制到菜單、按鈕、方法,粗粒度的例子比如:用戶具有用戶管理的權限,具有導出訂單明細的權限。對資源實例的控制稱為細顆粒度權限管理,即控制到數據級別的權限,比如:用戶只允許修改本部門的員工信息,用戶只允許導出自己創建的訂單明細。 - 如何實現粗顆粒度和細顆粒度
對于粗顆粒度的權限管理可以很容易做系統架構級別的功能,即系統功能操作使用統一的粗顆粒度的權限管理。
對于細顆粒度的權限管理不建議做成系統架構級別的功能,因為對數據級別的控制是系統的業務需求,隨著業務需求的變更業務功能變化的可能性很大,建議對數據級別的權限控制在業務層個性化開發,比如:用戶只允許修改自己創建的商品信息可以在service接口添加校驗實現,service接口需要傳入當前操作人的標識,與商品信息創建人標識對比,不一致則不允許修改商品信息。
-
基于url攔截
基于url攔截是企業中常用的權限管理方法,實現思路是:將系統操作的每個url配置在權限表中,將權限對應到角色,將角色分配給用戶,用戶訪問系統功能通過Filter進行過慮,過慮器獲取到用戶訪問的url,只要訪問的url是用戶分配角色中的url則放行繼續訪問。
權限管理解決方案.png
shiro介紹
- 什么是shiro
Shiro是apache旗下一個開源框架,它將軟件系統的安全認證相關的功能抽取出來,實現用戶身份認證,權限授權、加密、會話管理等功能,組成了一個通用的安全認證框架。 - 為什么要學shiro
使用shiro就可以非常快速的完成認證、授權等功能的開發,降低系統成本。
shiro使用廣泛,shiro可以運行在web應用,非web應用,集群分布式應用中越來越多的用戶開始使用shiro。
java領域中spring security(原名Acegi)也是一個開源的權限管理框架,但是spring security依賴spring運行,而shiro就相對獨立,最主要是因為shiro使用簡單、靈活,所以現在越來越多的用戶選擇shiro。 -
Shiro架構
shiro框架功能模塊.png
shiro架構.png
- Subject
Subject即主體,外部應用與subject進行交互,subject記錄了當前操作用戶,將用戶的概念理解為當前操作的主體,可能是一個通過瀏覽器請求的用戶,也可能是一個運行的程序。 Subject在shiro中是一個接口,接口中定義了很多認證授相關的方法,外部程序通過subject進行認證授,而subject是通過SecurityManager安全管理器進行認證授權 - SecurityManager
SecurityManager即安全管理器,對全部的subject進行安全管理,它是shiro的核心,負責對所有的subject進行安全管理。通過SecurityManager可以完成subject的認證、授權等,實質上SecurityManager是通過Authenticator進行認證,通過Authorizer進行授權,通過SessionManager進行會話管理等。
SecurityManager是一個接口,繼承了Authenticator, Authorizer, SessionManager這三個接口。 - Authenticator
Authenticator即認證器,對用戶身份進行認證,Authenticator是一個接口,shiro提供ModularRealmAuthenticator實現類,通過ModularRealmAuthenticator基本上可以滿足大多數需求,也可以自定義認證器。 - Authorizer
Authorizer即授權器,用戶通過認證器認證通過,在訪問功能時需要通過授權器判斷用戶是否有此功能的操作權限。 - realm
Realm即領域,相當于datasource數據源,securityManager進行安全認證需要通過Realm獲取用戶權限數據,比如:如果用戶身份數據在數據庫那么realm就需要從數據庫獲取用戶身份信息。
注意:不要把realm理解成只是從數據源取數據,在realm中還有認證授權校驗的相關的代碼。 - sessionManager
sessionManager即會話管理,shiro框架定義了一套會話管理,它不依賴web容器的session,所以shiro可以使用在非web應用上,也可以將分布式應用的會話集中在一點管理,此特性可使它實現單點登錄。 - SessionDAO
SessionDAO即會話dao,是對session會話操作的一套接口,比如要將session存儲到數據庫,可以通過jdbc將會話存儲到數據庫。 - CacheManager
CacheManager即緩存管理,將用戶權限數據存儲在緩存,這樣可以提高性能。 - Cryptography
Cryptography即密碼管理,shiro提供了一套加密/解密的組件,方便開發。比如提供常用的散列、加/解密等功能。
-
shiro認證
認證流程.png
入門程序(用戶登陸和退出)
shiro.ini----->通過Shiro.ini配置文件初始化SecurityManager環境。
[users]
zhang=123
lisi=123
認證代碼
// 用戶登陸、用戶退出
@Test
public void testLoginLogout() {
// 構建SecurityManager工廠,IniSecurityManagerFactory可以從ini文件中初始化SecurityManager環境
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro.ini");
// 通過工廠創建SecurityManager
SecurityManager securityManager = factory.getInstance();
// 將securityManager設置到運行環境中
SecurityUtils.setSecurityManager(securityManager);
// 創建一個Subject實例,該實例認證要使用上邊創建的securityManager進行
Subject subject = SecurityUtils.getSubject();
// 創建token令牌,記錄用戶認證的身份和憑證即賬號和密碼
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
// 用戶登陸
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 用戶認證狀態
Boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用戶認證狀態:" + isAuthenticated);
// 用戶退出
subject.logout();
isAuthenticated = subject.isAuthenticated();
System.out.println("用戶認證狀態:" + isAuthenticated);
}
認證執行流程
1、 創建token令牌,token中有用戶提交的認證信息即賬號和密碼
2、 執行subject.login(token),最終由securityManager通過Authenticator進行認證
3、 Authenticator的實現ModularRealmAuthenticator調用realm從ini配置文件取用戶真實的賬號和密碼,這里使用的是IniRealm(shiro自帶)
4、 IniRealm先根據token中的賬號去ini中找該賬號,如果找不到則給ModularRealmAuthenticator返回null,如果找到則匹配密碼,匹配密碼成功則認證通過。
- 常見的異常
- UnknownAccountException
賬號不存在異常如下:
org.apache.shiro.authc.UnknownAccountException: No account found for user。。。。 - IncorrectCredentialsException
當輸入密碼錯誤會拋此異常,如下:
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhangsan, rememberMe=false] did not match the expected credentials. - 更多如下:
DisabledAccountException(帳號被禁用)
LockedAccountException(帳號被鎖定)
ExcessiveAttemptsException(登錄失敗次數過多)
ExpiredCredentialsException(憑證過期)等
自定義Realm
上邊的程序使用的是Shiro自帶的IniRealm,IniRealm從ini配置文件中讀取用戶的信息,大部分情況下需要從系統的數據庫中讀取用戶信息,所以需要自定義realm。
shiro提供的realm
最基礎的是Realm接口,CachingRealm負責緩存處理,AuthenticationRealm負責認證,AuthorizingRealm負責授權,通常自定義的realm繼承AuthorizingRealm。
- 自定義Realm
public class CustomRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "customRealm1";
}
//支持UsernamePasswordToken
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//從token中 獲取用戶身份信息
String username = (String) token.getPrincipal();
//拿username從數據庫中查詢
//....
//如果查詢不到則返回null
if(!username.equals("zhang")){//這里模擬查詢不到
return null;
}
//獲取從數據庫查詢出來的用戶密碼
String password = "123";//這里使用靜態數據模擬。。
//返回認證信息由父類AuthenticatingRealm進行認證
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, getName());
return simpleAuthenticationInfo;
}
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
shiro-realm.ini
[main]
#自定義 realm
customRealm=cn.itcast.shiro.authentication.realm.CustomRealm1
#將realm設置到securityManager
securityManager.realms=$customRealm
散列算法
散列算法一般用于生成一段文本的摘要信息,散列算法不可逆,將內容可以生成摘要,無法將摘要轉成原始內容。散列算法常用于對密碼進行散列,常用的散列算法有MD5、SHA。
一般散列算法需要提供一個salt(鹽)與原始內容生成摘要信息,這樣做的目的是為了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿著“96e79218965eb72c92a549dd5a330112”去md5破解網站很容易進行破解,如果要是對111111和salt(鹽,一個隨機數)進行散列,這樣雖然密碼都是111111加不同的鹽會生成不同的散列值。
例子
//md5加密,不加鹽
String password_md5 = new Md5Hash("111111").toString();
System.out.println("md5加密,不加鹽="+password_md5);
//md5加密,加鹽,一次散列
String password_md5_sale_1 = new Md5Hash("111111", "eteokues", 1).toString();
System.out.println("password_md5_sale_1="+password_md5_sale_1);
String password_md5_sale_2 = new Md5Hash("111111", "uiwueylm", 1).toString();
System.out.println("password_md5_sale_2="+password_md5_sale_2);
//兩次散列相當于md5(md5())
//使用SimpleHash
String simpleHash = new SimpleHash("MD5", "111111", "eteokues",1).toString();
System.out.println(simpleHash);
在realm中使用
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//用戶賬號
String username = (String) token.getPrincipal();
//根據用戶賬號從數據庫取出鹽和加密后的值
//..這里使用靜態數據
//如果根據賬號沒有找到用戶信息則返回null,shiro拋出異常“賬號不存在”
//按照固定規則加密碼結果 ,此密碼 要在數據庫存儲,原始密碼 是111111,鹽是eteokues
String password = "cb571f7bd7a6f73ab004a70322b963d5";
//鹽,隨機數,此隨機數也在數據庫存儲
String salt = "eteokues";
//返回認證信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, ByteSource.Util.bytes(salt),getName());
return simpleAuthenticationInfo;
}
shiro-cryptography.ini
[main]
#定義憑證匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次數
credentialsMatcher.hashIterations=1
#將憑證匹配器設置到realm
customRealm=cn.itcast.shiro.authentication.realm.CustomRealm2
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm
shiro授權
Shiro 支持三種方式的授權:
- ν編程式:通過寫if/else 授權代碼塊完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有權限
} else {
//無權限
}
- 注解式:通過在執行的Java方法上放置相應的注解完成:
@RequiresRoles("admin")
public void hello() {
//有權限
}
- shiro 標簽:在JSP/GSP 頁面通過相應的標簽完成:
<shiro:hasRole name="admin">
擁有admin角色才能看到我!
</shiro:hasRole>
授權測試------》創建存放權限的配置文件shiro-permission.ini,如下:
[users]
#用戶zhang的密碼是123,此用戶具有role1和role2兩個角色
zhang=123,role1,role2
wang=123,role2
[roles]
#角色role1對資源user擁有create、update權限
role1=user:create,user:update
#角色role2對資源user擁有create、delete權限
role2=user:create,user:delete
#角色role3對資源user擁有create權限
role3=user:create
在ini文件中用戶、角色、權限的配置規則是:“用戶名=密碼,角色1,角色2…” “角色=權限1,權限2…”,首先根據用戶名找角色,再根據角色找權限,角色是權限集合。
權限字符串規則
權限字符串的規則是:“資源標識符:操作:資源實例標識符”,意思是對哪個資源的哪個實例具有什么操作,“:”是資源/操作/實例的分割符,權限字符串也可以使用*通配符。
例子:
用戶創建權限:user:create,或user:create:*
用戶修改實例001的權限:user:update:001
用戶實例001的所有權限:user:*:001
測試代碼
測試代碼同認證代碼,注意ini地址改為shiro-permission.ini,主要學習下邊授權的方法,注意:在用戶認證通過后執行下邊的授權代碼。
@Test
public void testPermission() {
// 從ini文件中創建SecurityManager工廠
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-permission.ini");
// 創建SecurityManager
SecurityManager securityManager = factory.getInstance();
// 將securityManager設置到運行環境
SecurityUtils.setSecurityManager(securityManager);
// 創建主體對象
Subject subject = SecurityUtils.getSubject();
// 對主體對象進行認證
// 用戶登陸
// 設置用戶認證的身份(principals)和憑證(credentials)
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 用戶認證狀態
Boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用戶認證狀態:" + isAuthenticated);
// 用戶授權檢測 基于角色授權
// 是否有某一個角色
System.out.println("用戶是否擁有一個角色:" + subject.hasRole("role1"));
// 是否有多個角色
System.out.println("用戶是否擁有多個角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));
// subject.checkRole("role1");
// subject.checkRoles(Arrays.asList("role1", "role2"));
// 授權檢測,失敗則拋出異常
// subject.checkRole("role22");
// 基于資源授權
System.out.println("是否擁有某一個權限:" + subject.isPermitted("user:delete"));
System.out.println("是否擁有多個權限:" + subject.isPermittedAll("user:create:1", "user:delete"));
//檢查權限
subject.checkPermission("sys:user:delete");
subject.checkPermissions("user:create:1","user:delete");
}
基于角色的授權
// 用戶授權檢測 基于角色授權
// 是否有某一個角色
System.out.println("用戶是否擁有一個角色:" + subject.hasRole("role1"));
// 是否有多個角色
System.out.println("用戶是否擁有多個角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));
對應的check方法:
subject.checkRole("role1");
subject.checkRoles(Arrays.asList("role1", "role2"));
上邊check方法如果授權失敗則拋出異常:
org.apache.shiro.authz.UnauthorizedException: Subject does not have role […..]
基于資源授權
// 基于資源授權
System.out.println("是否擁有某一個權限:" + subject.isPermitted("user:delete"));
System.out.println("是否擁有多個權限:" + subject.isPermittedAll("user:create:1", "user:delete"));
對應的check方法:
subject.checkPermission("sys:user:delete");
subject.checkPermissions("user:create:1","user:delete");
上邊check方法如果授權失敗則拋出異常:
org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [….]
自定義realm
與上邊認證自定義realm一樣,大部分情況是要從數據庫獲取權限數據,這里直接實現基于資源的授權。
realm代碼
在認證章節寫的自定義realm類中完善doGetAuthorizationInfo方法,此方法需要完成:根據用戶身份信息從數據庫查詢權限字符串,由shiro進行授權
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// 獲取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根據身份信息從數據庫中查詢權限數據
//....這里使用靜態數據模擬
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");
permissions.add("user.delete");
//將權限信息封閉為AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(String permission:permissions){
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
shiro-realm.ini
ini配置文件還使用認證階段使用的,不用改變。
測試代碼
同上邊的授權測試代碼,注意修改ini地址為shiro-realm.ini。
授權執行流程
1、 執行subject.isPermitted(“user:create”)
2、 securityManager通過ModularRealmAuthorizer進行授權
3、 ModularRealmAuthorizer調用realm獲取權限信息
4、 ModularRealmAuthorizer再通過permissionResolver解析權限字符串,校驗是否匹配
shiro與項目集成開發
- shiro與spring web項目整合
shiro與springweb項目整合在“基于url攔截實現的工程”基礎上整合,基于url攔截實現的工程的技術架構是springmvc+mybatis,整合注意兩點:shiro與springweb項目整合在“基于url攔截實現的工程”基礎上整合,基于url攔截實現的工程的技術架構是springmvc+mybatis,整合注意兩點:
- shiro與spring整合
- 加入shiro對web應用的支持
取消原springmvc認證和授權攔截器
去掉springmvc.xml中配置的LoginInterceptor和PermissionInterceptor攔截器。
web.xml添加shiro Filter
<!--shiro-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<!-- 該值缺省為false,表示生命周期由SpringApplicationContext管理,設置為true則表示由ServletContainer管理 -->
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
applicationContext-shiro.xml
<!-- Shiro 的Web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 如果沒有認證將要跳轉的登陸地址,http可訪問的url,如果不在表單認證過慮器FormAuthenticationFilter中指定此地址就為身份認證地址 -->
<property name="loginUrl" value="/login.action" />
<!-- 沒有權限跳轉的地址 -->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- shiro攔截器配置 -->
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property>
<property name="filterChainDefinitions">
<value>
<!-- 必須通過身份認證方可訪問,身份認 證的url必須和過慮器中指定的loginUrl一致 -->
/loginsubmit.action = authc
<!-- 退出攔截,請求logout.action執行退出操作 -->
/logout.action = logout
<!-- 無權訪問頁面 -->
/refuse.jsp = anon
<!-- roles[XX]表示有XX角色才可訪問 -->
/item/list.action = roles[item],authc
/js/** anon
/images/** anon
/styles/** anon
<!-- user表示身份認證通過或通過記住我認證通過的可以訪問 -->
/** = user
<!-- /**放在最下邊,如果一個url有多個過慮器則多個過慮器中間用逗號分隔,如:/** = user,roles[admin] -->
</value>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<!-- 自定義 realm -->
<bean id="userRealm" class="cn.itcast.ssm.realm.CustomRealm1">
</bean>
<!-- 基于Form表單的身份驗證過濾器,不配置將也會注冊此過慮器,表單中的用戶賬號、密碼及loginurl將采用默認值,建議配置 -->
<bean id="formAuthenticationFilter"
class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
<!-- 表單中賬號的input名稱 -->
<property name="usernameParam" value="usercode" />
<!-- 表單中密碼的input名稱 -->
<property name="passwordParam" value="password" />
<!-- <property name="rememberMeParam" value="rememberMe"/> -->
<!-- loginurl:用戶登陸地址,此地址是可以http訪問的url地址 -->
<property name="loginUrl" value="/loginsubmit.action" />
</bean>
使用shiro注解授權
在springmvc.xml中配置shiro注解支持,可在controller方法中使用shiro注解配置權限:
<!-- 開啟aop,對類代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 開啟shiro注解支持 -->
<bean
class="
org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
修改Controller代碼,在方法上添加授權注解,如下:
// 查詢列表
@RequestMapping("/queryItem")
@RequiresPermissions("item:query")
public ModelAndView queryItem() throws Exception {
上邊代碼@RequiresPermissions("item:query")表示必須擁有“item:query”權限方可執行。
- 自定義realm
此realm先不從數據庫查詢權限數據,當前需要先將shiro整合完成,在上邊章節定義的realm基礎上修改。
public class CustomRealm1 extends AuthorizingRealm {
@Autowired
private SysService sysService;
@Override
public String getName() {
return "customRealm";
}
// 支持什么類型的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// 從token中 獲取用戶身份信息
String username = (String) token.getPrincipal();
// 拿username從數據庫中查詢
// ....
// 如果查詢不到則返回null
if (!username.equals("zhang")) {// 這里模擬查詢不到
return null;
}
// 獲取從數據庫查詢出來的用戶密碼
String password = "123";// 這里使用靜態數據模擬。。
// 根據用戶id從數據庫取出菜單
//...先用靜態數據
List<SysPermission> menus = new ArrayList<SysPermission>();;
SysPermission sysPermission_1 = new SysPermission();
sysPermission_1.setName("商品管理");
sysPermission_1.setUrl("/item/queryItem.action");
SysPermission sysPermission_2 = new SysPermission();
sysPermission_2.setName("用戶管理");
sysPermission_2.setUrl("/user/query.action");
menus.add(sysPermission_1);
menus.add(sysPermission_2);
// 構建用戶身體份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(username);
activeUser.setUsername(username);
activeUser.setUsercode(username);
activeUser.setMenus(menus);
// 返回認證信息由父類AuthenticatingRealm進行認證
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
activeUser, password, getName());
return simpleAuthenticationInfo;
}
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// 獲取身份信息
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//用戶id
String userid = activeUser.getUserid();
// 根據用戶id從數據庫中查詢權限數據
// ....這里使用靜態數據模擬
List<String> permissions = new ArrayList<String>();
permissions.add("item:query");
permissions.add("item:update");
// 將權限信息封閉為AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
}
登錄
//用戶登陸頁面
@RequestMapping("/login")
public String login()throws Exception{
return "login";
}
// 用戶登陸提交
@RequestMapping("/loginsubmit")
public String loginsubmit(Model model, HttpServletRequest request)
throws Exception {
// shiro在認證過程中出現錯誤后將異常類路徑通過request返回
String exceptionClassName = (String) request
.getAttribute("shiroLoginFailure");
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
throw new CustomException("賬號不存在");
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
throw new CustomException("用戶名/密碼錯誤");
} else{
throw new Exception();//最終在異常處理器生成未知錯誤
}
}
首頁
由于session由shiro管理,需要修改首頁的controller方法:
//系統首頁
@RequestMapping("/first")
public String first(Model model)throws Exception{
//主體
Subject subject = SecurityUtils.getSubject();
//身份
ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
model.addAttribute("activeUser", activeUser);
return "/first";
}
退出
由于使用shiro的sessionManager,不用開發退出功能,使用shiro的logout攔截器即可。
<!-- 退出攔截,請求logout.action執行退出操作 -->
/logout.action = logout
無權限refuse.jsp
當用戶無操作權限,shiro將跳轉到refuse.jsp頁面。
參考:applicationContext-shiro.xml
- realm連接數據庫
- 添加憑證匹配器
- 添加憑證匹配器實現md5加密校驗。
修改applicationContext-shiro.xml:
<!-- 憑證匹配器 -->
<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="1" />
</bean>
<!-- 自定義 realm -->
<bean id="userRealm" class="cn.itcast.ssm.realm.CustomRealm1">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
realm代碼
修改realm代碼從數據庫中查詢用戶身份信息和權限信息,將sysService注入realm。
public class CustomRealm1 extends AuthorizingRealm {
@Autowired
private SysService sysService;
@Override
public String getName() {
return "customRealm";
}
// 支持什么類型的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// 從token中獲取用戶身份
String usercode = (String) token.getPrincipal();
SysUser sysUser = null;
try {
sysUser = sysService.findSysuserByUsercode(usercode);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 如果賬號不存在
if (sysUser == null) {
throw new UnknownAccountException("賬號找不到");
}
// 根據用戶id取出菜單
List<SysPermission> menus = null;
try {
menus = sysService.findMenuList(sysUser.getId());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 用戶密碼
String password = sysUser.getPassword();
//鹽
String salt = sysUser.getSalt();
// 構建用戶身體份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(sysUser.getId());
activeUser.setUsername(sysUser.getUsername());
activeUser.setUsercode(sysUser.getUsercode());
activeUser.setMenus(menus);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
activeUser, password, ByteSource.Util.bytes(salt),getName());
return simpleAuthenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//身份信息
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//用戶id
String userid = activeUser.getUserid();
//獲取用戶權限
List<SysPermission> permissions = null;
try {
permissions = sysService.findSysPermissionList(userid);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//構建shiro授權信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(SysPermission sysPermission:permissions){
simpleAuthorizationInfo.addStringPermission(sysPermission.getPercode());
}
return simpleAuthorizationInfo;
}
}
緩存
shiro每個授權都會通過realm獲取權限信息,為了提高訪問速度需要添加緩存,第一次從realm中讀取權限數據,之后不再讀取,這里Shiro和Ehcache整合。
- 添加Ehcache的依賴
在applicationContext-shiro.xml中配置緩存管理器。
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
</bean>
- session管理
在applicationContext-shiro.xml中配置sessionManager:
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<!-- 會話管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session的失效時長,單位毫秒 -->
<property name="globalSessionTimeout" value="600000"/>
<!-- 刪除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
</bean>
- 驗證碼
需要在驗證賬號和名稱之前校驗驗證碼-------》自定義FormAuthenticationFilter
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response, Object mappedValue) throws Exception {
// 校驗驗證碼
// 從session獲取正確的驗證碼
HttpSession session = ((HttpServletRequest)request).getSession();
//頁面輸入的驗證碼
String randomcode = request.getParameter("randomcode");
//從session中取出驗證碼
String validateCode = (String) session.getAttribute("validateCode");
if (!randomcode.equals(validateCode)) {
// randomCodeError表示驗證碼錯誤
request.setAttribute("shiroLoginFailure", "randomCodeError");
//拒絕訪問,不再校驗賬號和密碼
return true;
}
return super.onAccessDenied(request, response, mappedValue);
}
}
- 修改FormAuthenticationFilter配置
修改applicationContext-shiro.xml中對FormAuthenticationFilter的配置。
<bean id="formAuthenticationFilter"
class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
改為
<bean id="formAuthenticationFilter"
class="cn.itcast.ssm.shiro.MyFormAuthenticationFilter">
- 登陸頁面
添加驗證碼:
<TR>
<TD>驗證碼:</TD>
<TD><input id="randomcode" name="randomcode" size="8" />
<img id="randomcode_img" src="${baseurl}validatecode.jsp" alt="" width="56" height="20" align='absMiddle' />
<a href=javascript:randomcode_refresh()>刷新</a></TD>
</TR>
-
配置validatecode.jsp匿名訪問
修改applicationContext-shiro.xml:
validatecode.jsp.png - 記住我
用戶登陸選擇“自動登陸”本次登陸成功會向cookie寫身份信息,下次登陸從cookie中取出身份信息實現自動登陸。注意:用戶身份實現java.io.Serializable接口,向cookie記錄身份信息需要用戶身份信息對象實現序列化接口。
配置
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="cacheManager"/>
<!-- 記住我 -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<!-- rememberMeManager管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie" />
</bean>
<!-- 記住我cookie -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe" />
<!-- 記住我cookie生效時間30天 -->
<property name="maxAge" value="2592000" />
</bean>
修改formAuthenticationFitler添加頁面中“記住我checkbox”的input名稱:
<bean id="formAuthenticationFilter"
class="cn.itcast.ssm.shiro.MyFormAuthenticationFilter">
<!-- 表單中賬號的input名稱 -->
<property name="usernameParam" value="usercode" />
<!-- 表單中密碼的input名稱 -->
<property name="passwordParam" value="password" />
<property name="rememberMeParam" value="rememberMe"/>
<!-- loginurl:用戶登陸地址,此地址是可以http訪問的url地址 -->
<property name="loginUrl" value="/loginsubmit.action" />
</bean>
- 登陸頁面
在login.jsp中添加“記住我”checkbox。
<TR>
<TD></TD>
<TD>
<input type="checkbox" name="rememberMe" />自動登陸
</TD>
</TR>
附錄:
shiro過慮器
anon
org.apache.shiro.web.filter.authc.AnonymousFilter
authc
org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port
org.apache.shiro.web.filter.authz.PortFilter
rest
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl
org.apache.shiro.web.filter.authz.SslFilter
user
org.apache.shiro.web.filter.authc.UserFilter
logout
org.apache.shiro.web.filter.authc.LogoutFilter
anon :例子 /admins/=anon 沒有參數,表示可以匿名使用。
authc :例如 /admins/user/=authc 表示需要認證(登錄)才能使用,沒有參數
roles :例子 /admins/user/=roles[admin] ,參數可以寫多個,多個時必須加上引號,并且參數之間用逗號分割,當有多個參數時,如admins/user/=roles["admin,guest"] ,每個參數通過才算通過,相當于 hasAllRoles() 方法。
perms :例子 /admins/user/=perms[user:add:] 參數可以寫多個,多個時必須加上引號,并且參數之間用逗號分割,例如 /admins/user/=perms["user:add:,user:modify:"] ,當有多個參數時必須每個參數都通過才通過,想當于isPermitedAll()方法。
rest :例子 /admins/user/=rest[user] ,根據請求的方法,相當于 /admins/user/=perms[user:method] ,其中method為post,get,delete等。
por t:例子 /admins/user/=port[8081] ,當請求的url的端口不是8081是跳轉到 schemal://serverName:8081?queryString ,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置里port的端口,queryString
是你訪問的url里的?后面的參數。
authcBasic :例如 /admins/user/=authcBasic 沒有參數表示httpBasic認證
ssl: 例子 /admins/user/=ssl 沒有參數,表示安全的url請求,協議為https
user :例如 /admins/user/*=user 沒有參數表示必須存在用戶,當登入操作時不做檢查
注:
anon,authcBasic,auchc,user是認證過濾器,
perms,roles,ssl,rest,port是授權過濾器
- shiro的jsp標簽
Jsp頁面添加:
<%@ tagliburi="http://shiro.apache.org/tags" prefix="shiro" %>
標簽名稱 標簽條件(均是顯示標簽內容)
<shiro:authenticated> 登錄之后
<shiro:notAuthenticated> 不在登錄狀態時
<shiro:guest> 用戶在沒有RememberMe時
<shiro:user> 用戶在RememberMe時
<shiro:hasAnyRoles name="abc,123" > 在有abc或者123角色時
<shiro:hasRole name="abc"> 擁有角色abc
<shiro:lacksRole name="abc"> 沒有角色abc
<shiro:hasPermission name="abc"> 擁有權限資源abc
<shiro:lacksPermission name="abc"> 沒有abc權限資源
<shiro:principal> 顯示用戶身份名稱
<shiro:principal property="username"/> 顯示用戶身份中的屬性值