Shiro權限控制(一):Spring整合Shiro

一、目標:
1.介紹如何在SpringMVC中整合Shiro權限框架
2.介紹如何使用Shrio進行身份驗證,如常見的登錄
3.介紹如何控制哪些服務登錄後才能訪問,哪些服務不需要登錄就可以訪問

二、前言
本文是在前兩篇博文《Spring+Spring MVC+Mybatis+Maven搭建多模塊項目(一)》《Spring+Spring MVC+Mybatis+Maven搭建多模塊項目(二)》的基礎上整合Shiro框架,如果想了解Spring+SpringMVC+Mytatis是如何整合的,請查看前面的文章。

在項目中,我們經常會用到權限驗證,在項目初期,架構師已經提前把權限框架整合到工程中,開發人員只需要按一定的規則進行開發即可,並不需要過多關心權限是怎麼實現的,那Shiro到底是怎麼實現權限控制的呢?

三、Shiro簡單介紹
Shiro是Apache 旗下的一個簡單易用的權限框架,可以輕鬆的完成 認證、授權、加密、會話管理、與 Web 集成、緩存等,這裏只進行簡單的介紹,詳細的介紹請查閱官方文檔,先來看下Shiro如何工作的
在這裏插入圖片描述
可以看到:應用代碼直接交互的對象是 Subject,也就是說 Shiro 的對外 API 核心就是 Subject;其每個 API 的含義:

Subject:主體,代表了當前 “用戶”,這個用戶不一定是一個具體的人,與當前應用交互的任何東西都是 Subject,所有 Subject 都綁定到 SecurityManager,與 Subject 的所有交互都會委託給 SecurityManager

SecurityManager:安全管理器;即所有與安全有關的操作都會與 SecurityManager 交互;且它管理着所有 Subject;它是 Shiro 的核心,它負責與他組件進行交互,可以把它看成 DispatcherServlet 前端控制器

Realm:域,Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它需要從 Realm 獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色 / 權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource,即安全數據源

記住一點,Shiro 不會去維護用戶、權限;需要我們自己去設計 / 提供;然後通過相應的接口注入給 Shiro 即可。

四、Shiro與Spring集成
1.在pom.xml文件中引入對Shiro的依賴

    <!-- shiro 包-->
	<dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-quartz</artifactId>
        <version>1.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.2</version>
    </dependency>

