手摸手教你SSM框架整合Shiro後的開發html
前面,咱們學習了Shiro實現權限管理之表結構設計以及JQuery-Ztree.js使用範例 ,接下來就詳細介紹一下SSM框架整合Shiro框架後的開發。一樣推薦你們參看張開濤老師的 跟我學Shiro ,或者能夠看個人筆記:Shiro實現受權、Shiro實現身份認證。java
若是你對SSM框架的整合不是很熟悉,你或許能夠參看個人這個項目SSM框架整合。git
下面咱們就開始實現一個SSM+Shiro的權限管理項目吧!github
<!--more-->web
測試環境算法
IDEA + Tomcat8 + Mavenspring
初始化數據庫,請參考/db中的代碼sql
導入Shiro框架須要的依賴:數據庫
shiro-core-1.3.2.jar shiro-ehcache-1.3.2.jar shiro-quartz-1.3.2.jar shiro-spring-1.3.2.jar shiro-web-1.3.2.jarapache
其餘依賴請參看項目中的pom.xml 文件
搭建SSM框架的過程這裏再也不詳細說了,能夠參看個人SSM框架整合案例
<br/>
與Spring集成:
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <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>
和SpringMVC框架相似,Shiro框架也須要在web.xml中配置一個過濾器。DelegatingFilterProxy
會自動到Spring容器中name爲shiroFilter
的bean,而且將全部Filter的操做都委託給他管理。
這就要求在Spring配置中必須注入這樣一個這樣的Bean:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"></bean>
此處bean的id
和web.xml
中Shiro過濾器的名稱<filter-name>
必須是相同的,不然Shiro會找不到這個Bean。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- Shiro的安全管理器,全部關於安全的操做都會通過SecurityManager --> <property name="securityManager" ref="securityManager"/> <!-- 系統認證提交地址,若是用戶退出即session丟失就會訪問這個頁面 --> <property name="loginUrl" value="/login.jsp"/> <!-- 權限驗證失敗跳轉的頁面,須要配合Spring的ExceptionHandler異常處理機制使用 --> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <!-- 自定義的過濾器鏈,從上向下執行,通常將`/**`放到最下面 --> <property name="filterChainDefinitions"> <value> <!-- 靜態資源不攔截 --> /static/** = anon /lib/** = anon /js/** = anon <!-- 登陸頁面不攔截 --> /login.jsp = anon /login.do = anon <!-- Shiro提供了退出登陸的配置`logout`,會生成路徑爲`/logout`的請求地址,訪問這個地址即會退出當前帳戶並清空緩存 --> /logout = logout <!-- user表示身份經過或經過記住我經過的用戶都能訪問系統 --> /index.jsp = user <!-- `/**`表示全部請求,表示訪問該地址的用戶是身份驗證經過或RememberMe登陸的均可以 --> /** = user </value> </property> </bean> <!-- 基於Form表單的身份驗證過濾器 --> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password"/> <property name="loginUrl" value="/login.jsp"/> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> </bean> <!-- Realm實現 --> <bean id="userRealm" class="cn.tycoding.realm.UserRealm"></bean> </bean>
上面配置文件的核心處就是Shiro的web過濾器的配置,固然由於Shiro的全部涉及安全的操做都要通過DefaultWebSecurityManager
安全管理器,因此shiroFilter
首先就要將其交給SecurityManager
管理。loginUrl
是帳戶退出或者session丟失就跳轉的地址;unauthorizedUrl
是帳戶權限驗證失敗跳轉的地址,好比帳戶權限不夠等;而後就是過濾器鏈filterChainDefinitions
的配置,他和咱們以前配置的.ini
文件很是類似,其中主要就是配置資源的的攔截。Shiro提供了不少默認的攔截器,好比什麼驗證,受權等,這裏舉例幾個比較經常使用的默認攔截器:
<style> table th:first-of-type { width: 100px; } </style>
默認攔截器名 | 說明 |
---|---|
authc | 基於表單的攔截器,好比若用戶沒有登陸就會跳轉到loginUrl 的地址,其攔截的請求必須是經過登陸驗證的,即Subject.isAuthenticated() == true 的帳戶才能訪問 |
anon | 匿名攔截器,和authc 攔截器恰好做用相反。anon 配置的請求容許用戶爲登陸就等訪問,通常咱們配置登陸頁面和靜態CSS等資源是容許匿名訪問 |
logout | 退出攔截器,Shiro提供了一個退出的功能,配置了/logout = logout ,Shiro就會生成一個虛擬的映射路徑,當用戶訪問了這個路徑,Shiro會自動清空緩存並跳轉到loginUrl 頁面 |
user | 用戶攔截器,和authc 攔截器很相似,都是帳戶爲登陸的進行攔截並跳轉到loginUrl 地址;不一樣之處在於authc 容許帳戶必須是經過Subject.siAuthenticated() ==true 的;而user 不只容許登陸帳戶訪問,經過rememberMe登陸的用戶也能訪問 |
身份認證的流程
若是用戶爲登陸,將跳轉到loginUrl
進行登陸,登陸表單中,包含了兩個主要參數:用戶名username
、密碼password
(這兩個參數名稱不是固定的,可是要和FormAuthenticationFilter
表單過濾器的參數配置要對應)。
UsernamePasswordToken
傳入用戶名和密碼。subject.login(token)
進行登陸,SecurityManager會委託Authenticator
把相應的token傳給Realm,從Realm中獲取身份認證信息。@Controller public class LoginController { @RequestMapping("/login") public String login( @RequestParam(value = "username", required = false) String username, @RequestParam(value = "password", required = false) String password, Model model) { String error = null; if (username != null && password != null) { //初始化 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { //登陸,即身份校驗,由經過Spring注入的UserRealm會自動校驗輸入的用戶名和密碼在數據庫中是否有對應的值 subject.login(token); return "redirect:index.do"; }catch (Exception e){ e.printStackTrace(); error = "未知錯誤,錯誤信息:" + e.getMessage(); } } else { error = "請輸入用戶名和密碼"; } //登陸失敗,跳轉到login頁面,這裏不作登陸成功的處理,由 model.addAttribute("error", error); return "login"; } }
拓展
如上,當login()
映射方法獲得用戶輸入的用戶名和密碼後調用subject.login(token)
進行登陸,隨後就是經過Realm進行登陸校驗,若是登陸失敗就可能拋出一系列異常,好比UnknownAccountException
用戶帳戶不存在異常、IncorrectCredentialsException
用戶名或密碼錯誤異常、LockedAccountException
帳戶鎖定異常... 。
可能,你也看到有些示例中在Controller層中沒有處理登陸成功,而是在ShiroFilterFactoryBean
中配置successUrl
,不少博文中講到:若是登陸成功Shiro會自動跳轉到登陸前訪問的地址,若是找不到登陸前訪問的地址,就會跳轉到successUrl
中配置的地址;But,我在測試中並無看到這種特性,你們能夠研究一波。
與登陸認證相關的攔截器在前面spring-shiro-web
配置文件中已經講到了。主要是使用Shiro提供的默認攔截器配置請求資源資源的攔截和驗證,如:
<!-- 靜態資源不攔截 --> /static/** = anon /lib/** = anon /js/** = anon <!-- 登陸頁面不攔截 --> /login.jsp = anon /login.do = anon ...
運行項目,若是用戶沒有輸入用戶名和密碼或者輸入的用戶名或密碼有誤等,將會拋出異常並從新跳轉到loginUrl
地址上,若是正確輸入用戶名和密碼(數據庫中存在的)將跳轉到系統首頁index.do
。那麼:咱們在Controller僅僅調用了subject.login(token)
,Shiro是怎樣進行登陸驗證的呢?
那咱們就要分析一下自定義的Realm了:
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 權限校驗 */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); return authorizationInfo; } /** * 身份校驗 */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByName(username); if (user == null) { throw new UnknownAccountException(); //沒有找到帳號 } if (Boolean.TRUE.equals(user.getLocked())) { throw new LockedAccountException(); //帳號鎖定 } //交給AuthenticationRealm使用CredentialsMatcher進行密碼匹配 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUsername(), //用戶名 user.getPassword(), //密碼 getName() //realm name ); return authenticationInfo; } }
Shiro從Realm中獲取安全數據,咱們能夠自定義多個Realm實現,但都要在SecurityManager中定義。通常咱們自定義實現的Realm繼承AuthorizingRealm(受權)便可,它繼承了AuthenticatingRealm(身份驗證);因此自定義Realm通常存在兩個最主要的功能:1.身份驗證;2.權限校驗。
在用戶登陸後,Controller會接收到用戶輸入的用戶名和密碼,並調用subject.login(token)
進行登陸,實際上SecurityManager會委託Authenticator
調用自定義的Realm進行身份驗證。要知道,調用Realm傳入的並不直接是用戶名和密碼,而是在Controller中綁定了用戶名和密碼的Token對象,那麼你首先要清楚身份驗證中兩個重要的參數:
<style> table th:first-of-type { width: 100px; } </style>
屬性名稱 | 做用 |
---|---|
principals | 身份,主體的惟一標識,好比用戶名、郵箱等,若是你將用戶名和密碼傳給了Token對象,那麼在Token對象中就能getPrincipal 獲取這個標識 |
credentials | 證實、憑證。好比密碼、數字證書等。可是在Shiro等安全框架中,相似於密碼這種數據通常都是通過加密處理的,它肯能不僅僅是密碼的數據,後面講 |
瞭解了上述兩個參數後,下面天然是從token對象中調用token.getPrincipal()
獲取用戶名,而後調用Service層方法根據這個用戶名查詢數據庫中是否存在一個密碼與其對應,根據返回的User對象,最後經過Shiro提供的SimpleAuthenticationInfo
進行密碼匹配。SimpleAuthenticationInfo
存在多個構造方法:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {} public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {} public SimpleAuthenticationInfo(PrincipalCollection principals, Object credentials) {} public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCredentials, ByteSource credentialsSalt) {}
SimpleAuthenticationInfo
類提供了多個構造方法,可是通常而言咱們的密碼是通過加密的(後面講);如此Shiro會自動根據token中的用戶名和密碼與從數據庫中查詢到的數據進行匹配,若是匹配成功就登陸成功,否者就拋出異常。
註銷登陸就簡單不少了,在之前咱們都是手動寫一個請求映射方法,當用戶調用這個請求的時候,手動清空Session,可是在Shiro中,這些步驟都省略了,咱們只須要在配置文件Shiro的過濾器shiroFilter
中過濾器鏈filterChainDefinitions
中的<value>
標籤中配置這一行:
/logout = logout
便可。Shiro會根據這個配置生成一個虛擬的請求映射路徑,當用戶請求localhost:8080/logout
這個接口的時候,Shiro會自動清空Session,並跳轉到loginUrl
指定的地址。
常見的加密方式有不少,這裏咱們介紹Shiro中提供的一套散列算法加密方式。散列算法,是一種不可逆的算法(天然是要不可逆的,由於可逆的算法破解起來也很容易,因此不可逆的算法更安全),常見的散列算法如MD5,、SHA,可是咱們再網上看到不少破解MD5加密的網站,不是說散列算法是不可逆的嗎?爲何還存在那麼多破解密碼的網站?其實散列算法確實是不可逆的,即便是常見的MD5加密也是不可逆的加密方式,而網上的破解網站並非可以逆向算出這個加密密碼,而是經過大數據的方式得出來的,至關於,MD5解密的網站中存在一個很大的數據庫,裏面存放了用戶常見的加密密碼,而後當用戶再用此密碼解密時,再從數據庫中比對加密後的MD5密碼,若是存在就能獲得原密碼了。爲了不這種狀況,引入了鹽salt
的概念,若是能經過大數據的方式破解MD5的加密,但若是在加密的密碼中再添加一組數據進行混淆,破解起來就至關難了,由於添加的salt
只有咱們本身知道是什麼。
自定義一套散列算法:
SimpleHash()
構造方法,將算法名稱、用戶輸入的密碼、鹽值、迭代次數傳入。SimpleHash()
構造方法,Shiro能自動幫咱們對密碼進行加密,並調用實體類對象的setter
方法將密碼設置進去。@Component public class PasswordHelper { //實例化RandomNumberGenerator對象,用於生成一個隨機數 private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); //散列算法名稱 private String algorithName = "MD5"; //散列迭代次數 private int hashInterations = 2; //加密算法 public void encryptPassword(User user){ if (user.getPassword() != null){ //對user對象設置鹽:salt;這個鹽值是randomNumberGenerator生成的隨機數,因此鹽值並不須要咱們指定 user.setSalt(randomNumberGenerator.nextBytes().toHex()); //調用SimpleHash指定散列算法參數:一、算法名稱;二、用戶輸入的密碼;三、鹽值(隨機生成的);四、迭代次數 String newPassword = new SimpleHash( algorithName, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), hashInterations).toHex(); user.setPassword(newPassword); } } //getter/setter .... }
如上,在encryptPassword
中進行了核心的密碼加密過程,咱們只須要調用SimpleHash()
傳入須要加密的參數便可,可是在這裏你應該會注意到兩個地方:user.setSalt()
和user.getCredentialsSalt()
。 其實,在實體類中咱們的肯定義了一個屬性private String salt;
,這裏調用的setSalt()
正是向其中設置RandomNumberGenerator
生成的隨機數做爲鹽值;可是又矛盾了,爲何還存在一個getCredentialsSalt()
方法?
那麼咱們看一下SimpleHash
的構造方法:
public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {}
其中也須要一個參數salt
。可是,要注意此salt非彼salt;咱們先看一下User實體類中定義的getCredentialsSalt()
方法:
private String salt; //鹽 public String getCredentialsSalt() { return username + salt; } //getter/setter...
意義就是指定以後要使用的鹽值salt
其實是username
和salt
的組合體,可是你確定好奇,爲何又定義getCredentialsSalt()
呢? 要區分:setSalt()
是爲User實體了設置salt參數的值,salt的值本就是RandomNumberGenerator
生成的隨機數;可是getCredentialsSalt()
獲得的鹽值是用戶名+隨機數
,這個值最終成爲了SimpleHash
加密密碼的一個重要組成部分,那麼最終經過指定加密方式(這裏是MD5)加密的密碼由用戶名+隨機數+密碼
組合而得。
上面介紹了核心的加密流程,那麼如何使用?何時須要加密呢?
當然是在建立新用戶的時候加密用戶密碼了,那麼咱們來看下建立用戶的Service層:
public void create(User user) { //加密密碼 passwordHelper.encryptPassword(user); userDao.create(user); }
建立用戶時,要調用passwordHelper
的encryptPassword()
方法對傳入的User對象進行密碼加密和設定鹽值處理。那麼在數據庫中保存的數據就如:
除了建立用戶,更新用戶數據的時候也要從新加密密碼(只要更新了User表的用戶名或密碼)都必須調用encryptPassword()
從新加密密碼和設置鹽值,由於最終存在數據庫表中的密碼是用戶名+密碼+鹽值
。
public void update(User user) { //加密密碼 passwordHelper.encryptPassword(user); userDao.update(user); }
上面講了半天的加密過程,下面說一下解密實現。以前已經說過,散列算法是不可逆的,因此一旦密碼被加密是沒法算出來的,可是咱們能夠用另一種方式:比對。就是將散列算法的加密方式傳給Realm,當用戶登陸系統時,獲取用戶輸入的密碼根據已定義的加密方式對此密碼進行加密,而後交給SimpleAuthenticationInfo
將用戶登陸輸入的加密密碼和數據庫中根據username獲得的加密密碼進行比對,若是比對成功就證實你的登陸密碼是正確的,從而實現解密。
那麼應該怎麼實現?很簡單,在Realm中咱們應該調用SimpleAuthenticationInfo
的這個構造方法:
public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {}
那麼咱們要更改Realm中的SimpleAuthenticationInfo
的這個實現:
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUsername(), //用戶名 user.getPassword(), //密碼 ByteSource.Util.bytes(user.getCredentialsSalt()), //salt=username+salt getName() //realm name );
若是使用了散列算法進行密碼加密和驗證服務,你必須在Spring配置文件中注入credentialsMatcher
來實現密碼驗證服務。
<bean id="credentialsMatcher" class="cn.tycoding.credentials.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean>
建立一個RetryLimitHashedCredentialsMatcher
類,繼承HashedCredentialsMathcer
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){} public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {} }
這樣就能獲取到加密密碼的鹽值,而後SimpleAuthenticationInfo
會結合這個鹽值進行密碼比對實現解密。
<br/>
受權,即賦予用戶必定的操做權限,這時,就該參考一下項目的表設計了: Shiro實現權限管理系統之表結構設計 。結合數據庫的表設計咱們彷佛就清楚了爲何那樣設計表,根據什麼進行權限校驗和受權,想必你也有一些思路了。
在受權中須要瞭解幾個關鍵對象:
<style> table th:first-of-type { width: 100px; } </style>
對象名稱 | 做用 |
---|---|
主體(Subject) | 即表明當前登陸的用戶 |
資源(Resource) | 即用戶登陸成功後容許訪問的東西,好比某個頁面,某個文件;它能夠精確到某個按鈕等.. |
權限(Permission) | 即表明用戶操做系統功能的權利,若是擁有了這個權限才能操做該功能,和資源關聯,有權限就意味着有訪問資源的權利 |
角色(Role) | 表明了操做(資源)集合,能夠理解爲權限的集合,和權限關聯,角色對應的權限,權限關聯着資源 |
因此,咱們要清楚:用戶和角色間是一對多的關係;角色和權限是多對多的關係;權限和資源是多對多的關係。可是在咱們設計的表:Shiro實現權限管理系統之表結構的設計中,我並無設置單獨設置資源表,而是僅用了權限表。 固然你能夠再寫一個資源表(Resource),創建權限和資源間的關係,這樣權限管理能精確到對每一個按鈕的管理。
實現受權前,首先,用戶得擁有權限,那麼就要創建用戶-角色的關係、角色-權限的關係;具體操做步驟請參看個人這篇博文:Shiro實現權限管理系統之表結構設計中介紹的sql。
Shiro提供了多種受權方式,好比咱們能夠看subject實例擁有的受權方法:
從方法名上就能看出subject
提供了哪些受權方式;那麼這裏咱們不講用subject
實例受權的方式,咱們講一種更簡便的方式:Shiro註解、Shiro-Spring註解的方式。
Shiro結合Spring提供了相應的註解用戶權限控制,咱們先來看一下都有哪些註解:
<style> table th:first-of-type { width: 400px; } </style>
註解名稱 | 解釋 |
---|---|
@RequiresAuthentication | 表示當前Subject已經經過login身份驗證;即Subject.isAuthenticated() == true;不然就攔截 |
@RequiresUser | 表示當前Subject已經經過login身份驗證或經過記住我登陸;不然就攔截 |
@RequiresGuest | 表示當前Subject沒有身份驗證或經過記住我登陸過,便是遊客身份 |
@RequiresRoles(value ={"admin", "user"}, logical=Logical.AND) | 表示當前Subject須要同時(由Logical.AND體現)擁有admin和user角色;不然攔截 |
@RequiresPermissions(vale={"user:a","user:b"}, logical=Logical.OR) | 表示當前Subject須要擁有user:a 或者(由Logical.OR體現)user:b 角色;不然攔截 |
由於Shiro的某些權限註解須要AOP的功能進行判斷,因此須要開啓AOP功能的支持;項目中使用了Spring AOP,Shiro提供了Spring AOP的集成用於權限註解的解析和驗證。 在SpringMVC的配置文件中開啓Shiro Spring AOP 的支持:
<aop:config proxy-target-class="true"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
建立用戶的方法上添加權限註解:
@ResponseBody @RequestMapping("/create") @RequiresRoles(value={"admin","personnel-resource"}, logical = Logical.OR) public Result create(@RequestBody User user) {}
刪除用戶信息的方法上添加權限註解:
@ResponseBody @RequestMapping("/delete") @RequiresRoles(value = {"admin", "personnel-resource"}, logical = Logical.OR) public Result delete(@RequestParam("id") Long id){}
根據用戶名查找其角色的方法上添加權限註解:
@ResponseBody @RequestMapping("/findRoles") @RequiresRoles(value = {"admin"}, logical = Logical.OR) @RequiresPermissions(value = {"role:view", "role:*"}, logical = Logical.OR) public List<Role> findRoles(String username) {}
...
Shiro提供了JSTL標籤用於在JSP/GSP頁面進行權限控制;首先須要導入標籤庫:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<style> table th:first-of-type { width: 200px; } </style>
標籤名稱 | 做用 |
---|---|
<shiro:guest> |
用戶沒有身份驗證時顯示相應的信息,即遊客訪問信息 |
<shiro:user> |
用戶已經身份驗證、記住我登陸後顯示相應的信息,未登陸用戶將會攔截 |
<shiro:authenticated> |
用戶已經身份驗證經過,即Subject.isAuthenticated() == true;未登陸或記住我登陸的都會攔截 |
<shiro:notAuthenticated> |
用戶已經身份驗證經過,可是Subject.isAuthenticated() == false,便可能是經過記住我登陸的 |
<shiro:principal> |
顯示用戶身份信息,默認調用Subject.getPrincipal()獲取用戶登陸信息 |
<shiro:hasRole> |
如:<shiro:hasRole name="admin"> ,若是當前Subject有admin角色就顯示數據,相似於@RequiresRoles() 註解;不然就攔截 |
<shiro:hasAnyRole> |
如:<shiro:hasAnyRole name="admin,user"> ,若是當前Subject有admin或user角色就顯示數據,相似於@RequireRoles(Logical=Logical.OR) 註解;不然將就攔截 |
<shiro:lackRole> |
若是當前Subject沒有角色就顯示數據 |
<shiro:hasPermission> |
如:<shiro:hasPermission name="user:create"> ,若是當前Subject有user:create 權限,就顯示數據;不然就攔截 |
<shiro:lacksPermission> |
如:<shiro:lacksPermission name="user:create"> ,若是當前Subject沒有user:create 權限,就顯示數據;不然攔截 |
<br/>
會話:用戶登陸後直至註銷(Session丟失)前稱爲一次會話,即用戶訪問應用時保持的鏈接關係,能夠保證在屢次交互中應用可以識別出當前訪問的用戶是誰,且可在屢次交互中保存一些數據。常見的應用實例如:登陸時記住個人功能、單點登陸的功能...
Shiro提供了會話管理器:sessionManager
,管理着全部會話的建立、維護、刪除、等工做。在web環境中使用Shiro的會話管理器,咱們須要在Spring的配置文件中注入DefaultWebSessionManager
:
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 設置全局會話過時時間:默認30分鐘 --> <property name="globalSessionTimeout" value="1800000"/> <!-- 是否啓用sessionIdCookie,默認是啓用的 --> <property name="sessionIdCookieEnabled" value="true"/> <!-- 會話Cookie --> <property name="sessionIdCookie" ref="sessionIdCookie"/> </bean> <!-- 會話Cookie模板 --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid"/> <!-- 若是設置爲true,則客戶端不會暴露給服務端腳本代碼,有助於減小某些類型的跨站腳本攻擊 --> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/><!-- maxAge=-1表示瀏覽器關閉時失效此Cookie --> </bean>
還要將sessionManager
注入到SecurityManager
中:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> <!-- 注入sessionManager --> <property name="sessionManager" ref="sessionManager"/> </bean>
Shiro也集成了緩存機制,例如Shiro提供了CachingRealm,提供了一些基礎的緩存實現。Shiro默認是禁用緩存的,首先咱們要開啓Shiro的緩存管理,在XML中進行以下配置:
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:other/ehcache.xml"/> </bean>
在自定義的Realm實現中配置緩存的實現:
<!-- Realm實現 --> <bean id="userRealm" class="cn.tycoding.realm.UserRealm"> <!-- 使用credentialsMatcher實現密碼驗證服務 --> <property name="credentialsMatcher" ref="credentialsMatcher"/> <!-- 是否啓用緩存 --> <property name="cachingEnabled" value="true"/> <!-- 是否啓用身份驗證緩存 --> <property name="authenticationCachingEnabled" value="true"/> <!-- 緩存AuthenticationInfo信息的緩存名稱 --> <property name="authenticationCacheName" value="authenticationCache"/> <!-- 是否啓用受權緩存,緩存AuthorizationInfo信息 --> <property name="authorizationCachingEnabled" value="true"/> <!-- 緩存AuthorizationInfo信息的緩存名稱 --> <property name="authorizationCacheName" value="authorizationCache"/> </bean>
在resources/other/
文件夾下建立配置文件ehcache.xml
:
<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>
設置SecurityManager的cacheManager:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> ... </bean>
在Shiro會話管理時咱們就講到會話的功能,例如:Shiro實現了RememberMe記住個人功能,當用戶在登陸頁面中勾選了記住我,再瀏覽器關閉後再次訪問系統發現是能夠直接登陸的;可是若是沒有實現這一功能,Shiro默認設置瀏覽器關閉後當即清除緩存,那麼再次打開瀏覽器要從新進行登陸。
RememberMe和使用Subject.login(token)
登陸是有所不一樣的,RememberMe是使用緩存Cookie的技術實現的登陸,在前面講到的一些權限註解中就說到了二者的區別。
在配置文件中寫入:
<!-- 會話Cookie模板 --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid"/> <!-- 若是設置爲true,則客戶端不會暴露給服務端腳本代碼,有助於減小某些類型的跨站腳本攻擊 --> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/><!-- maxAge=-1表示瀏覽器關閉時失效此Cookie --> </bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/><!-- 30天 --> </bean> <!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- cipherKey是加密rememberMe Cookie的密匙,默認AES算法 --> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean>
在sessionIdCookie
中設置maxAge=-1
表示瀏覽器關閉後即失效此Cookie在rememberMeCookie
中設置maxAge=2592000
表示記住此Cookie,保存30天。
在SecurityManager
中設置rememberMeManager
:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>
在登陸表單中添加一個checkbox:
<input type="checkbox" name="remember">請記住我
若是用戶勾選了這個複選框,點擊登陸按鈕提交後臺的參數中會多一個remember
參數,且值是on
(若是用戶沒有勾選,提交表單中就不存在這個參數);因此咱們修改Controller的登陸方法:
public String login( @RequestParam(value = "username", required = false) String username, @RequestParam(value = "password", required = false) String password, @RequestParam(value = "remember", required = false) String remember, Model model) { if (username != null && password != null) { //初始化 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); if (remember != null){ if (remember.equals("on")) { //說明選擇了記住我 token.setRememberMe(true); } else { token.setRememberMe(false); } }else{ token.setRememberMe(false); } try { //登陸,即身份校驗,由經過Spring注入的UserRealm會自動校驗輸入的用戶名和密碼在數據庫中是否有對應的值 subject.login(token); return "redirect:index.do"; }catch (Exception e){ e.printStackTrace(); error = "未知錯誤,錯誤信息:" + e.getMessage(); } } else { error = "請輸入用戶名和密碼"; } //登陸失敗,跳轉到login頁面 model.addAttribute("error", error); return "login"; }
建立一個authenticated.jsp
頁面,隨便寫一段文字此頁面必須是Subject.isAuthenticated() == true才能訪問
。而後在配置文件的filterChainDefinitions
中定義
/authenticated.jsp = authc
啓動項目,訪問localhost:8080/
自動跳轉到登陸頁面,勾選登陸表單中的記住我複選框,成功登陸系統後,關閉瀏覽器。再次打開瀏覽器,直接訪問localhost:8080/index.do
發現直接就能登陸系統。可是直接在瀏覽器中輸入localhost:8080/authenticated.jsp
發現確是不能訪問的,而且被攔截道登陸頁面,緣由就是rememberMe登陸系統並非經過Subject.login(token)
的方式,而authc
攔截器攔截的資源要求必須是Subject.isAuthenticated() == true
才能訪問。
從新啓動項目(或者註銷帳戶),從新進入登陸頁面,這次不勾選記住我複選框,成功進入系統後關閉瀏覽器,再次打開瀏覽器輸入localhost:8080/index.do
發現會再次被攔截跳轉到loginUrl
地址。
<br/>
若是你們有興趣,歡迎你們加入個人Java交流羣:671017003 ,一塊兒交流學習Java技術。博主目前一直在自學JAVA中,技術有限,若是能夠,會盡力給你們提供一些幫助,或是一些學習方法,固然羣裏的大佬都會積極給新手答疑的。因此,別猶豫,快來加入咱們吧!
<br/>
If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.