1. 程式人生 > >spring整合應用安全框架Shiro

spring整合應用安全框架Shiro

Shiro的介紹

 Apache Shiro是一個強大易用的Java安全框架,它提供的主要功能有:

     認證 -——使用者身份識別,常被稱為使用者“登入”;

      授權—— 訪問控制;

密碼加密——保護或隱藏資料防止被偷窺;

會話管理——每使用者相關的時間敏感的狀態。

Shiro的三個核心元件(Subject,SecurityManager 和 Realms)介紹

Subject:“當前操作使用者”。但是,在Shiro中,Subject這一概念並不僅僅指人,也可以是第三方程序、後臺帳戶(Daemon Account)或其他類似事物。它僅僅意味著“當前跟軟體互動的東西”。但考慮到大多數目的和用途,你可以把它認為是Shiro的“使用者”概念。 Subject代表的是當前使用者的安全操作。

SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。

Realm: Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查詢使用者及其許可權資訊。(當配置Shiro時,你至少要指定一個Realm,用於認證和(或)授權,至少需要一個。 Shiro內建了可以連線大量安全資料來源(又名目錄)的Realm,如LDAP、關係資料庫(JDBC)、類似INI的文字配置資源以及屬性檔案等。)

下圖為Shiro功能模組結構:

 

這些模組各有作用:

Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份;

Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權;

Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;

Cryptography:加密,保護資料的安全性,如密碼加密儲存到資料庫,而不是明文儲存;

Web Support:Web支援,可以非常容易的整合到Web環境;

Caching:快取,比如使用者登入後,其使用者資訊、擁有的角色/許可權不必每次去查,這樣可以提高效率;

Concurrency:shiro支援多執行緒應用的併發驗證,即如在一個執行緒中開啟另一個執行緒,能把許可權自動傳播過去;

Testing:提供測試支援;

Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行訪問;

Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。

Shiro依賴包

maven環境下,pom.xml中依賴包配置:

<dependency>  
    <groupId>org.apache.shiro</groupId>  
    <artifactId>shiro-core</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-ehcache</artifactId>  
    <version>1.2.3</version>  
</dependency>  
<dependency>  
    <groupId>org.apache.shiro</groupId>  
    <artifactId>shiro-spring</artifactId>  
    <version>1.2.3</version>  
</dependency>  

web工程中引入Shiro框架,首先要在web.xml中配置:
<!-- Apache Shiro -->

  <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:application.xml,classpath:shiro/spring-shiro.xml</param-value>
  </context-param>

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

web程式啟動時,首先會載入spring-shiro.xml配置檔案,然後執行web中的過濾器,實現安全登入。

配置Realm,進行驗證及授權

定義該一個安全認證的實現類,需要繼承AuthorizingRealm並實現登入驗證和賦予角色許可權的兩個方法

即:

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationTokenauthcToken);--------登入認證時使用

protected AuthorizationInfogetAuthorizationInfo(PrincipalCollection principals);---------使用者授權時使用

還可以自定義一些其他業務中使用到的方法,如下:

@SuppressWarnings("restriction")
@Service
//@DependsOn({"userDao","roleDao","menuDao"})
public class SystemAuthorizingRealm extends AuthorizingRealm {

	private Logger logger = LoggerFactory.getLogger(getClass());
	
	private SystemService systemService;
	
	public SystemAuthorizingRealm() {
		this.setCachingEnabled(false);
	}

	/**
	 * 認證回撥函式, 登入時呼叫
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
		UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
		
		int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
		if (logger.isDebugEnabled()){
			logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, token.getUsername());
		}
		
		// 校驗登入驗證碼
		if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
			Session session = UserUtils.getSession();
			String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
			if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
				throw new AuthenticationException("msg:驗證碼錯誤, 請重試.");
			}
		}
		
		// 校驗使用者名稱密碼
		User user = getSystemService().getUserByLoginName(token.getUsername());
		if (user != null) {
			if (Global.NO.equals(user.getLoginFlag())){
				throw new AuthenticationException("msg:該已帳號禁止登入.");
			}
			byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
			return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()), 
					user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
		} else {
			return null;
		}
	}
	
	/**
	 * 獲取許可權授權資訊,如果快取中存在,則直接從快取中獲取,否則就重新獲取, 登入成功後呼叫
	 */
	protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
		if (principals == null) {
            return null;
        }
		
