1. 程式人生 > >深入淺出學Shiro(一)--登入認證

深入淺出學Shiro(一)--登入認證

ApacheShiro是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理等功能:

Shiro為解決下列問題,提供了保護應用的API:

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

  授權 - 訪問控制;

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

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

對於任何一個應用程式,Shiro都可以提供全面的安全管理服務。並且相對於其他安全框架,Shiro要簡單的多。

核心概念:Subject,SecurityManager和Realms


Subject

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

Subject代表了當前使用者的安全操作,SecurityManager則管理所有使用者的安全操作。

SecurityManager

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

Realms

Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當切實與像使用者帳戶這類安全相關資料進行互動,執行認證(登入)和授權(訪問控制)時,Shiro會從應用配置的Realm中查詢很多內容。

  從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了資料來源的連線細節,並在需要時將相關資料提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)授權。配置多個Realm是可以的,但是至少需要一個。

認證流程:

1、應用程式構建了一個終端使用者認證資訊的AuthenticationToken例項後,呼叫Subject.login方法。

2Sbuject會委託應用程式設定的securityManager例項呼叫securityManager.login(token)方法。 

3、SecurityManager接受到token(令牌)資訊後會委託內建的Authenticator的例項(通常都是ModularRealmAuthenticator類的例項)呼叫authenticator.authenticate(token).ModularRealmAuthenticator在認證過程中會對設定的一個或多個Realm例項進行適配,它實際上為Shiro提供了一個可拔插的認證機制。

4、如果在應用程式中配置了多個Realm,ModularRealmAuthenticator會根據配置的AuthenticationStrategy(認證策略)來進行多Realm的認證過程。在Realm被呼叫後,AuthenticationStrategy將對每一個Realm的結果作出響應。 

注:如果應用程式中僅配置了一個Realm,Realm將被直接呼叫而無需再配置認證策略。 

5、Realm將呼叫getAuthenticationInfo(token);getAuthenticationInfo方法就是實際認證處理,我們通過覆蓋Realm的doGetAuthenticationInfo方法來編寫我們自定義的認證處理。 

下面結合一個例項來理解以上這些概念(結合SpringMVC):

Web.xml中新增 Shiro Filter

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationContext.xml,classpath:spring-shiro.xml</param-value>
	</context-param>
	<!-- apache shiro許可權 -->
	<!-- Shiro主過濾器本身功能十分強大,其強大之處就在於它支援任何基於URL路徑表示式的、自定義的過濾器的執行-->  
	<!-- Shiro Filter -->  
	<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>*.do</url-pattern>
	</filter-mapping>
	<filter-mapping>
		<filter-name>shiroFilter</filter-name>
		<url-pattern>*.jsp</url-pattern>
	</filter-mapping>

spring-mvc.xml

<!-- 開啟Shiro的註解,實現對Controller的方法級許可權檢查(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 -->  
    <!-- 配置以下兩個bean即可實現此功能, 應該放在spring-mvc.xml中 -->  
    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  
	<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>

spring-shiro.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:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd   
    http://www.springframework.org/schema/util 
    http://www.springframework.org/schema/util/spring-util-3.0.xsd">
    <description>Shiro 配置</description>
    
   	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<!--設定自定義realm -->
		<property name="realm" ref="monitorRealm" />
	</bean>
	<!--繼承自AuthorizingRealm的自定義Realm,即指定Shiro驗證使用者的認證和授權 --> 
	<!--自定義Realm 繼承自AuthorizingRealm -->
	<bean id="monitorRealm" class="com.shiro.service.MonitorRealm"></bean>
	
	<!-- Shiro主過濾器本身功能十分強大,其強大之處就在於它支援任何基於URL路徑表示式的、自定義的過濾器的執行 --> 
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<!-- Shiro的核心安全介面,這個屬性是必須的 -->  
		<property name="securityManager" ref="securityManager" />
		<!-- 要求登入時的連結,非必須的屬性,預設會自動尋找Web工程根目錄下的"/login.jsp"頁面 -->  
		<property name="loginUrl" value="/login.jsp" />
		<!-- 使用者訪問未對其授權的資源時,所顯示的連線 -->
		<property name="unauthorizedUrl" value="/error/noperms.jsp" />
		<property name="filterChainDefinitions">
			<value>
			 <!-- Shiro 過濾鏈的定義-->   
      <!-- 
					Anon:不指定過濾器 
					Authc:驗證,這些頁面必須驗證後才能訪問,也就是我們說的登入後才能訪問。
					-->
					 <!--下面value值的第一個'/'代表的路徑是相對於HttpServletRequest.getContextPath()的值來的 -->   
					<!--anon:它對應的過濾器裡面是空的,什麼都沒做,這裡.do和.jsp後面的*表示引數,比方說login.jsp?main這種 -->   
        <!--authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內建的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter-->   
				
				/login.jsp* = anon
				/login.do* = anon
				/index.jsp*= anon
				/error/noperms.jsp*= anon
				/*.jsp* = authc
				/*.do* = authc
			</value>
		</property>
	</bean>
	
		<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->
	<bean id="lifecycleBeanPostProcessor"
		class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
		
</beans>

自定義RealmMonitorRealm

@Service("monitorRealm")
public class MonitorRealm extends AuthorizingRealm {

	//獲取身份驗證相關資訊 
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken authcToken) throws AuthenticationException {
		/* 這裡編寫登陸認證程式碼 */
//		UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
//		User user = userService.get(token.getUsername());
		User user = new User();
		user.setUserName("admin");
		user.setPassword(EncryptUtils.encryptMD5("admin"));
		
		
		return new SimpleAuthenticationInfo(user.getUserName(),
			user.getPassword(), getName());
		
		/* //令牌——基於使用者名稱和密碼的令牌  
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;  
        //令牌中可以取出使用者名稱密碼  
        String accountName = token.getUsername();  
          
      // 此處無需比對,比對的邏輯Shiro會做,我們只需返回一個和令牌相關的正確的驗證資訊,因此在隨後的登入頁面上只有admin/admin123才能通過驗證  
        return new SimpleAuthenticationInfo("admin","admin123",getName());  */
	}

