1. 程式人生 > >shiro (java安全框架)

shiro (java安全框架)

shiro是幹什麼的:

Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼學和會話管理

為什麼要學shiro?優勢在哪?

既然shiro將安全認證相關的功能抽取出來組成一個框架,使用shiro就可以非常快速的完成認證、授權等功能的開發,降低系統成本。

shiro使用廣泛,shiro可以執行在web應用,非web應用,叢集分散式應用中越來越多的使用者開始使用shiro。

java領域中spring security(原名Acegi)也是一個開源的許可權管理框架,但是spring security依賴spring執行,而shiro就相對獨立,最主要是因為shiro使用簡單、靈活,

所以現在越來越多的使用者選擇shiro。

Shiro架構

核心元件:

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即授權器,使用者通過認證器認證通過,在訪問功能時需要通過授權器判斷使用者是否有此功能的操作許可權。

sessionManager 會話管理

sessionManager即會話管理,shiro框架定義了一套會話管理,它不依賴web容器的session,所以shiro可以使用在非web應用上,也可以將分散式應用的會話集中在一點管理,此特性可使它實現單點登入。

realm 領域

Realm即領域,相當於datasource資料來源,securityManager進行安全認證需要通過Realm獲取使用者許可權資料,比如:如果使用者身份資料在資料庫,那麼realm就需要從資料庫獲取使用者身份資訊。

注意:不要把realm理解成只是從資料來源取資料,在realm中還有認證授權校驗的相關的程式碼在realm中儲存授權和認證的邏輯。

SessionDAO  會話dao

SessionDAO即會話dao,是對session會話操作的一套介面,比如要將session儲存到資料庫,可以通過jdbc將會話儲存到資料庫。

CacheManager 快取管理

CacheManager即快取管理,將使用者許可權資料儲存在快取,這樣可以提高效能。

Cryptography 密碼管理

Cryptography即密碼管理,shiro提供了一套加密/解密的元件,方便開發。比如提供常用的雜湊、加/解密等功能。

本章節以許可權管理展開:

許可權管理

什麼是許可權管理?

基本上涉及到使用者參與的系統都要進行許可權管理,許可權管理屬於系統安全的範疇,許可權管理實現對使用者訪問系統的控制,按照安全規則或者安全策略控制使用者可以訪問而且只能訪問自己被授權的資源

許可權管理包括使用者身份認證授權兩部分,簡稱認證授權。對於需要訪問控制的資源使用者首先經過身份認證,認證通過後使用者具有該資源的訪問許可權方可訪問。

舉例使用者名稱密碼身份認證流程

關鍵物件

subject:主體,理解為使用者,可能是程式,都要去訪問系統的資源,系統需要對subject進行身份認證。

principal:身份資訊,通常是唯一的,一個主體還有多個身份資訊,但是都有一個主身份資訊(primary principal)

credential:憑證資訊,是隻有主體自己知道的安全資訊。可以是密碼 、證書、指紋。

授權流程

授權的過程理解為:who對what(which)進行how操作。

who:主體即subject,subject在認證通過後系統進行訪問控制。

what(which):資源(Resource),subject必須具備資源的訪問許可權才可訪問該 資源。資源比如:系統使用者列表頁面、商品修改選單、商品id為001的商品資訊。

        資源分為資源型別和資源例項

            系統的使用者資訊就是資源型別,相當於java類。

            系統中id為001的使用者就是資源例項,相當於new的java物件。

how:許可權/許可(permission) ,針對資源的許可權或許可,subject具有permission訪問資源,如何訪問/操作需要定義permission,許可權比如:使用者新增、使用者修改、商品刪除。

許可權控制

使用者擁有了許可權即可操作許可權範圍內的資源,系統不知道主體是否具有訪問許可權需要對使用者的訪問進行控制。

1,基於角色的訪問控制

上圖中的判斷邏輯程式碼可以理解為:

if(主體.hasRole("總經理角色id")){
	查詢工資
}