2.在config目錄下創建Shiro配置文件spring-shiro-web.xml,內容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:aop="http://www.springframework.org/schema/aop" 
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
			http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
			http://www.springframework.org/schema/mvc 
    		http://www.springframework.org/schema/mvc/spring-mvc-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">

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    	<property name="realm" ref="userShiroRealm"/>
    </bean>
    
    <!-- 自定義域 -->
    <bean id="userShiroRealm" class="com.bug.realm.UserShiroRealm">
    	<property name="userService" ref="userService"/>
    	<property name="credentialsMatcher" ref="credentialsMatcher"/>
    	<property name="cachingEnabled" value="true"/>
    </bean>
    
     <!-- 自定義憑證(密碼)匹配器 -->
    <bean id="credentialsMatcher" class="com.bug.credentials.BugCredentialsMatcher"></bean>
    
    <!-- 自定義登錄驗證過濾器 -->
    <bean id="loginCheckPermissionFilter" class="com.bug.filter.LoginCheckPermissionFilter"></bean>
    
    <!-- Shiro的web過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    	<property name="securityManager" ref="securityManager"></property>
    	<property name="unauthorizedUrl" value="/index.jsp"></property>
    	<property name="filters">
    		<map>
    			<entry key="authc" value-ref="loginCheckPermissionFilter"></entry>
    		</map>
    	</property>
    	<property name="filterChainDefinitions">
    		<value>
    			/index.jsp = anon
                /unauthorized.jsp = anon
                /user/checkLogin = anon
                /user/queryUserInfo = authc
                /user/logout = anon
                /pubApi/** = anon
    		</value>
    	</property>
    </bean>
    
</beans>

說明:
1.首先聲明SecurityManager,用於管理所有的 Subject,在SecurityManager中需要引用Realm,也就是權限數據的來源,通過自定義的Realm告訴SecurityManager有哪些數據權限需要管理

2.自定義Realm,注入UserService,通過UserService獲得用戶信息,如用戶名及密碼,另外還自定義了憑證(密碼)匹配規則credentialsMatcher

3.自定義登錄驗證過濾器loginCheckPermissionFilter,登錄校驗的核心過濾器,哪些服務需要登錄才能訪問,就通過此過濾器控制

4.聲明Shiro的WEB過濾器,必須引入securityManager,否則啓動報錯,WEB過濾器主要用於URL的訪問控制,控制哪些URL需要權限控制,哪些不需要權限控制,如
/user/logout = anon:表示不需要權限控制
/user/queryUserInfo = authc:表示需要登錄後才能訪問
/pubApi/** = anon:表示URL路徑中存在pubApi關鍵字的,都不需要權限控制

5.配置文件中用到的userShiroRealm,credentialsMatcher及loginCheckPermissionFilter在下面會給出代碼的實現

3.修改web.xml文件,將Shiro集成到工程中

3.1將上面增加的spring-shiro-web.xml文件引入到web.xml文件中

<!-- Spring配置 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:config/applicationContext.xml,
			classpath:config/spring-shiro-web.xml
		</param-value>
	</context-param>

3.2 在web.xml中配置shiroFilter過濾器,注意過濾器名稱shiroFilter一定要和spring-shiro-web.xml中聲明的shiroFilter保存一致

    <!-- shiro filter start -->
	<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>
	<!-- shiro filter end -->

4.自定義Realm
在上面的配置中,我們注入了自定義的Realm UserShiroRealm,此Realm需要繼承AuthorizingRealm,並實現doGetAuthorizationInfo權限驗證方法和doGetAuthenticationInfo身份驗證方法,,代碼實現如下

package com.bug.realm;

import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import com.bug.excption.BugException;
import com.bug.model.user.UserVO;
import com.bug.service.user.IUserService;
/**
 * 自定義Realm
 * @author longwentao
 *
 */
public class UserShiroRealm extends AuthorizingRealm{
	
	private IUserService userService;
	
	public void setUserService(IUserService userService) {
		this.userService = userService;
	}

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String username = (String)principals.getPrimaryPrincipal();
		if(username == null) {
			throw new BugException("未登錄");
		}
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		Set<String> roles = new HashSet<String>();
		Set<String> stringPermissions = new HashSet<String>();
		roles.add("USER");
		stringPermissions.add("USER:DELETE");//角色:權限
		
		info.setRoles(roles);//角色可以通過數據庫查詢得到
		info.setStringPermissions(stringPermissions);//權限可以通過數據庫查詢得到
		
		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken autToken) throws AuthenticationException {

		UsernamePasswordToken userPwdToken = (UsernamePasswordToken) autToken;
		String userName = userPwdToken.getUsername();

		UserVO user = userService.selectUserByUserName(userName);
		if (null == user) {
			throw new BugException("未知賬號");
		}
		
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(),
				user.getPassword().toCharArray(), getName());

		return authenticationInfo;
	}
}

說明:
1.Shiro進行身份驗證時,會調用到doGetAuthenticationInfo方法,在方法內部,我們通過UsernamePasswordToken 獲得用戶傳過來的用戶名,再通過userService.selectUserByUserName方法從數據庫中查詢用戶信息,如果用戶爲空,說賬號不存在,否則將查詢出來的用戶名及密碼,封裝到SimpleAuthenticationInfo 對象中,並返回,用於接下來的密碼驗證

2.Shiro角色權限驗證,會調用doGetAuthorizationInfo方法,通過SimpleAuthorizationInfo.setRoles()方法設置用戶角色,通過SimpleAuthorizationInfo.setStringPermissions()設置用戶權限,這裏暫時給個空集合,在項目中,用戶的角色權限需要從數據庫中查詢

