Shiro+Spring+SpringMVC+Mybatis+資料庫整合並進行登陸認證和授權詳細配置

a745233700發表於2018-08-02

本篇部落格將進行詳細介紹Shiro+Spring+SpringMVC+Mybatis+資料庫整合並進行登陸認證和授權詳細配置。

SSM的整合可以參考:https://blog.csdn.net/a745233700/article/details/81049763

下面主要介紹Shiro與SSM的整合和使用:

1、匯入Shiro需要的maven依賴:

		<!-- shiro -->
		<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-all -->
		<dependency>  
		        <groupId>org.apache.shiro</groupId>  
		        <artifactId>shiro-core</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-web</artifactId>
		        <version>1.2.3</version>
		    </dependency>
		    <dependency>
		        <groupId>org.apache.shiro</groupId>
		        <artifactId>shiro-spring</artifactId>
		        <version>1.2.3</version>
		    </dependency>

或者一次性匯入shiro的所有依賴:

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-all</artifactId>
	<version>1.2.3</version>
</dependency>

2、在web.xml檔案中配置shiro的filter攔截器:

在與Spring整合中,shiro也通過Filter進行攔截,但是攔截後的操作權交給spring中配置的filterChainDefinitions(過濾鏈)處理。

        <!-- shiro的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>

