-
1 權限管理
-
1.1 什麼是權限管理
- 基本上涉及到使用者參與的系統都要進行權限管理,權限管理屬于系統安全的範疇,權限管理實作對使用者通路系統的控制,按照安全規則或者安全政策控制使用者可以通路而且隻能通路自己被授權的資源。
- 權限管理包括使用者身份認證和授權兩部分,簡稱認證授權。對于需要通路控制的資源使用者首先經過身份認證,認證通過後使用者具有該資源的通路權限方可通路。
-
1.2 使用者身份認證
-
1.2.1 概念
- 身份認證,就是判斷一個使用者是否為合法使用者的處理過程。最常用的簡單身份認證方式是系統通過核對使用者輸入的使用者名和密碼,看其是否與系統中存儲的該使用者的使用者名和密碼一緻,來判斷使用者身份是否正确。對于采用指紋等系統,則出示指紋;對于硬體Key等刷卡系統,則需要刷卡。
-
1.2.2 使用者名密碼身份認證流程
-
1.2.3 關鍵對象
- 上邊的流程圖中需要了解以下關鍵對象:
- n Subject:主體
- 通路系統的使用者,主體可以是使用者、程式等,進行認證的都稱為主體;
- n Principal:身份資訊
- 是主體(subject)進行身份認證的辨別,辨別必須具有唯一性,如使用者名、手機号、郵箱位址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)。
- n credential:憑證資訊
- 是隻有主體自己知道的安全資訊,如密碼、證書等。
-
1.3 授權
-
1.3.1 概念
- 授權,即通路控制,控制誰能通路哪些資源。主體進行身份認證後需要配置設定權限方可通路系統的資源,對于某些資源沒有權限是無法通路的。
-
1.3.2 授權流程
- 下圖中橙色為授權流程。
-
1.3.3 關鍵對象
- 授權可簡單了解為who對what(which)進行How操作:
- n Who,即主體(Subject),主體需要通路系統中的資源。
- n What,即資源(Resource),如系統菜單、頁面、按鈕、類方法、系統商品資訊等。資源包括資源類型和資源執行個體,比如商品資訊為資源類型,類型為t01的商品為資源執行個體,編号為001的商品資訊也屬于資源執行個體。
- n How,權限/許可(Permission),規定了主體對資源的操作許可,權限離開資源沒有意義,如使用者查詢權限、使用者添權重限、某個類方法的調用權限、編号為001使用者的修改權限等,通過權限可知主體對哪些資源都有哪些操作許可。
- 權限分為粗顆粒和細顆粒,粗顆粒權限是指對資源類型的權限,細顆粒權限是對資源執行個體的權限。
- 主體、資源、權限關系如下圖:
-
1.3.4 權限模型
- 對上節中的主體、資源、權限通過資料模型表示。
- 主體(賬号、密碼)
- 資源(資源名稱、通路位址)
- 權限(權限名稱、資源id)
- 角色(角色名稱)
- 角色和權限關系(角色id、權限id)
- 主體和角色關系(主體id、角色id)
- 如下圖:
- 通常企業開發中将資源和權限表合并為一張權限表,如下:
- 資源(資源名稱、通路位址)
- 權限(權限名稱、資源id)
- 合并為:
- 權限(權限名稱、資源名稱、資源通路位址)
- 上圖常被稱為權限管理的通用模型,不過企業在開發中根據系統自身的特點還會對上圖進行修改,但是使用者、角色、權限、使用者角色關系、角色權限關系是需要去了解的。
-
1.3.5 權限配置設定
- 對主體配置設定權限,主體隻允許在權限範圍内對資源進行操作,比如:對u01使用者配置設定商品修改權限,u01使用者隻能對商品進行修改。
- 權限配置設定的資料通常需要持久化,根據上邊的資料模型建立表并将使用者的權限資訊存儲在資料庫中。
-
1.3.6 權限控制
- 使用者擁有了權限即可操作權限範圍内的資源,系統不知道主體是否具有通路權限需要對使用者的通路進行控制。
-
1.3.6.1基于角色的通路控制
- RBAC基于角色的通路控制(Role-Based Access Control)是以角色為中心進行通路控制,比如:主體的角色為總經理可以查詢企業營運報表,查詢員工工資資訊等,通路控制流程如下:
- 上圖中的判斷邏輯代碼可以了解為:
- if(主體.hasRole("總經理角色id")){
- 查詢工資
- }
- 缺點:以角色進行通路控制粒度較粗,如果上圖中查詢工資所需要的角色變化為總經理和部門經理,此時就需要修改判斷邏輯為“判斷主體的角色是否是總經理或部門經理”,系統可擴充性差。
- 修改代碼如下:
- if(主體.hasRole("總經理角色id") || 主體.hasRole("部門經理角色id")){
- 查詢工資
- }
-
1.3.6.2基于資源的通路控制
- RBAC基于資源的通路控制(Resource-Based Access Control)是以資源為中心進行通路控制,比如:主體必須具有查詢工資權限才可以查詢員工工資資訊等,通路控制流程如下:
- 上圖中的判斷邏輯代碼可以了解為:
- if(主體.hasPermission("查詢工資權限辨別")){
- 查詢工資
- }
- 優點:系統設計時定義好查詢工資的權限辨別,即使查詢工資所需要的角色變化為總經理和部門經理也隻需要将“查詢工資資訊權限”添加到“部門經理角色”的權限清單中,判斷邏輯不用修改,系統可擴充性強。
-
2 權限管了解決方案
-
2.1 粗顆粒度和細顆粒度
-
2.1.1 什麼是粗顆粒度和細顆粒度
- 對資源類型的管理稱為粗顆粒度權限管理,即隻控制到菜單、按鈕、方法,粗粒度的例子比如:使用者具有使用者管理的權限,具有導出訂單明細的權限。對資源執行個體的控制稱為細顆粒度權限管理,即控制到資料級别的權限,比如:使用者隻允許修改本部門的員工資訊,使用者隻允許導出自己建立的訂單明細。
-
2.1.2 如何實作粗顆粒度和細顆粒度
- 對于粗顆粒度的權限管理可以很容易做系統架構級别的功能,即系統功能操作使用統一的粗顆粒度的權限管理。
- 對于細顆粒度的權限管理不建議做成系統架構級别的功能,因為對資料級别的控制是系統的業務需求,随着業務需求的變更業務功能變化的可能性很大,建議對資料級别的權限控制在業務層個性化開發,比如:使用者隻允許修改自己建立的商品資訊可以在service接口添加校驗實作,service接口需要傳入目前操作人的辨別,與商品資訊建立人辨別對比,不一緻則不允許修改商品資訊。
-
3 shiro介紹
-
3.1 什麼是shiro
- Shiro是apache旗下一個開源架構,它将軟體系統的安全認證相關的功能抽取出來,實作使用者身份認證,權限授權、加密、會話管理等功能,組成了一個通用的安全認證架構。
-
3.2 為什麼要學shiro
- 既然shiro将安全認證相關的功能抽取出來組成一個架構,使用shiro就可以非常快速的完成認證、授權等功能的開發,降低系統成本。
- shiro使用廣泛,shiro可以運作在web應用,非web應用,叢集分布式應用中越來越多的使用者開始使用shiro。
- java領域中spring security(原名Acegi)也是一個開源的權限管理架構,但是spring security依賴spring運作,而shiro就相對獨立,最主要是因為shiro使用簡單、靈活,是以現在越來越多的使用者選擇shiro。
-
3.3 Shiro架構
-
3.3.1 Subject
- Subject即主體,外部應用與subject進行互動,subject記錄了目前操作使用者,将使用者的概念了解為目前操作的主體,可能是一個通過浏覽器請求的使用者,也可能是一個運作的程式。 Subject在shiro中是一個接口,接口中定義了很多認證授相關的方法,外部程式通過subject進行認證授,而subject是通過SecurityManager安全管理器進行認證授權
-
3.3.2 SecurityManager
- SecurityManager即安全管理器,對全部的subject進行安全管理,它是shiro的核心,負責對所有的subject進行安全管理。通過SecurityManager可以完成subject的認證、授權等,實質上SecurityManager是通過Authenticator進行認證,通過Authorizer進行授權,通過SessionManager進行會話管理等。
- SecurityManager是一個接口,繼承了Authenticator, Authorizer, SessionManager這三個接口。
-
3.3.3 Authenticator
- Authenticator即認證器,對使用者身份進行認證,Authenticator是一個接口,shiro提供ModularRealmAuthenticator實作類,通過ModularRealmAuthenticator基本上可以滿足大多數需求,也可以自定義認證器。
-
3.3.4 Authorizer
- Authorizer即授權器,使用者通過認證器認證通過,在通路功能時需要通過授權器判斷使用者是否有此功能的操作權限。
-
3.3.5 realm
- Realm即領域,相當于datasource資料源,securityManager進行安全認證需要通過Realm擷取使用者權限資料,比如:如果使用者身份資料在資料庫那麼realm就需要從資料庫擷取使用者身份資訊。
- 注意:不要把realm了解成隻是從資料源取資料,在realm中還有認證授權校驗的相關的代碼。
-
3.3.6 sessionManager
- sessionManager即會話管理,shiro架構定義了一套會話管理,它不依賴web容器的session,是以shiro可以使用在非web應用上,也可以将分布式應用的會話集中在一點管理,此特性可使它實作單點登入。
-
3.3.7 SessionDAO
- SessionDAO即會話dao,是對session會話操作的一套接口,比如要将session存儲到資料庫,可以通過jdbc将會話存儲到資料庫。
-
3.3.8 CacheManager
- CacheManager即緩存管理,将使用者權限資料存儲在緩存,這樣可以提高性能。
-
3.3.9 Cryptography
- Cryptography即密碼管理,shiro提供了一套加密/解密的元件,友善開發。比如提供常用的散列、加/解密等功能。
-
3.4 shiro的jar包
- 與其它java開源架構類似,将shiro的jar包加入項目就可以使用shiro提供的功能了。shiro-core是核心包必須選用,還提供了與web整合的shiro-web、與spring整合的shiro-spring、與任務排程quartz整合的shiro-quartz等,下邊是shiro各jar包的maven坐标。
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- <version>1.2.3</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-web</artifactId>
- <version>1.2.3</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-spring</artifactId>
- <version>1.2.3</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-ehcache</artifactId>
- <version>1.2.3</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-quartz</artifactId>
- <version>1.2.3</version>
- </dependency>
- 也可以通過引入shiro-all包括shiro所有的包:
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-all</artifactId>
- <version>1.2.3</version>
- </dependency>
-
4 shiro認證
-
4.1 認證流程
-
4.2 入門程式(使用者登陸和退出)
-
4.2.1 加入shiro-core的Jar包及依賴包
-
4.2.2 log4j.properties日志配置檔案
- log4j.rootLogger=debug, stdout
- log4j.appender.stdout=org.apache.log4j.ConsoleAppender
- log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
- log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
-
4.2.3 shiro.ini
- 通過Shiro.ini配置檔案初始化SecurityManager環境。
- 配置 eclipse支援ini檔案編輯:
- 在eclipse配置後,在classpath建立shiro.ini配置檔案,為了友善測試将使用者名和密碼配置的shiro.ini配置檔案中:
- [users]
- zhang=123
- lisi=123
-
4.2.4 認證代碼
- // 使用者登陸、使用者退出
- @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);
- }
-
4.2.5 認證執行流程
- 1、 建立token令牌,token中有使用者送出的認證資訊即賬号和密碼
- 2、 執行subject.login(token),最終由securityManager通過Authenticator進行認證
- 3、 Authenticator的實作ModularRealmAuthenticator調用realm從ini配置檔案取使用者真實的賬号和密碼,這裡使用的是IniRealm(shiro自帶)
- 4、 IniRealm先根據token中的賬号去ini中找該賬号,如果找不到則給ModularRealmAuthenticator傳回null,如果找到則比對密碼,比對密碼成功則認證通過。
-
4.2.6 常見的異常
- n UnknownAccountException
- 賬号不存在異常如下:
- org.apache.shiro.authc.UnknownAccountException: No account found for user。。。。
- n 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(憑證過期)等
-
4.3 自定義Realm
- 上邊的程式使用的是Shiro自帶的IniRealm,IniRealm從ini配置檔案中讀取使用者的資訊,大部分情況下需要從系統的資料庫中讀取使用者資訊,是以需要自定義realm。
-
4.3.1 shiro提供的realm
- 最基礎的是Realm接口,CachingRealm負責緩存處理,AuthenticationRealm負責認證,AuthorizingRealm負責授權,通常自定義的realm繼承AuthorizingRealm。
-
4.3.2 自定義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;
- }
- }
-
4.3.3 shiro-realm.ini
- [main]
- #自定義 realm
- customRealm=cn.itcast.shiro.authentication.realm.CustomRealm1
- #将realm設定到securityManager
- securityManager.realms=$customRealm
- 思考:這裡為什麼不用配置[users]了??
-
4.3.4 測試代碼
- 測試代碼同入門程式,将ini的位址修改為shiro-realm.ini。
- 分别模拟賬号不存在、密碼錯誤、賬号和密碼正确進行測試。
-
4.4 雜湊演算法
- 雜湊演算法一般用于生成一段文本的摘要資訊,雜湊演算法不可逆,将内容可以生成摘要,無法将摘要轉成原始内容。雜湊演算法常用于對密碼進行散列,常用的雜湊演算法有MD5、SHA。
- 一般雜湊演算法需要提供一個salt(鹽)與原始内容生成摘要資訊,這樣做的目的是為了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿着“96e79218965eb72c92a549dd5a330112”去md5破解網站很容易進行破解,如果要是對111111和salt(鹽,一個随機數)進行散列,這樣雖然密碼都是111111加不同的鹽會生成不同的散列值。
-
4.4.1 例子
- //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);
-
4.4.2 在realm中使用
- 實際應用是将鹽和散列後的值存在資料庫中,自動realm從資料庫取出鹽和加密後的值由shiro完成密碼校驗。
-
4.4.2.1自定義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;
- }
-
4.4.2.2realm配置
- 配置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
-
4.4.2.3測試代碼
- 測試代碼同上個章節,注意修改ini路徑。
-
5 shiro授權
-
5.1 授權流程
-
5.2 授權方式
- Shiro 支援三種方式的授權:
- n 程式設計式:通過寫if/else 授權代碼塊完成:
- Subject subject = SecurityUtils.getSubject();
- if(subject.hasRole(“admin”)) {
- //有權限
- } else {
- //無權限
- }
- n 注解式:通過在執行的Java方法上放置相應的注解完成:
- @RequiresRoles("admin")
- public void hello() {
- //有權限
- }
- n JSP/GSP 标簽:在JSP/GSP 頁面通過相應的标簽完成:
- <shiro:hasRole name="admin">
- <!— 有權限—>
- </shiro:hasRole>
- 本教程式授權測試使用第一種程式設計方式,實際與web系統內建使用後兩種方式。
-
5.3 授權測試
-
5.3.1 shiro-permission.ini
- 建立存放權限的配置檔案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...”,首先根據使用者名找角色,再根據角色找權限,角色是權限集合。
-
5.3.2 權限字元串規則
- 權限字元串的規則是:“資源辨別符:操作:資源執行個體辨別符”,意思是對哪個資源的哪個執行個體具有什麼操作,“:”是資源/操作/執行個體的分割符,權限字元串也可以使用*通配符。
- 例子:
- 使用者建立權限:user:create,或user:create:*
- 使用者修改執行個體001的權限:user:update:001
- 使用者執行個體001的所有權限:user:*:001
-
5.3.3 測試代碼
- 測試代碼同認證代碼,注意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");
- }
-
5.3.4 基于角色的授權
- // 使用者授權檢測 基于角色授權
- // 是否有某一個角色
- 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 [.....]
-
5.3.5 基于資源授權
- // 基于資源授權
- 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 [....]
-
5.4 自定義realm
- 與上邊認證自定義realm一樣,大部分情況是要從資料庫擷取權限資料,這裡直接實作基于資源的授權。
-
5.4.1 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;
- }
-
5.4.2 shiro-realm.ini
- ini配置檔案還使用認證階段使用的,不用改變。
- 思考:shiro-permission.ini中的[roles]為什麼不需要了??
-
5.4.3 測試代碼
- 同上邊的授權測試代碼,注意修改ini位址為shiro-realm.ini。
-
5.4.4 授權執行流程
- 1、 執行subject.isPermitted("user:create")
- 2、 securityManager通過ModularRealmAuthorizer進行授權
- 3、 ModularRealmAuthorizer調用realm擷取權限資訊
- 4、 ModularRealmAuthorizer再通過permissionResolver解析權限字元串,校驗是否比對
-
6 shiro與項目內建開發
-
6.1 shiro與spring web項目整合
- shiro與springweb項目整合在“基于url攔截實作的工程”基礎上整合,基于url攔截實作的工程的技術架構是springmvc+mybatis,整合注意兩點:
- 1、shiro與spring整合
- 2、加入shiro對web應用的支援
-
6.1.1 取消原springmvc認證和授權攔截器
- 去掉springmvc.xml中配置的LoginInterceptor和PermissionInterceptor攔截器。
-
6.1.2 加入shiro的 jar包
-
6.1.3 web.xml添加shiro Filter
- <!-- shiro過慮器,DelegatingFilterProx會從spring容器中找shiroFilter -->
- <filter>
- <filter-name>shiroFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- <init-param>
- <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>
-
6.1.4 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>
- securityManager:這個屬性是必須的。
- loginUrl:沒有登入認證的使用者請求将跳轉到此位址,不是必須的屬性,不輸入位址的話會自動尋找項目web項目的根目錄下的”/login.jsp”頁面。
- unauthorizedUrl:沒有權限預設跳轉的頁面。
-
6.1.5 使用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”權限方可執行。
- 其它的方法參考示例添加注解,一邊添加一邊思考這比基于url攔截有什麼好處。
-
6.1.6 自定義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;
- }
- }
-
6.1.7 登入
- //使用者登陸頁面
- @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();//最終在異常處理器生成未知錯誤
- }
- }
-
6.1.8 首頁
- 由于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";
- }
-
6.1.9 退出
- 由于使用shiro的sessionManager,不用開發退出功能,使用shiro的logout攔截器即可。
- <!-- 退出攔截,請求logout.action執行退出操作 -->
- /logout.action = logout
-
6.1.10 無權限refuse.jsp
- 當使用者無操作權限,shiro将跳轉到refuse.jsp頁面。
- 參考:applicationContext-shiro.xml
-
6.2 realm連接配接資料庫
-
6.2.1 添加憑證比對器
- 添加憑證比對器實作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>
-
6.2.2 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;
- }
- }
-
6.3 緩存
- shiro每個授權都會通過realm擷取權限資訊,為了提高通路速度需要添加緩存,第一次從realm中讀取權限資料,之後不再讀取,這裡Shiro和Ehcache整合。
-
6.3.1 添加Ehcache的jar包
-
6.3.2 配置
- 在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>
-
6.4 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>
-
6.5 驗證碼
-
6.5.1 自定義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);
- }
- }
-
6.5.2 修改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">
-
6.5.3 登陸頁面
- 添加驗證碼:
- <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>
6.5.4 配置validatecode.jsp匿名通路
- 修改applicationContext-shiro.xml:
-
6.6 記住我
- 使用者登陸選擇“自動登陸”本次登陸成功會向cookie寫身份資訊,下次登陸從cookie中取出身份資訊實作自動登陸。
-
6.6.1 使用者身份實作java.io.Serializable接口
- 向cookie記錄身份資訊需要使用者身份資訊對象實作序列化接口,如下:
-
6.6.2 配置
- <!-- 安全管理器 -->
- <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>
-
6.6.3 登陸頁面
- 在login.jsp中添加“記住我”checkbox。
- <TR>
- <TD></TD>
- <TD>
- <input type="checkbox" name="rememberMe" />自動登陸
- </TD>
- </TR>
-
7 附:
-
7.1 shiro過慮器
過濾器簡稱 | 對應的java類 |
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等。
port:例子/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是授權過濾器
7.2 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> | 顯示使用者身份名稱 |