        AuthorizationInfo info = null;

        info = (AuthorizationInfo)UserUtils.getCache(UserUtils.CACHE_AUTH_INFO);

        if (info == null) {
            info = doGetAuthorizationInfo(principals);
            if (info != null) {
            	UserUtils.putCache(UserUtils.CACHE_AUTH_INFO, info);
            }
        }

        return info;
	}

	/**
	 * 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		Principal principal = (Principal) getAvailablePrincipal(principals);
		// 獲取當前已登入的使用者
		if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))){
			Collection<Session> sessions = getSystemService().getSessionDao().getActiveSessions(true, principal, UserUtils.getSession());
			if (sessions.size() > 0){
				// 如果是登入進來的,則踢出已線上使用者
				if (UserUtils.getSubject().isAuthenticated()){
					for (Session session : sessions){
						getSystemService().getSessionDao().delete(session);
					}
				}
				// 記住我進來的,並且當前使用者已登入,則退出當前使用者提示資訊。
				else{
					UserUtils.getSubject().logout();
					throw new AuthenticationException("msg:賬號已在其它地方登入,請重新登入。");
				}
			}
		}
		User user = getSystemService().getUserByLoginName(principal.getLoginName());
		if (user != null) {
			SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
			List<Menu> list = UserUtils.getMenuList();
			for (Menu menu : list){
				if (StringUtils.isNotBlank(menu.getPermission())){
					// 新增基於Permission的許可權資訊
					for (String permission : StringUtils.split(menu.getPermission(),",")){
						info.addStringPermission(permission);
					}
				}
			}
			// 新增使用者許可權
			info.addStringPermission("user");
			// 新增使用者角色資訊
			for (Role role : user.getRoleList()){
				info.addRole(role.getEnname());
			}
			// 更新登入IP和時間
			getSystemService().updateUserLoginInfo(user);
			// 記錄登入日誌
			LogUtils.saveLog(Servlets.getRequest(), "系統登入");
			return info;
		} else {
			return null;
		}
	}
	
	@Override
	protected void checkPermission(Permission permission, AuthorizationInfo info) {
		authorizationValidate(permission);
		super.checkPermission(permission, info);
	}
	
	@Override
	protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
		if (permissions != null && !permissions.isEmpty()) {
            for (Permission permission : permissions) {
        		authorizationValidate(permission);
            }
        }
		return super.isPermitted(permissions, info);
	}
	
	@Override
	public boolean isPermitted(PrincipalCollection principals, Permission permission) {
		authorizationValidate(permission);
		return super.isPermitted(principals, permission);
	}
	
	@Override
	protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
		if (permissions != null && !permissions.isEmpty()) {
            for (Permission permission : permissions) {
            	authorizationValidate(permission);
            }
        }
		return super.isPermittedAll(permissions, info);
	}
	
	/**
	 * 授權驗證方法
	 * @param permission
	 */
	private void authorizationValidate(Permission permission){
		// 模組授權預留介面
	}
	
	/**
	 * 設定密碼校驗的Hash演算法與迭代次數
	 */
	@PostConstruct
	public void initCredentialsMatcher() {
		HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(SystemService.HASH_ALGORITHM);
		matcher.setHashIterations(SystemService.HASH_INTERATIONS);
		setCredentialsMatcher(matcher);
	}

	/**
	 * 獲取系統業務物件
	 */
	public SystemService getSystemService() {
		if (systemService == null){
			systemService = SpringContextHolder.getBean(SystemService.class);
		}
		return systemService;
	}
	
	/**
	 * 授權使用者資訊
	 */
	public static class Principal implements Serializable {

		private static final long serialVersionUID = 1L;
		
		private String id; // 編號
		private String loginName; // 登入名
		private String name; // 姓名
		private boolean mobileLogin; // 是否手機登入
		
//		private Map<String, Object> cacheMap;

		public Principal(User user, boolean mobileLogin) {
			this.id = user.getId();
			this.loginName = user.getLoginName();
			this.name = user.getName();
			this.mobileLogin = mobileLogin;
		}

		public String getId() {
			return id;
		}

		public String getLoginName() {
			return loginName;
		}

		public String getName() {
			return name;
		}

		public boolean isMobileLogin() {
			return mobileLogin;
		}

		/**
		 * 獲取SESSIONID
		 */
		public String getSessionid() {
			try{
				return (String) UserUtils.getSession().getId();
			}catch (Exception e) {
				return "";
			}
		}
		
		@Override
		public String toString() {
			return id;
		}

	}
}