5.自定義憑證(密碼)匹配器
此過濾器主要用於憑證(密碼)匹配,即校驗用戶輸入的密碼和從數據庫中查詢的密碼是否相同,相同則返回true,否則返回false,此匹配器繼承了SimpleCredentialsMatcher,並重寫doCredentialsMatch方法,代碼如下

package com.bug.credentials;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.util.SimpleByteSource;

/**
 * 自定義憑證(密碼)匹配器
 * @author longwentao
 *
 */
public class BugCredentialsMatcher extends SimpleCredentialsMatcher {

	@Override
	public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
		// 對前臺傳入的明文數據加密,根據自定義加密規則加密
		Object tokencredential = new SimpleByteSource((char[]) token.getCredentials());
		// 從數據庫獲取的加密數據
		Object accunt = new SimpleByteSource((char[]) info.getCredentials());
		// 返回對比結果
		return equals(accunt, tokencredential);
	}
}

6.自定義登錄驗證過濾器
此過濾器主要用於校驗用戶訪問某個URL時,是否已經提前登錄過,如果登錄過,則允許訪問,否則拒絕訪問;此過濾器繼承了AuthorizationFilter,並重寫了isAccessAllowed方法和onAccessDenied方法,代碼如下

package com.bug.filter;

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定義登錄驗證過濾器
 * @author longwentao
 *
 */
public class LoginCheckPermissionFilter extends AuthorizationFilter {
	private final static Logger logger = LoggerFactory.getLogger(LoginCheckPermissionFilter.class);

	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object arg2) throws Exception {
		HttpServletRequest req = (HttpServletRequest) request;
		String url = req.getRequestURI();
		try {
			Subject subject = SecurityUtils.getSubject();

			return subject.isPermitted(url);
		} catch (Exception e) {
			logger.error("Check perssion error", e);
		}
		return false;
	}

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
		Subject subject = getSubject(request, response);
		if (subject.getPrincipal() == null) {
			saveRequestAndRedirectToLogin(request, response);
		} else {
			return true;
		}
		return false;
	}
}

說明:
在onAccessDenied方法中,如果用戶身份爲空,說明未登錄,則跳轉到登錄頁面,如果未指定跳轉的路徑,Shiro給了默認值的跳轉頁面 /login.jsp
在這裏插入圖片描述
到此,所有的配置及自定義的過濾器都已經實現完成,Shiro已經集成到項目中,接下來進行用例驗證

五.驗證準備工作

一個login.jsp頁面及一個UserController.java,在Controller中提供3個服務,權限如下

  1. 一個登錄頁面login.jsp --不需要權限控制
  2. 登錄校驗服務 /user/checkLogin --不需要權限控制,即/user/checkLogin = anon
  3. 退出服務/user/logout --不需要權限控制,即/user/logout = anon
  4. 查詢用戶信息服務/user/queryUserInfo --需要登錄後纔可訪問,即/user/queryUserInfo = authc

這裏有個疑惑,退出服務爲什麼不要權限控制呢,如果A用戶已經登錄,那B用戶知道退出的服務地址,直接請求退出服務,豈不是將A用戶強制退出了?不着急,看下面的驗證就知道會不會有影響了

login.jsp頁面代碼如下:

<form action="/bug.web/user/checkLogin" method="post">
    用戶名:<input type="text" name="userName"><br/>
    密碼:<input type="password" name="password"><br/>
    <input type="submit" value="登錄">
</form>

UserController.java代碼如下:

package com.bug.controller.user;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.bug.excption.BugException;
import com.bug.model.common.ResponseVO;
import com.bug.model.user.UserVO;
import com.bug.service.user.IUserService;

@Controller
@RequestMapping("/user")
public class UserController {
	private final static Logger logger = LoggerFactory.getLogger(UserController.class);

	@Autowired
	private IUserService userService;
	