缺點:以角色進行訪問控制粒度較粗,如果上圖中查詢工資所需要的角色變化為總經理和部門經理,此時就需要修改判斷邏輯為“判斷主體的角色是否是總經理或部門經理”,系統可擴充套件性差。

2,基於資源的訪問控制

資源在系統中是不變的,比如資源有:類中的方法,頁面中的按鈕。

對資源的訪問需要具有permission許可權,程式碼可以寫為:

if(主體.hasPermission("查詢工資許可權標識")){
	查詢工資
}

優點:系統設計時定義好查詢工資的許可權標識,即使查詢工資所需要的角色變化為總經理和部門經理也只需要將“查詢工資資訊許可權”新增到“部門經理角色”的許可權列表中,判斷邏輯不用修改,系統可擴充套件性強。

建議使用基於資源的訪問控制實現許可權管理。

許可權管理解決方案

粗顆粒度和細顆粒度

什麼是粗顆粒度和細顆粒度

資源型別的管理稱為粗顆粒度許可權管理,即只控制到選單、按鈕、方法,粗粒度的例子比如:使用者具有使用者管理的許可權,具有匯出訂單明細的許可權。

資源例項的控制稱為細顆粒度許可權管理,即控制到資料級別的許可權,比如:使用者只允許修改本部門的員工資訊,使用者只允許匯出自己建立的訂單明細。

如何實現粗顆粒度和細顆粒度

對於粗顆粒度的許可權管理可以很容易做系統架構級別的功能,即系統功能操作使用統一的粗顆粒度的許可權管理。

對於細顆粒度的許可權管理不建議做成系統架構級別的功能,因為對資料級別的控制是系統的業務需求,隨著業務需求的變更業務功能變化的可能性很大,建議對資料級別的許可權控制在業務層個性化開發,比如:使用者只允許修改自己建立的商品資訊可以在service介面新增校驗實現,service介面需要傳入當前操作人的標識,與商品資訊建立人標識對比,不一致則不允許修改商品資訊。

比如:部門經理只查詢本部門員工資訊,在service介面提供一個部門id的引數,controller中根據當前使用者的資訊得到該 使用者屬於哪個部門,呼叫service時將部門id傳入service,實現該使用者只查詢本部門的員工。

1,基於url攔截

基於url攔截是企業中常用的許可權管理方法,實現思路是:將系統操作的每個url配置在許可權表中,將許可權對應到角色,將角色分配給使用者,使用者訪問系統功能通過Filter進行過慮,過慮器獲取到使用者訪問的url,只要訪問的url是使用者分配角色中的url則放行繼續訪問。

1,基於url攔截實現

環境準備

jdk:1.7.0_72

web容器:tomcat7

系統框架:springmvc3.2.0+mybatis3.2.7(詳細參考springmvc教案)

前臺UI:jquery easyUI1.2.2

資料庫

建立mysql5.1資料庫

建立使用者表、角色表、許可權表、角色許可權關係表、使用者角色關係表。

匯入指令碼,先匯入shiro_sql_talbe.sql再匯入shiro-sql_table_data.sql

activeUser使用者身份類

使用者登陸成功記錄activeUser資訊並將activeUser存入session。

public class ActiveUser implements java.io.Serializable {
	private String userid;//使用者id
	private String usercode;// 使用者賬號
	private String username;// 使用者名稱稱

	private List<SysPermission> menus;// 選單
	private List<SysPermission> permissions;// 許可權

anonymousURL.properties

anonymousURL.properties公開訪問地址,無需身份認證即可訪問。

commonURL.properties

commonURL.properties公共訪問地址,身份認證通過無需分配許可權即可訪問。

使用者身份認證攔截器

使用springmvc攔截器對使用者身份認證進行攔截,如果使用者沒有登陸則跳轉到登陸頁面,本功能也可以使用filter實現 。

public class LoginInterceptor implements HandlerInterceptor {