Shiro配置檔案

<?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" xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.1.xsd"
	default-lazy-init="true">

	<description>Shiro Configuration</description>

    <!-- 載入配置屬性檔案 -->
	<context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" />
	
	<!-- Shiro許可權過濾過濾器定義 -->
	<bean name="shiroFilterChainDefinitions" class="java.lang.String">
		<constructor-arg>
			<value>
				/static/** = anon
				/userfiles/** = anon
				${adminPath}/cas = cas
				${adminPath}/login = authc
				${adminPath}/logout = logout
				${adminPath}/** = user
				/act/editor/** = user
				/ReportServer/** = user
			</value>
		</constructor-arg>
	</bean>
	
	<!-- 安全認證過濾器 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager" />
		<property name="loginUrl" value="${adminPath}/login" />
		<property name="successUrl" value="${adminPath}?login" />
		<property name="filters">
            <map>
                <entry key="cas" value-ref="casFilter"/>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
            </map>
        </property>
		<property name="filterChainDefinitions">
			<ref bean="shiroFilterChainDefinitions"/>
		</property>
	</bean>
	
	<!-- CAS認證過濾器 -->  
	<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">  
		<property name="failureUrl" value="${adminPath}/login"/>
	</bean>
	
	<!-- 定義Shiro安全管理配置 -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="systemAuthorizingRealm" />
		<property name="sessionManager" ref="sessionManager" />
		<property name="cacheManager" ref="shiroCacheManager" />
	</bean>
	
	<!-- 自定義會話管理配置 -->
	<bean id="sessionManager" class="com.thinkgem.jeesite.common.security.shiro.session.SessionManager"> 
		<property name="sessionDAO" ref="sessionDAO"/>
		
		<!-- 會話超時時間,單位:毫秒  -->
		<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
		
		<!-- 定時清理失效會話, 清理使用者直接關閉瀏覽器造成的孤立會話   -->
		<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!--  		<property name="sessionValidationSchedulerEnabled" value="false"/> -->
 		<property name="sessionValidationSchedulerEnabled" value="true"/>
 		
		<property name="sessionIdCookie" ref="sessionIdCookie"/>
		<property name="sessionIdCookieEnabled" value="true"/>
	</bean>
	
	<!-- 指定本系統SESSIONID, 預設為: JSESSIONID 問題: 與SERVLET容器名衝突, 如JETTY, TOMCAT 等預設JSESSIONID,
		當跳出SHIRO SERVLET時如ERROR-PAGE容器會為JSESSIONID重新分配值導致登入會話丟失! -->
	<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
	    <constructor-arg name="name" value="jeesite.session.id"/>
	</bean>

	<!-- 自定義Session儲存容器 -->
	<bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.CacheSessionDAO">
		<property name="sessionIdGenerator" ref="idGen" />
		<property name="activeSessionsCacheName" value="activeSessionsCache" />
		<property name="cacheManager" ref="shiroCacheManager" />
	</bean>
	
	<!-- 自定義系統快取管理器-->
	<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
		<property name="cacheManager" ref="cacheManager"/>
	</bean>
	
	<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
	
	<!-- AOP式方法級許可權檢查  -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
		<property name="proxyTargetClass" value="true" />
	</bean>
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    	<property name="securityManager" ref="securityManager"/>
	</bean>
	
</beans>

這裡做一下說明,Shiro預設到的許可權驗證類別:

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

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沒有引數表示必須存在使用者,當登入操作時不做檢查

注意:

Shiro.xml載入配置是從上而下的,也就是向上面的配置,如/** = anon  ,如果把這個配置在第一行,那麼下面的配置都沒用。因為是從上往下去匹配,只要匹配中了,就不匹配了所以必須要有序。