LoginController

@Controller
@RequestMapping(value = "login")
public class LoginController {
	/**
	 * 使用者登入
	 * 
	 * @param user
	   *            登入使用者
	 * @return
	 */
	@RequestMapping(params = "main")
	public ModelAndView login(User user,HttpSession session, HttpServletRequest request) {

		ModelAndView modelView = new ModelAndView();
		/*就是代表當前的使用者。*/
		Subject currentUser = SecurityUtils.getSubject();
		//獲取基於使用者名稱和密碼的令牌 
		//這裡的token大家叫他令牌,也就相當於一張表格,你要去驗證,你就得填個表,裡面寫好使用者名稱密碼,交給公安局的同志給你驗證。
		UsernamePasswordToken token = new UsernamePasswordToken(
				user.getUserName(), EncryptUtils.encryptMD5(user.getPassword()));
		/*UsernamePasswordToken token = new UsernamePasswordToken(
				user.getUserName(), user.getPassword());*/
//		但是,“已記住”和“已認證”是有區別的: 
//		已記住的使用者僅僅是非匿名使用者,你可以通過subject.getPrincipals()獲取使用者資訊。但是它並非是完全認證通過的使用者,當你訪問需要認證使用者的功能時,你仍然需要重新提交認證資訊。 
//		這一區別可以參考亞馬遜網站,網站會預設記住登入的使用者,再次訪問網站時,對於非敏感的頁面功能,頁面上會顯示記住的使用者資訊,但是當你訪問網站賬戶資訊時仍然需要再次進行登入認證。 
		token.setRememberMe(true); 
		
		try {
			//這句是提交申請,驗證能不能通過,也就是交給公安局同志了。這裡會回撥reaml裡的一個方法
			// 回撥doGetAuthenticationInfo,進行認證
			currentUser.login(token);
		} catch (AuthenticationException e) {
			modelView.addObject("message", "login errors");
			modelView.setViewName("/login");
			e.printStackTrace();
			return modelView;
		}
		//驗證是否通過 
		if(currentUser.isAuthenticated()){
			user.setUserName("張三");
			session.setAttribute("userinfo", user);
			modelView.setViewName("/main");
		}else{
			modelView.addObject("message", "login errors");
			modelView.setViewName("/login");
		}
		return modelView;
	}


附:currentUser.login(token);的方法呼叫,呼叫到Subjectsubject = securityManager.login(this, token);方法後,則跳轉到自定義Realm

public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);

        PrincipalCollection principals;

        String host = null;

        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
            String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                    "empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
        this.principals = principals;
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
        Session session = subject.getSession(false);
        if (session != null) {
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }


總結:

以上是一個簡單的Shiro的登入認證過程,其實這部分功能也就是幫助我們驗證此使用者是否能登入本系統,和我們普通的登入完成的是同樣的功能,Shiro是幫我們封裝了這部分內容,讓我們無需將登入的驗證均寫到程式中,而是使用配置的方式,更加靈活的應對變化,符合我們所說的OCP原則。