前言
Apache Shiro 是一個框架,可用於身份驗證和授權。雖然這兩個術語代表的是不同的含義,但出於它們在應用程序安全性方面各自的角色考慮,它們有時會被交換使用。
身份驗證 指的是驗證用戶的身份。在驗證用戶身份時,需要確認用戶的身份的確如他們所聲稱的那樣。在大多數應用程序中,身份驗證是通過用戶名和密碼的組合完成的。只要用戶選擇了他人很難猜到的密碼,那麼用戶名和密碼的組合通常就足以確立身份。但是,還有其他的身份驗證方式可用,比如指紋、證書和生成鍵。
一旦身份驗證過程成功地建立起身份,授權 就會接管以便進行訪問的限制或允許。 所以,有這樣的可能性:用戶雖然通過了身份驗證可以登錄到一個系統,但是未經過授權,不準做任何事情。還有一種可能是用戶雖然具有了某種程度的授權,卻並未經過身份驗證。
在爲應用程序規劃安全性模型時,必須處理好這兩個元素以確保系統具有足夠的安全性。身份驗證是應用程序常見的問題(特別是在只有用戶和密碼組合的情況下),所以讓框架來處理這項工作是一個很好的做法。合理的框架可提供經過測試和維護的優勢,讓您可以集中精力處理業務問題,而不是解決其解決方案已經實現的問題。
Apache Shiro 提供了一個可用的安全性框架,各種客戶機都可將這個框架應用於它們的應用程序。本文中的這些例子旨在介紹 Shiro 並着重展示對用戶進行身份驗證的基本任務。
本文只針對Jeesite中shiro的用法進行整理,不會包括shiro環境配置和搭建等內容。
Jeesite中的shiro
2.1 spring-context-shiro.xml
spring-context-shiro.xml是shiro的主配置文件,配置信息是重點主要是安全認證過濾器的配置。它規定哪些url需要進行哪些方面的認證和過濾
<!-- 安全認證過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="p" value="${adminPath}/login" /> <property name="successUrl" value="${adminPath}" /> <property name="filters"> <map> <entry key="authc" value-ref="formAuthenticationFilter"/> </map> </property> <property name="filterChainDefinitions"> <value> /static/** = anon /userfiles/** = anon ${adminPath}/login = authc ${adminPath}/logout = logout ${adminPath}/** = user </value> </property> </bean>
shiroFilter是shiro的安全認證過濾器,其中,
securityManager:指定一個負責管理的bean,這個新的bean在接下來會定義,其中包含了認證的主要邏輯。
loginUrl:沒有登錄的用戶請求需要登錄的頁面時自動跳轉到登錄頁面,不是必須的屬性,不輸入地址的話會自動尋找項目web項目的根目錄下的”/login.jsp”頁面。
successUrl:登錄成功默認跳轉頁面,不配置則跳轉至”/”。如果登陸前點擊的一個需要登錄的頁面,則在登錄自動跳轉到那個需要登錄的頁面。不跳轉到此。
unauthorizedUrl:沒有權限默認跳轉的頁面。
map中的entry指定了authc權限所對應的過濾器實體
而屬性中的filterChainDefinitions則詳細規定啦不同的url的對應權限
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是授權過濾器
<!-- 定義 Shiro 主要業務對象 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- <property name="sessionManager" ref="sessionManager" /> --> <property name="realm" ref="systemAuthorizingRealm" /> <property name="cacheManager" ref="shiroCacheManager" /> </bean>
這部分代碼定義了securitymanager的主要屬性的實體,systemAuthorizingRealm和shiroCacheManager都是自己實現的,分別用於進行驗證管理和cache管理。從配置文件能看出,配置文件指定了作爲安全邏輯的幾個結構,除了這兩部分,還包括formAuthenticationFilter。其中realm和filter在com.thinkgem.jeesite.modules.sys.security包裏實現。
2.2 FormAuthenticationFilter
從可見都邏輯上講,FormAuthenticationFilter類是用戶驗證時所接觸的第一個類。
當用戶登錄任意界面時,shiro會對當前狀態進行檢查。如果發現需要登錄,則會自動跳轉到配置文件裏loginUrl屬性所指定的url中。
而這一url又被指定爲authc權限,即需要驗證。接着,authc的filter被指定爲formAuthenticationFilter,因此login頁面所提交的信息被改filter截獲進行處理。
其中的核心邏輯是createToken函數:
protected AuthenticationToken createToken(S2ervletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); if (password==null){ password = ""; } boolean rememberMe = isRememberMe(request); String host = getHost(request); String captcha = getCaptcha(request); return new U
UsernamePasswordToken是security包裏的第四個類,它繼承自shiro的同名類,用於shiro處理中的參數傳遞。createtoken函數接受到login網頁所接受的表單,生成一個token傳給下一個類處理。
函數中的rememberme以及captcha分別表示的是記住用戶功能和驗證碼功能,這部分也是shiro自身攜帶,我們並不需要修改。filter是通過aop的方式結合到系統裏的,因此並沒有具體的接口實現。
2.3 SystemAuthorizingRealm
shiro的最終處理都將交給Real進行處理。因爲在Shiro中,最終是通過Realm來獲取應用程序中的用戶、角色及權限信息的。通常情況下,在Realm中會直接從我們的數據源中獲取Shiro需要的驗證信息。可以說,Realm是專用於安全框架的DAO.
Realm中有個參數是systemService,這個便是spring的具體業務邏輯,其中也包含了具體的DAO,正是在這個部分,shiro與spring的接口具體的結合了起來。
realm當中有兩個函數特別重要,分別是用戶認證函數和授權函數。
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){ // 判斷驗證碼 Session session = SecurityUtils.getSubject().getSession(); String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE); if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){ throw new CaptchaException("驗證碼錯誤."); } } User user = getSystemService().getUserByLoginName(token.getUsername()); if (user != null) { byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16)); return new SimpleAuthenticationInfo(new Principal(user), user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName()); } else { return null; } }
以上便是用戶認證函數,中間關於驗證碼檢測的部分我們暫且不表,其最核心的語句是
函數的參數AuthenticationToken便是filter所截獲並生成的token實例,其內容是login表單裏的內容,通常是用戶名和密碼。在前一句話裏,getSystemService取到了systemService實例,該實例中又含有配置好的userDAO,能夠直接與數據庫交互。因此將用戶id傳入,取出數據庫裏所存有的user實例,接着在SimpleAuthenticationInfo生成過程中分別以user實例的用戶名密碼,以及token裏的用戶名密碼做爲參數,驗證密碼是否相同,以達到驗證的目的。
doGetAuthorizationInfo函數是授權的函數,其具體的權限是在實現類中以annotation的形式指派的,它負責驗證用戶是否有權限訪問。詳細的細節容我之後添加。
2.3 LoginController
LoginController是本該實現登錄認證的部分。由於shiro的引入和AOP的使用,jeesite中的LoginController只處理驗證之後的部分。
如果通過驗證,系統中存在user實例,則返回對應的主頁。否則重新定位於login頁面。
2.4 annotation
常用的annotation主要如下:
@RequiresAuthentication
要求當前Subject 已經在當前的session 中被驗證通過才能被註解的類/實例/方法訪問或調用。
驗證用戶是否登錄,等同於方法subject.isAuthenticated() 結果爲true時。@RequiresUser
需要當前的Subject 是一個應用程序用戶才能被註解的類/實例/方法訪問或調用。要麼是通過驗證被確認,或者在之前session 中的'RememberMe'服務被記住。
驗證用戶是否被記憶,user有兩種含義:一種是成功登錄的(subject.isAuthenticated() 結果爲true);另外一種是被記憶的(subject.isRemembered()結果爲true)。@RequiresGuest
要求當前的Subject 是一個“guest”,也就是他們必須是在之前的session中沒有被驗證或記住才能被註解的類/實例/方法訪問或調用。
驗證是否是一個guest的請求,與@RequiresUser完全相反。
換言之,RequiresUser == !RequiresGuest。此時subject.getPrincipal() 結果爲null.@RequiresRoles
要求當前的Subject 擁有所有指定的角色。如果他們沒有,則該方法將不會被執行,而且AuthorizationException 異常將會被拋出。例如:@RequiresRoles("administrator")
或者@RequiresRoles("aRoleName");
void someMethod();
如果subject中有aRoleName角色纔可以訪問方法someMethod。如果沒有這個權限則會拋出異常AuthorizationException。@RequiresPermissions
要求當前的Subject 被允許一個或多個權限,以便執行註解的方法,比如:
@RequiresPermissions("account:create")
或者@RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();