	// 在進入controller方法之前執行
	// 使用場景:比如身份認證校驗攔截,使用者許可權攔截,如果攔截不放行,controller方法不再執行
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {

		// 校驗使用者訪問是否是公開資源地址(無需認證即可訪問)
		List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");

		// 使用者訪問的url
		String url = request.getRequestURI();
		for (String open_url : open_urls) {
			if (url.indexOf(open_url) >= 0) {
				// 如果訪問的是公開 地址則放行
				return true;
			}
		}

		// 校驗使用者身份是否認證通過
		HttpSession session = request.getSession();
		ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
		if (activeUser != null) {
			// 使用者已經登陸認證,放行
			return true;
		}
		// 跳轉到登陸頁面
		request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,
				response);
		return false;
	}

配置攔截器      在springmvc.xml中配置攔截器

使用者授權攔截器

使用springmvc攔截器對使用者訪問url進行攔截,如果使用者訪問的url沒有分配許可權則跳轉到無權操作提示頁面(refuse.jsp),本功能也可以使用filter實現。

public class PermissionInterceptor implements HandlerInterceptor {

	// 在進入controller方法之前執行
	// 使用場景:比如身份認證校驗攔截,使用者許可權攔截,如果攔截不放行,controller方法不再執行
	// 進入action方法前要執行
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {
		// TODO Auto-generated method stub
		// 使用者訪問地址:
		String url = request.getRequestURI();

		// 校驗使用者訪問是否是公開資源地址(無需認證即可訪問)
		List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
		// 使用者訪問的url
		for (String open_url : open_urls) {
			if (url.indexOf(open_url) >= 0) {
				// 如果訪問的是公開 地址則放行
				return true;
			}
		}
		//從 session獲取使用者公共訪問地址(認證通過無需分配許可權即可訪問)
		List<String> common_urls = ResourcesUtil.gekeyList("commonURL");
		// 使用者訪問的url
		for (String common_url : common_urls) {
			if (url.indexOf(common_url) >= 0) {
				// 如果訪問的是公共地址則放行
				return true;
			}
		}
		// 從session獲取使用者許可權資訊

		HttpSession session = request.getSession();

		ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");

		// 取出session中許可權url
		// 獲取使用者操作許可權
		List<SysPermission> permission_list = activeUser.getPermissions();
		// 校驗使用者訪問地址是否在使用者許可權範圍內
		for (SysPermission sysPermission : permission_list) {
			String permission_url = sysPermission.getUrl();
			if (url.contains(permission_url)) {
				return true;
			}
		}

		// 跳轉到頁面
		request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(
				request, response);
		return false;
	}

mapper介面:根據使用者id查詢使用者許可權的選單

使用者登陸

使用者輸入使用者賬號和密碼登陸,登陸成功將使用者的身份資訊(使用者賬號、密碼、許可權選單、許可權url等)記入activeUser類,並寫入session。

service(1,根據賬號查詢使用者,進行使用者名稱和密碼校驗;2,根據使用者id查詢使用者許可權;3,根據使用者id獲取選單)

    /**
	 * 
	 * <p>
	 * Title: authenticat
	 * </p>
	 * <p>
	 * Description:使用者認證
	 * </p>
	 * 
	 * @param usercode
	 *            使用者賬號
	 * @param password
	 *            使用者密碼
	 * @return ActiveUser 使用者身份資訊
	 * @throws Exception
	 */
	public ActiveUser authenticat(String usercode, String password)
			throws Exception;

	// 根據賬號查詢使用者
	public SysUser findSysuserByUsercode(String usercode) throws Exception;

	// 根據使用者id獲取許可權
	public List<SysPermission> findSysPermissionList(String userid)
			throws Exception;

	// 根據使用者id獲取選單
	public List<SysPermission> findMenuList(String userid) throws Exception;

controller(使用者身份認證、記錄session)

