深入淺出學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方法。
2、Sbuject會委託應用程式設定的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原則。