3、配置Shiro框架的相關配置:(applicationContext-shiro.xml檔案中)

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	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-4.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

	<!-- id屬性值要對應 web.xml中shiro的filter對應的bean -->
	 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager"></property>
		<!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證,請求地址將由formAuthenticationFilter進行表單認證 -->
		<property name="loginUrl" value="/login.action"></property>
		<!-- 認證成功統一跳轉到first.action,建議不配置,shiro認證成功會預設跳轉到上一個請求路徑 -->
		<!-- <property name="successUrl" value="/first.action"></property> -->
		<!-- 通過unauthorizedUrl指定沒有許可權操作時跳轉頁面,這個位置會攔截不到,下面有給出解決方法 -->
		<!-- <property name="unauthorizedUrl" value="/refuse.jsp"></property> -->
		
		<!-- 過濾器定義,從上到下執行,一般將/**放在最下面 -->
 		<property name="filterChainDefinitions">
			<value>
				<!-- 對靜態資源設定匿名訪問 -->
				/images/** = anon
				/js/** = anon
				/styles/** = anon
				/validatecode.jsp =anon
				<!-- 請求logout.action地址,shiro去清除session -->
				/logout.action = logout
				<!-- /**=authc 所有的url都必須通過認證才可以訪問 -->
				/** = authc
				
				<!-- /**=anon 所有的url都可以匿名訪問,不能配置在最後一排,不然所有的請求都不會攔截 -->
			</value>
		</property>
	</bean>
	
	<!-- 解決shiro配置的沒有許可權訪問時,unauthorizedUrl不跳轉到指定路徑的問題 -->
	<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
            </props>
        </property>
    </bean>
	
	<!-- securityManager安全管理器 -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="customRealm"></property>
	</bean>
	
	<!-- 配置自定義Realm -->
	<bean id="customRealm" class="com.zwp.shiro.CustomRealm">
		<!-- 將憑證匹配器設定到realm中,realm按照憑證匹配器的要求進行雜湊 -->
		<property name="credentialsMatcher" ref="credentialsMatcher"></property>
	</bean>

	<!-- 憑證匹配器 -->
	<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
		<!-- 加密演算法 -->
		<property name="hashAlgorithmName" value="md5"></property>
		<!-- 迭代次數 -->
		<property name="hashIterations" value="1"></property>
	</bean>
</beans>

4、配置自定義的Realm,重寫認證的方法:

//自定義的Realm
public class CustomRealm extends AuthorizingRealm{

	//注入service
	@Autowired
	private SysService sysService;
	// 設定realm的名稱
	@Override
	public void setName(String name) {
		super.setName("customRealm");
	}
	
	//認證的方法
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// token是使用者輸入的使用者名稱和密碼
		// 第一步從token中取出使用者名稱
		String userCode = (String) token.getPrincipal();

		// 第二步:根據使用者輸入的userCode從資料庫查詢使用者資訊
		SysUser sysUser = null;
		try {
			sysUser = sysService.findSysUserByUserCode(userCode);
		} catch (Exception e1) {
			e1.printStackTrace();
		}
		// 如果查詢不到返回null
		if(sysUser==null){
			return null;
		}
		
		// 從資料庫查詢到密碼
		String password = sysUser.getPassword();
		//鹽
		String salt = sysUser.getSalt();

		// 如果查詢到,返回認證資訊AuthenticationInfo
		//activeUser就是使用者身份資訊
		ActiveUser activeUser = new ActiveUser();
		activeUser.setUserid(sysUser.getId());
		activeUser.setUsercode(sysUser.getUsercode());
		activeUser.setUsername(sysUser.getUsername());
		//..
		
		//根據使用者id取出選單
		List<SysPermission> menus  = null;
		try {
			//通過service取出選單 
			menus = sysService.findMenuListByUserId(sysUser.getId());
		} catch (Exception e) {
			e.printStackTrace();
		}
		//將使用者選單,設定到activeUser
		activeUser.setMenus(menus);

		//將activeUser設定simpleAuthenticationInfo
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
				activeUser, password,ByteSource.Util.bytes(salt), this.getName());
		return simpleAuthenticationInfo;
	}

}

5、登陸:

(1)原理:使用FormAuthenticationFilter的過濾器實現。

①在使用者沒有認證時,請求loginUrl進行認證,使用者身份和使用者密碼提交到loginUrl;

②FormAuthenticationFilter攔截住,並取出request中的username和password(兩個引數名稱可以配置)

③FormAuthenticationFilter呼叫realm傳入一個token(即username和password);

④realm認證是根據username查詢使用者資訊,(並在ActiveUser中儲存,包括userid、usercode、username、menus等),如果查詢不到,realm返回null,FormAuthenticationFilter向requset域中填充一個引數(記錄了異常資訊)。

(2)登陸頁面:

由於FormAuthenticationFilter的使用者身份和密碼的input的預設值(username和password),修改頁面的賬號和密碼 的input的名稱為username和password。

(3)登陸程式碼實現:

@Controller
public class LoginController {

	//loginUrl指定的認證提交地址
	@RequestMapping("/login.action")
	public String login(HttpServletRequest request) throws Exception{
		
		//如果登陸失敗,則從request中獲取認證異常資訊,shiroLoginFailure就是shiro異常類的全限定名
		String exceptionClassName=request.getParameter("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{
				throw new Exception();
			}
		}
		
		//此方法不處理登陸成功(認證成功),如果shiro認證成功會自動跳轉到上一個請求路徑。
		
		//登陸失敗回到login頁面:
		return "login";
	}
}

(4)認證攔截器:/** = authu 

6、退出登陸:

在shiro中,不需要我們去實現退出登陸介面,只要去訪問一個退出的url(該url是可以不存在),由LogoutFilter攔截住,清除session。

7、認證資訊在頁面顯示:

在controller層取出使用者資訊,並設定在Attribute中,first.action會跳轉到首頁頁面。

	@RequestMapping("/first.action")
	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";
	}

、至此,Shiro+Spring+SpringMVC+Mybatis整合實現登陸認證和退出登陸的功能就完成了。啟動工程進行測試,在你沒有登陸認證成功之前,訪問專案中的任何路徑,都會被強制跳轉到loginUrl指定的路徑進行登陸提交。只有登陸認證成功,才可以訪問專案中的內容。

 

8、配置Shiro授權過濾器:使用PermissionsAuthorizationFilter

在applicationContext-shiro.xml中配置url所對應的許可權。

測試流程:

(1)在applicationContext-shiro.xml中配置filter規則

/permissionTest = perms[item:query]   //即訪問“/permissionTest“” 路徑需要“item:query”許可權

(2)使用者在認證通過後,請求“/permissionTest”,被PermissionsAuthorizationFilter攔截,發現需要“item:query”許可權;

(3)PermissionsAuthorizationFilter 呼叫 doGetAuthorizationInfo 獲取資料庫中的正確許可權並返回;

(4)PermissionsAuthorizationFilter對“item:query”和從realm中獲取的許可權進行對比,如果“item:query”在realm返回的許可權列表中,授權通過。

9、建立授權失敗頁面refuse.jsp,並配置自動跳轉:(記錄一個在此處遇到的小問題)

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

(1)授權失敗,頁面沒有自動跳轉:

 在一開始,使用上面這種方法進行配置,但是配置完之後,發現授權失敗之後,頁面並沒有自動跳轉,而是直接丟擲異常。

2)原因:通過檢視Shiro的原始碼:

private void applyUnauthorizedUrlIfNecessary(Filter filter) {
        String unauthorizedUrl = getUnauthorizedUrl();
        if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
            AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
            //only apply the unauthorizedUrl if they haven't explicitly configured one already:
            String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
            if (existingUnauthorizedUrl == null) {
                authzFilter.setUnauthorizedUrl(unauthorizedUrl);
            }
        }
    }

 發現是因為shiro原始碼中判斷了filter是否為AuthorizationFilter,只有perms,roles,ssl,rest,port才是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl設定後不起作用。

(3)解決方法:異常全路徑做key,錯誤頁面做value。

	<!-- 解決shiro配置的沒有許可權訪問時,unauthorizedUrl不跳轉到指定路徑的問題 -->
	<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
            </props>
        </property>
    </bean>

10、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是表單認證,沒有引數

perms:例子/admins/user/**=perms[user:add:*],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個引數時必須每個引數都通過才通過,想當於isPermitedAll()方法。

user:例如/admins/user/**=user沒有參數列示必須存在使用者, 身份認證通過或通過記住我認證通過的可以訪問,當登入操作時不做檢查。

11、授權:重寫自定義realm的doGetAuthorizationInfo方法,從資料庫查詢許可權資訊。

通常使用註解式授權方法和Jsp標籤授權方法。

//自定義的Realm
public class CustomRealm extends AuthorizingRealm{

	//注入service
	@Autowired
	private SysService sysService;
	// 設定realm的名稱
	@Override
	public void setName(String name) {
		super.setName("customRealm");
	}
	
	//授權的方法
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		
		//從principals獲取主身份資訊
		//將getPrimaryPrincipal方法返回值轉為真實身份型別(在上邊doGetAuthenticationInfo認證通過填充到SimpleAuthenticationInfo中的身份型別)
		ActiveUser activeUser=(ActiveUser)principals.getPrimaryPrincipal();
		
		//根據身份資訊獲取許可權資訊:從資料庫獲取到許可權資料
		List<SysPermission> permissionList = null;
		try{
			permissionList = sysService.findPermissionListByUserId(activeUser.getUserid());
		}catch (Exception e) {
			e.printStackTrace();
		}
		
		//單獨定一個集合物件
		List<String> permissions = new ArrayList<String>();
		if(permissionList!=null){
			for(SysPermission sysPermission:permissionList){
				//將資料庫中許可權標籤符放入集合
				permissions.add(sysPermission.getPercode());
			}
		}
	
		//查到許可權資料,返回授權資訊(要包括上邊的permissions)
		SimpleAuthorizationInfo simpleAuthorizationInfo =new SimpleAuthorizationInfo();
		simpleAuthorizationInfo.addStringPermissions(permissions);//這裡新增使用者有的許可權列表
		simpleAuthorizationInfo.addRole("manager");//這裡新增使用者所擁有的角色
		
		return simpleAuthorizationInfo;
	}
}

12、授權:開啟controller類的aop支援:

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

在springmvc.xml檔案中配置:

	<!-- 使用註解驅動:自動配置處理器對映器與處理器介面卡 -->
	<mvc:annotation-driven />
	<!-- 開啟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>

13、授權:在controller方法中新增註解:

	//許可權測試方法
	@RequestMapping("/permissionTest")
	@RequiresPermissions("item:update")//執行此方法需要"item:update"許可權
	public String permissionTest(Model model){
		
		return "/permissionTest";
	}

至此,專案啟動後,當訪問“/permissionTest”時,如果使用者沒有“item:update”許可權,將會自動跳轉到refuse.jsp頁面;如果如果使用者擁有該許可權,就會跳轉到permissionTest.jsp頁面。

14、授權:Jsp標籤授權:(permissionTest.jsp)

(1)jsp頁面新增:

<%@ tagliburi="http://shiro.apache.org/tags" prefix="shiro" %>

(2)jsp標籤授權:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ tagliburi="http://shiro.apache.org/tags" prefix="shiro" %>
<html>
    <body>
        <shiro:authenticated>
        	這是登陸之後 才可以看到的內容
        </shiro:authenticated><br/>
        <shiro:notAuthenticated>
        	這是未登陸的時候 才可以看到的內容
        </shiro:notAuthenticated><br/>
        <shiro:hasRole name="manager">
        	這是擁有商品管理員角色才可以看到的內容
        </shiro:hasRole><br/>
        <shiro:lacksRole name="manager">
      		  這是沒有商品管理員角色才可以看到的內容
        </shiro:lacksRole><br/>
        <shiro:hasPermission name="item:update">
        	這是擁有item:update資源許可權才可以看到的內容
        </shiro:hasPermission><br/>
        <shiro:lacksPermission name="item:update">
    	    這是沒有item:update資源許可權才可以看到的內容
        </shiro:lacksPermission><br/>
        <shiro:lacksPermission name="user:query">
    	    這是沒有user:query資源許可權才可以看到的內容
        </shiro:lacksPermission><br/>
    </body>
</html>

至此,當使用者成功進入到授權成功頁面時,只能看到符合自己所屬許可權和角色的內容。

(3)常見的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"/> 

顯示使用者身份中的屬性值

15、授權測試:

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

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

16、完成:

至此,使用Shiro整合Spring+SpringMVC+Mybatis進行登陸認證和授權就完成了,但是在這裡,授權的時候存在一個問題,只要遇到註解或jsp標籤的授權,都會呼叫realm方法查詢資料庫,因此需要使用快取解決此問題。

 

最後,推薦幾篇有關Shiro的文章:

https://www.cnblogs.com/learnhow/p/5694876.html

https://www.sojson.com/shiro#so358852059

https://www.xttblog.com/?p=1272

 

 

相關文章