    //使用者登陸提交
	@RequestMapping("/loginsubmit")
	public String loginsubmit(HttpSession session,String usercode,String password,String randomcode) throws Exception{

		//校驗驗證碼
		//從session獲取正確的驗證碼
		String validateCode = (String)session.getAttribute("validateCode");
		if(!randomcode.equals(validateCode)){
			//丟擲異常:驗證碼錯誤
			throw new CustomException("驗證碼 錯誤 !");
		}
		//使用者身份認證
		ActiveUser activeUser = sysService.authenticat(usercode, password);
		
		//記錄session
		session.setAttribute("activeUser", activeUser);
		
		return "redirect:first.action";
	}

配置授權攔截器  注意:將授權攔截器配置在使用者認證攔截的下邊。

小結

使用基於url攔截的許可權管理方式,實現起來比較簡單,不依賴框架,使用web提供filter就可以實現。

問題:需要將所有的url全部配置起來,有些繁瑣,不易維護,url(資源)和許可權表示方式不規範。

2,基於shiro實現

shiro的jar包

與其它java開源框架類似,將shiro的jar包加入專案就可以使用shiro提供的功能了。shiro-core是核心包必須選用,還提供了與web整合的shiro-web、與spring整合的shiro-spring、與任務排程quartz整合的shiro-quartz等。

shiro認證:

認證流程

通過Shiro.ini配置檔案初始化SecurityManager環境。

.ini 檔案是Initialization File的縮寫,即初始化檔案。

INI配置檔案是一種key/value的鍵值對配置,提供了分類的概念,每一個類中的key不可重複,#號代表註釋,shiro.ini檔案預設在/WEB-INF/ 或classpath下,shiro會自動查詢,INI配置檔案一般適用於使用者少且不需要在執行時動態建立的情景下使用。

shiro框架裡新造了一個Ini類,當我們傳入資源時,Ini裡使用流一行一行的讀資源,

當遇到”#”或”;”開頭的則直接跳過;

遇到“[*]”則將中括號裡的字串看過Section(區塊)的key,後面一行一行都視做該區域的內容,直到遇到新的中括號。隨後再解讀區域下面多行字串(至少一行);

如果遇到“:”或“=”或“”,則前面當做key,後面的則是為value(同時會過濾掉value裡前後空格以及“=”前後空格),存到一個Section裡,最後把所有行解析完後放到名為sections的HashMap裡。

IniSecurityManagerFacotry繼承自IniFactorySupport,而IniFactorySupport有個setIni()方法將解析出來的Ini結構資料儲存到該類裡,其它什麼都不做。

原始碼解析:

當配置檔案裡出現[users]或[roles]時,IniSecurityManagerFacotry會初始化一個IniRealm做為資料來源,把ini傳入到IniRealm裡,IniRealm的name是“iniRealm”。並把realm存到securityManager的realms屬性集合裡。

當出現[main]時,說明是主配置。當key裡不出現“.”或者以“.class”結尾,說明是需要例項化的類,value值即為類的全限名,這些例項最張會被反射注入到DefaultSecurityManager的例項securityManager裡。否則視為屬性,用反射去設定上次例項化的物件屬性值。其中objects是包含著key是“securityManager”,value為DefaultSecurityManager物件的Map物件。所有被main標記的都會被注入到securityManager”。

當不出現“[]”時,“”空即為sections的key。只要第一行沒出現”[]”則一定會出現key為空的map的鍵值對。(注意一點,如果沒有[main],則取sections裡””即空為的key的資料做為主配置)

當我們呼叫IniSecurityManagerFacotry裡getInstance()方法時,會根據是否有ini資料來呼叫不同的方法建立不同的SecurityManager.當有ini時呼叫


protected SecurityManager createInstance(Ini ini) {
        if (CollectionUtils.isEmpty(ini)) {
            throw new NullPointerException("Ini argument cannot be null or empty.");
        }
        //createSecurityManager()才是重點
        SecurityManager securityManager = createSecurityManager(ini);
        if (securityManager == null) {
            String msg = SecurityManager.class + " instance cannot be null.";
            throw new ConfigurationException(msg);
        }
        return securityManager;
  }

當沒有時呼叫:

protected SecurityManager createDefaultInstance() {
        return new DefaultSecurityManager();
}

一般將realm標記為[main],那麼會生成Realm的例項,儲存到DefaultSecurityManager的realms集合裡,這樣securityManager就有資料來源了。

常用的用shiro的API的例子:

Factory<org.apache.shiro.mgt.SecurityManager> factory = 
            new IniSecurityManagerFactory("auth.ini");
        // Setting up the SecurityManager...
        org.apache.shiro.mgt.SecurityManager securityManager 
            = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        Subject user = SecurityUtils.getSubject();

把配置檔案裡配置的引數放到Ini裡,並把ini傳給Realm例項,同時造一個DefaultSecurityManager例項。將securityManager放到SecurityUtils裡,同時從SecurityUtils裡取Subject。Subject相當於當前執行緒裡的相當“使用者”,體現在程式裡即是儲存使用者相關身份和憑證等的資訊,以及操作方法。

如果我們跳出Ini配置的束縛,我們應該能得到結論,我們應該給SecurityManager提供一個或多個Realm物件在到realms裡,比如在spring框架的xml裡配,SecurityUtils作用把Subject和SecurityManager關聯起來了,只要能把當前使用者資訊(Subject)和SecurityManager(包含驗證的資料來源資訊等配置)搭上話,那麼要麼我們自己用API做事,要麼Spring幫我們管理都很方便。

1.[users]部分

#提供了對使用者/密碼及其角色的配置,使用者名稱=密碼,角色1,角色2 

username=password,role1,role2

例如:
配置使用者名稱/密碼及其角色,格式:“使用者名稱=密碼,角色1,角色2”,角色部分可省略。如:

[users] 
zhang=123,role1,role2 
wang=123    

2. [roles] 

#提供了角色及許可權之間關係的配置,角色=許可權1,許可權2 

role1=permission1,permission2

例如:
配置角色及許可權之間的關係,格式:“角色=許可權1,許可權2”;如:

[roles] 
role1=user:create,user:update 
role2=*  

如果只有角色沒有對應的許可權,可以不配roles

3. [main]部分

提供了對根物件securityManager及其依賴物件的配置。

建立物件

securityManager=org.apache.shiro.mgt.DefaultSecurityManager 

其構造器必須是public空參構造器,通過反射建立相應的例項。

1、物件名=全限定類名  相對於呼叫public無參構造器建立物件

2、物件名.屬性名=值   相當於呼叫setter方法設定常量值

3、物件名.屬性名=$物件引用   相當於呼叫setter方法設定物件引用

4.[urls] 

#用於web,提供了對web url攔截相關的配置,url=攔截器[引數],攔截器 

/index.html = anon 
/admin/** = authc, roles[admin],perms["permission1"]

shiro-first.ini   通過此配置檔案建立securityManager工廠。

需要修改eclipse的ini的編輯器:

認證程式碼

    // 使用者登陸和退出
	@Test
	public void testLoginAndLogout() {
		// 建立securityManager工廠,通過ini配置檔案建立securityManager工廠
		Factory<SecurityManager> factory = new IniSecurityManagerFactory(
				"classpath:shiro-first.ini");
		//建立SecurityManager
		SecurityManager securityManager = factory.getInstance();
		//將securityManager設定當前的執行環境中
		SecurityUtils.setSecurityManager(securityManager);
		//從SecurityUtils裡邊建立一個subject
		Subject subject = SecurityUtils.getSubject();
		
		//在認證提交前準備token(令牌)
		UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "111111");
		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、通過ini配置檔案建立securityManager

2、呼叫subject.login方法主體提交認證,提交的token

3、securityManager進行認證,securityManager最終由ModularRealmAuthenticator進行認證。

4、ModularRealmAuthenticator呼叫IniRealm(給realm傳入token) 去ini配置檔案中查詢使用者資訊

5、IniRealm根據輸入的token(UsernamePasswordToken)從 shiro-first.ini查詢使用者資訊,根據賬號查詢使用者資訊(賬號和密碼)

         如果查詢到使用者資訊,就給ModularRealmAuthenticator返回使用者資訊(賬號和密碼)

         如果查詢不到,就給ModularRealmAuthenticator返回null

6、ModularRealmAuthenticator接收IniRealm返回Authentication認證資訊

         如果返回的認證資訊是null,ModularRealmAuthenticator丟擲異常(org.apache.shiro.authc.UnknownAccountException

        如果返回的認證資訊不是null(說明inirealm找到了使用者),對IniRealm返回使用者密碼 (在ini檔案中存在)和 token中的密碼 進行對比,如果不一致丟擲異常(org.apache.shiro.authc.IncorrectCredentialsException

小結:

ModularRealmAuthenticator作用進行認證,需要呼叫realm查詢使用者資訊(在資料庫中存在使用者資訊)

ModularRealmAuthenticator進行密碼對比(認證過程)。

realm:需要根據token中的身份資訊去查詢資料庫(入門程式使用ini配置檔案),如果查到使用者返回認證資訊,如果查詢不到返回null。

常見的異常

  1. UnknownAccountException

賬號不存在異常如下:

org.apache.shiro.authc.UnknownAccountException: No account found for user。。。。

  1. 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

測試

同上邊的入門程式,需要更改ini配置檔案路徑:

Factory<SecurityManager> factory = new IniSecurityManagerFactory(

            "classpath:shiro-realm.ini");

雜湊演算法

雜湊演算法一般用於生成一段文字的摘要資訊,雜湊演算法不可逆,將內容可以生成摘要,無法將摘要轉成原始內容。雜湊演算法常用於對密碼進行雜湊,常用的雜湊演算法有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中使用

實際應用是將鹽和雜湊後的值存在資料庫中,自動realm從資料庫取出鹽和加密後的值由shiro完成密碼校驗。

自定義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() {
//有許可權
}

JSP/GSP 標籤:在JSP/GSP 頁面通過相應的標籤完成:

<shiro:hasRole name="admin">
<!— 有許可權—>
</shiro:hasRole>

在此授權測試使用第一種程式設計方式,實際與web系統整合使用後兩種方式。

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
                //查到授權資料,返回授權資訊(要包括上邊的permissions)
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                //將上邊查詢到的授權資訊填充到simpleAuthorInfo物件中
		for(String permission:permissions){
			simpleAuthorizationInfo.addStringPermission(permission);
		}
		return simpleAuthorizationInfo;
	}

在shiro-realm.ini中配置自定義的realm,將realm設定到securityManager中。

ini配置檔案還使用認證階段使用的,不用改變。

授權執行流程

  1. 執行subject.isPermitted("user:create")
  2. securityManager通過ModularRealmAuthorizer進行授權
  3. ModularRealmAuthorizer呼叫realm(自定義的CustomRealm)從資料庫查詢許可權資料,呼叫realm的授權方法:doGetAuthorizationInfo
  4. realm從資料庫查詢許可權資料,返回ModularRealmAuthorizer
  5. ModularRealmAuthorizer再通過permissionResolver解析許可權字串,校驗是否匹配
  6. 如果比對後,isPermitted中"permission串"在realm查詢到許可權資料中,說明使用者訪問permission串有許可權,否則 沒有許可權,丟擲異常。

    shiro與專案整合開發

shiro與spring web專案整合

shiro與springweb專案整合在“基於url攔截實現的工程”基礎上整合,基於url攔截實現的工程的技術架構是springmvc+mybatis,整合注意兩點:

         1、shiro與spring整合

         2、加入shiro對web應用的支援

取消原springmvc認證和授權攔截器

去掉springmvc.xml中配置的LoginInterceptor(登入攔截器)和PermissionInterceptor攔截器。

web.xml中配置shiro的filter

在web系統中,shiro也通過filter進行攔截。filter攔截後將操作權交給spring中配置的filterChain(過慮鏈兒)

shiro提供很多filter。在web.xml中配置filter

    <!-- shiro過慮器,DelegatingFilterProxy通過代理模式將spring容器中的bean和filter關聯起來 -->
	<filter>
		<filter-name>shiroFilter</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
		<!-- 設定true由servlet容器控制filter的生命週期 -->
		<init-param>
			<param-name>targetFilterLifecycle</param-name>
			<param-value>true</param-value>
		</init-param>
		<!-- 設定spring容器filter的bean id,如果不設定則找與filter-name一致的bean-->
		<init-param>
			<param-name>targetBeanName</param-name>
			<param-value>shiroFilter</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" />
		<!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求此地址將由formAuthenticationFilter進行表單認證 -->
		<property name="loginUrl" value="/login.action" />
		<property name="unauthorizedUrl" value="/refuse.jsp" />
		<!-- 過慮器鏈定義,從上向下順序執行,一般將/**放在最下邊 -->
		<property name="filterChainDefinitions">
			<value>
				<!-- 退出攔截,請求logout.action執行退出操作 -->
				/logout.action = logout
				<!-- 無權訪問頁面 -->
				/refuse.jsp = anon
				<!-- roles[XX]表示有XX角色才可訪問 -->
				/item/list.action = roles[item],authc
                                <!--對靜態資源設定逆名訪問-->
				/js/** anon
				/images/** anon
				/styles/** anon
				/validatecode.jsp anon
				/item/* authc
				<!-- user表示身份認證通過或通過記住我認證通過的可以訪問 -->
				/** = authc
			</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>

securityManager:這個屬性是必須的。

loginUrl:沒有登入認證的使用者請求將跳轉到此地址進行認證,不是必須的屬性,不輸入地址的話會自動尋找專案web專案的根目錄下的”/login.jsp”頁面。

unauthorizedUrl:沒有許可權預設跳轉的頁面

登陸

原理

使用FormAuthenticationFilter過慮器實現 ,原理如下:

將使用者沒有認證時,請求loginurl進行認證,使用者身份和使用者密碼提交資料到loginurl。FormAuthenticationFilter攔截住取出request中的username和password(兩個引數名稱是可以配置的)
FormAuthenticationFilter呼叫realm傳入一個token(username和password)
realm認證時根據username查詢使用者資訊(在Activeuser中儲存,包括 userid、usercode、username、menus)。
如果查詢不到,realm返回null,FormAuthenticationFilter向request域中填充一個引數(記錄了異常資訊)

登陸程式碼實現

        // 使用者登陸提交
        // 登入提交地址和applicationContext-shiro.xml中配置的loginurl一致
	@RequestMapping("/login")
	public String loginsubmit(Model model, HttpServletRequest request)
			throws Exception {

		// shiro在認證過程中出現錯誤後將異常類路徑通過request返回
		// shiroLoginFailure就是shiro異常類的許可權名
		String exceptionClassName = (String) request
				.getAttribute("shiroLoginFailure");
		// 根據shiro返回的異常類路徑判斷,丟擲指定異常資訊
		if(exceptionClassName!=null){
			if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
				// 最終會拋給異常處理類
				throw new CustomException("賬號不存在");
			} else if (IncorrectCredentialsException.class.getName().equals(
					exceptionClassName)) {
				throw new CustomException("使用者名稱/密碼錯誤");
			} else if("randomCodeError".equals(exceptionClassName)){
				throw new CustomException("驗證碼錯誤");
			} else{
				throw new Exception();//最終在異常處理器生成未知錯誤
			}
		}
		// 此方法不處理登陸成功(認證成功),shiro認證成功會自動跳轉到上一個請求路徑
		// 登入失敗還到login頁面
		return "login";

退出:

由於使用shiro的sessionManager,不用開發退出功能,使用shiro的logout攔截器即可。使用LogoutFilter,不用我們去實現退出,只要去訪問一個退出的url(該url是可以不存在),由LogoutFilter攔截住,清除session。

在applicationContext-shiro.xml配置LogoutFilter:

<!-- 退出攔截,請求logout.action執行退出操作 -->
/logout.action = logout

可以刪除原來的logout的controller方法程式碼。

認證資訊在頁面顯示

1、認證後用戶選單在首頁顯示

2、認證後用戶的資訊在頁頭顯示

由於session由shiro管理,需要修改首頁的controller方法,將session中的資料通過model傳到頁面。

        //系統首頁
	@RequestMapping("/first")
	public String first(Model model)throws Exception{
		
		//主體,從shiro的session中獲取activeUser
		Subject subject = SecurityUtils.getSubject();
		//身份
		ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
		// 通過model傳到頁面
		model.addAttribute("activeUser", activeUser);
		return "/first";
	}

無許可權refuse.jsp

如果授權失敗,跳轉到refuse.jsp,需要在spring容器中配置:

當用戶無操作許可權,shiro將跳轉到refuse.jsp頁面。

<property name="unauthorizedUrl" value="/refuse.jsp" />

問題總結

1、在applicationContext-shiro.xml中配置過慮器連結,需要將全部的url和許可權對應起來進行配置,比較發麻不方便使用。

2、每次授權都需要呼叫realm查詢資料庫,對於系統性能有很大影響,可以通過shiro快取來解決。

認證

修改realm的doGetAuthenticationInfo,從資料庫查詢使用者資訊,realm返回的使用者資訊中包括 (md5加密後的串和salt),實現讓shiro進行雜湊串的校驗。

新增憑證匹配器

資料庫中儲存到的md5的雜湊值,在realm中需要設定資料庫中的雜湊值它使用雜湊演算法 及雜湊次數,讓shiro進行雜湊對比時和原始資料庫中的雜湊值使用的演算法 一致。

新增憑證匹配器實現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 {
	// 注入sevice
	@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) {
			return null;
		}
		// 根據使用者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;
	}
}

授權:

修改realm的doGetAuthorizationInfo,從資料庫查詢許可權資訊。

使用註解式授權方法。

使用jsp標籤授權方法。

1,修改doGetAuthorizationInfo從資料庫查詢許可權

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 AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {		
		//身份資訊
                //從 principals獲取主身份資訊
		//將getPrimaryPrincipal方法返回值轉為真實身份型別(在上邊的doGetAuthenticationInfo認證通過填充到SimpleAuthenticationInfo中身份型別),
		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;		
	}
}

對controller開啟AOP

對系統中類的方法給使用者授權,建議在controller層進行方法授權。

在springmvc.xml中配置shiro註解支援,可在controller方法中使用shiro註解配置許可權:

在springmvc.xml中配置:

        <!-- 開啟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方法新增許可權(item:query

        // 查詢商品列表
	@RequestMapping("/queryItem")
	@RequiresPermissions("item:query")
	public ModelAndView queryItem() throws Exception {

上邊程式碼@RequiresPermissions("item:query")表示必須擁有“item:query”許可權方可執行。

同理,商品修改controller方法新增許可權(item:update):

        @RequestMapping(value = "/editItem")
	@RequiresPermissions("item:update")
	public String editItem(@RequestParam(value = "id", required = true) Integer id, Model model) throws Exception
        // 商品修改提交
	@RequestMapping("/editItemSubmit")
	@RequiresPermissions("item:update")
	public String editItemSubmit(@ModelAttribute("item") Items items,BindingResult result,
			MultipartFile pictureFile,Model model,HttpServletRequest request)
			throws Exception

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"/>     顯示使用者身份中的屬性值

jsp頁面新增標籤

如果有商品修改許可權頁面顯示“修改”連結。

<shiro:hasPermission name="item:update">
    <a href="${pageContext.request.contextPath }/item/editItem.action?id=${item.id}">修改</a>
</shiro:hasPermission>

授權測試

當呼叫controller的一個方法,由於該方法加了@RequiresPermissions("item:query"),shiro呼叫realm獲取資料庫中的許可權資訊,看"item:query"是否在許可權資料中存在,如果不存在就拒絕訪問,如果存在就授權通過。

當展示一個jsp頁面時,頁面中如果遇到<shiro:hasPermission name="item:update">,shiro呼叫realm獲取資料庫中的許可權資訊,看item:update是否在許可權資料中存在,如果不存在就拒絕訪問,如果存在就授權通過。

問題:只要遇到註解或jsp標籤的授權,都會呼叫realm方法查詢資料庫,需要使用快取解決此問題。

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表示需要認證(登入)才能使用,FormAuthenticationFilter是表單認證,沒有引數<