	@RequestMapping(value = "/checkLogin", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded")
	@ResponseBody
	public ResponseVO<String> checkLogin(@RequestParam("userName") String userName,
			@RequestParam("password") String password) {
		ResponseVO<String> response = new ResponseVO<String>();
		try {
			UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
			
			Subject subject = SecurityUtils.getSubject();

			subject.login(token);
		}catch (Exception e) {
			logger.error("Login Error:",e);
			response.setStatus(ResponseVO.failCode);
			Throwable ex = e.getCause();
			if(ex instanceof BugException) {
				if(ex.getMessage() != null) {
					response.setMessage(ex.getMessage());
				}
			}else if(e instanceof IncorrectCredentialsException) {
				response.setMessage("密碼錯誤");
			}else {
				response.setMessage("登錄失敗");
			}
		}

		return response;
	}
	@RequestMapping(value = "/logout", method = RequestMethod.GET)
	public ResponseVO<String> logout(){
		ResponseVO<String> response = new ResponseVO<String>();
		Subject subject = SecurityUtils.getSubject();
		if(subject.isAuthenticated()) {
			subject.logout();
		}
		return response;
	}
	
	@RequestMapping(value="/queryUserInfo",method = RequestMethod.GET)
	@ResponseBody
	public ResponseVO<UserVO> queryUserInfo() {
		ResponseVO<UserVO> response = new ResponseVO<UserVO>();
		try {
			UserVO user = userService.selectUserById("1");
			response.setData(user);
		} catch (Exception e) {
			logger.error("queryUserInfo error:",e);
			response.setStatus(ResponseVO.failCode);
		}

		return response;
	}

}

六、驗證場景:

1.輸入錯誤密碼,看Shiro如何進行憑證校驗
訪問localhost:8080/bug.web/login.jsp,輸入用戶名及錯誤密碼,點擊登錄
在這裏插入圖片描述

開始訪問到UserController中的checkLogin,在checkLogin中使用Shiro提供的Subject.login方法進行登錄
在這裏插入圖片描述
接下來會訪問到UserShiroRealm.doGetAuthenticationInfo方法,在方法中使用傳進來的username通過UserService查詢用戶信息
在這裏插入圖片描述
用戶名驗證通過後,從源碼中可以看出接下來進行密碼驗證,在AuthenticatingRealm.getAuthenticationInfo方法中
在這裏插入圖片描述
在assertCredentialsMatch方法中,獲得的CredentialsMatcher就是我們自定義的BugCredentialsMatcher
在這裏插入圖片描述
繼續往下執行,就到了我們的自定義憑證(密碼)匹配器BugCredentialsMatcher
在這裏插入圖片描述
繼續向下執行,就會拋IncorrectCredentialsException異常,說明密碼錯誤
在這裏插入圖片描述
異常拋出後,在異常處理中捕獲,並將提示信息返回給用戶,整個登錄校驗過程就完成了
在這裏插入圖片描述
在這裏插入圖片描述
2.未登錄時,訪問queryUserInfo 服務,看能否訪問
直接訪問http://localhost:8080/bug.web/user/queryUserInfo,Shiro發現用戶未登錄,已經自動重定向到登錄頁面
在這裏插入圖片描述
在這裏插入圖片描述
3.登錄後,訪問queryUserInfo 服務,看能否訪問
登錄後直接訪問http://localhost:8080/bug.web/user/queryUserInfo,發現調用鏈是這樣的:LoginCheckPermissionFilter.isAccessAllowed–>UserShiroRealm.doGetAuthorizationInfo–>LoginCheckPermissionFilter.onAccessDenied,在onAccessDenied方法中返回true,說明已經登錄,用戶可以訪問
在這裏插入圖片描述
在這裏插入圖片描述
4.A用戶已經登錄,B用戶未登錄直接請求退出服務,校驗A用戶是否有影響
A用戶登錄後,B用戶直接訪問http://localhost:8080/bug.web/user/logout服務,發現subject.isAuthenticated()爲false,並不會用戶subject.logout();,因此A用戶已經登錄,B用戶未登錄直接請求退出服務,A用戶沒有影響
在這裏插入圖片描述

到此爲止,Spring集成Shiro權限框架基本已經完成,當然,這只是最基本的訪問控制,更復雜的權限控制需要整合角色一起設計,即什麼角色擁有查詢權限,什麼角色擁有增刪改權限,這些到下一篇博文中再介紹!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章