質文章,及時送達
作者:冷豪
鏈接:www.cnblogs.com/learnhow/p/5694876.html
一、架構
要學習如何使用Shiro必須先從它的架構談起,作為一款安全框架Shiro的設計相當精妙。Shiro的應用不依賴任何容器,它也可以在JavaSE下使用。但是最常用的環境還是JavaEE。下面以用戶登錄為例:
1、使用用戶的登錄信息創建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token可以理解為用戶令牌,登錄的過程被抽象為Shiro驗證令牌是否具有合法身份以及相關權限。
2、執行登陸動作
SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject; // 獲取Subject單例對象
subject.login(token); // 登陸
Shiro的核心部分是SecurityManager,它負責安全認證與授權。Shiro本身已經實現了所有的細節,用戶可以完全把它當做一個黑盒來使用。SecurityUtils對象,本質上就是一個工廠類似Spring中的ApplicationContext。
Subject是初學者比較難于理解的對象,很多人以為它可以等同于User,其實不然。Subject中文翻譯:項目,而正確的理解也恰恰如此。它是你目前所設計的需要通過Shiro保護的項目的一個抽象概念。通過令牌(token)與項目(subject)的登陸(login)關系,Shiro保證了項目整體的安全。
我把歷史發布過的實戰文章整理成了 PDF ,關注微信公眾號「Java后端」回復 666 下載。
3、判斷用戶
Shiro本身無法知道所持有令牌的用戶是否合法,因為除了項目的設計人員恐怕誰都無法得知。因此Realm是整個框架中為數不多的必須由設計者自行實現的模塊,當然Shiro提供了多種實現的途徑,本文只介紹最常見也最重要的一種實現方式——數據庫查詢。
4、兩條重要的英文
我在學習Shiro的過程中遇到的第一個障礙就是這兩個對象的英文名稱:AuthorizationInfo,AuthenticationInfo。不用懷疑自己的眼睛,它們確實長的很像,不但長的像,就連意思都十分近似。
在解釋它們前首先必須要描述一下Shiro對于安全用戶的界定:和大多數操作系統一樣。用戶具有角色和權限兩種最基本的屬性。例如,我的Windows登陸名稱是learnhow,它的角色是administrator,而administrator具有所有系統權限。這樣learnhow自然就擁有了所有系統權限。那么其他人需要登錄我的電腦怎么辦,我可以開放一個guest角色,任何無法提供正確用戶名與密碼的未知用戶都可以通過guest來登錄,而系統對于guest角色開放的權限極其有限。
同理,Shiro對用戶的約束也采用了這樣的方式。AuthenticationInfo代表了用戶的角色信息集合,AuthorizationInfo代表了角色的權限信息集合。如此一來,當設計人員對項目中的某一個url路徑設置了只允許某個角色或具有某種權限才可以訪問的控制約束的時候,Shiro就可以通過以上兩個對象來判斷。說到這里,大家可能還比較困惑。先不要著急,繼續往后看就自然會明白了。
二、實現Realm
如何實現Realm是本文的重頭戲,也是比較費事的部分。這里大家會接觸到幾個新鮮的概念:緩存機制、散列算法、加密算法。由于本文不會專門介紹這些概念,所以這里僅僅拋磚引玉的談幾點,能幫助大家更好的理解Shiro即可。
1、緩存機制
Ehcache是很多Java項目中使用的緩存框架,Hibernate就是其中之一。它的本質就是將原本只能存儲在內存中的數據通過算法保存到硬盤上,再根據需求依次取出。你可以把Ehcache理解為一個Map<String,Object>對象,通過put保存對象,再通過get取回對象。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir" />
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
以上是ehcache.xml文件的基礎配置,timeToLiveSeconds為緩存的最大生存時間,timeToIdleSeconds為緩存的最大空閑時間,當eternal為false時ttl和tti才可以生效。更多配置的含義大家可以去網上查詢。
2、散列算法與加密算法
md5是本文會使用的散列算法,加密算法本文不會涉及。散列和加密本質上都是將一個Object變成一串無意義的字符串,不同點是經過散列的對象無法復原,是一個單向的過程。例如,對密碼的加密通常就是使用散列算法,因此用戶如果忘記密碼只能通過修改而無法獲取原始密碼。但是對于信息的加密則是正規的加密算法,經過加密的信息是可以通過秘鑰解密和還原。
3、用戶注冊
請注意,雖然我們一直在談論用戶登錄的安全性問題,但是說到用戶登錄首先就是用戶注冊。如何保證用戶注冊的信息不丟失,不泄密也是項目設計的重點。
public classPasswordHelper{
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator;
private String algorithmName = "md5";
private final int hashIterations = 2;
publicvoidencryptPassword(User user) {
// User對象包含最基本的字段Username和Password
user.setSalt(randomNumberGenerator.nextBytes.toHex);
// 將用戶的注冊密碼經過散列算法替換成一個不可逆的新密碼保存進數據,散列過程使用了鹽
String newPassword = new SimpleHash(algorithmName, user.getPassword,
ByteSource.Util.bytes(user.getCredentialsSalt), hashIterations).toHex;
user.setPassword(newPassword);
}
}
如果你不清楚什么叫加鹽可以忽略散列的過程,只要明白存儲在數據庫中的密碼是根據戶注冊時填寫的密碼所產生的一個新字符串就可以了。經過散列后的密碼替換用戶注冊時的密碼,然后將User保存進數據庫。剩下的工作就丟給UserService來處理。
那么這樣就帶來了一個新問題,既然散列算法是無法復原的,當用戶登錄的時候使用當初注冊時的密碼,我們又應該如何判斷?答案就是需要對用戶密碼再次以相同的算法散列運算一次,再同數據庫中保存的字符串比較。
4、匹配
CredentialsMatcher是一個接口,功能就是用來匹配用戶登錄使用的令牌和數據庫中保存的用戶信息是否匹配。當然它的功能不僅如此。本文要介紹的是這個接口的一個實現類:HashedCredentialsMatcher
public classRetryLimitHashedCredentialsMatcherextendsHashedCredentialsMatcher{
// 聲明一個緩存接口,這個接口是Shiro緩存管理的一部分,它的具體實現可以通過外部容器注入
private Cache<String, AtomicInteger> passwordRetryCache;
publicRetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
publicbooleandoCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal;
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == ) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 自定義一個驗證過程:當用戶連續輸入密碼錯誤5次以上禁止用戶登錄一段時間
if (retryCount.incrementAndGet > 5) {
throw new ExcessiveAttemptsException;
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}
可以看到,這個實現里設計人員僅僅是增加了一個不允許連續錯誤登錄的判斷。真正匹配的過程還是交給它的直接父類去完成。連續登錄錯誤的判斷依靠Ehcache緩存來實現。顯然match返回true為匹配成功。
5、獲取用戶的角色和權限信息
說了這么多才到我們的重點Realm,如果你已經理解了Shiro對于用戶匹配和注冊加密的全過程,真正理解Realm的實現反而比較簡單。我們還得回到上文提及的兩個非常類似的對象AuthorizationInfo和AuthenticationInfo。因為Realm就是提供這兩個對象的地方。
public class UserRealm extends AuthorizingRealm {
// 用戶對應的角色信息與權限信息都保存在數據庫中,通過UserService獲取數據
private UserService userService = new UserServiceImpl;
/**
* 提供用戶信息返回權限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal;
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo;
// 根據用戶名查詢當前用戶擁有的角色
Set<Role> roles = userService.findRoles(username);
Set<String> roleNames = new HashSet<String>;
for (Role role : roles) {
roleNames.add(role.getRole);
}
// 將角色名稱提供給info
authorizationInfo.setRoles(roleNames);
// 根據用戶名查詢當前用戶權限
Set<Permission> permissions = userService.findPermissions(username);
Set<String> permissionNames = new HashSet<String>;
for (Permission permission : permissions) {
permissionNames.add(permission.getPermission);
}
// 將權限名稱提供給info
authorizationInfo.setStringPermissions(permissionNames);
return authorizationInfo;
}
/**
* 提供賬戶信息返回認證信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal;
User user = userService.findByUsername(username);
if (user == ) {
// 用戶名不存在拋出異常
throw new UnknownAccountException;
}
if (user.getLocked == 0) {
// 用戶被管理員鎖定拋出異常
throw new LockedAccountException;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername,
user.getPassword, ByteSource.Util.bytes(user.getCredentialsSalt), getName);
return authenticationInfo;
}
}
根據Shiro的設計思路,用戶與角色之前的關系為多對多,角色與權限之間的關系也是多對多。在數據庫中需要因此建立5張表,分別是:
用戶表(存儲用戶名,密碼,鹽等)
角色表(角色名稱,相關描述等)
權限表(權限名稱,相關描述等)
用戶-角色對應中間表(以用戶ID和角色ID作為聯合主鍵)
角色-權限對應中間表(以角色ID和權限ID作為聯合主鍵)
具體dao與service的實現本文不提供。總之結論就是,Shiro需要根據用戶名和密碼首先判斷登錄的用戶是否合法,然后再對合法用戶授權。而這個過程就是Realm的實現過程。
6、會話
用戶的一次登錄即為一次會話,Shiro也可以代替Tomcat等容器管理會話。目的是當用戶停留在某個頁面長時間無動作的時候,再次對任何鏈接的訪問都會被重定向到登錄頁面要求重新輸入用戶名和密碼而不需要程序員在Servlet中不停的判斷Session中是否包含User對象。
啟用Shiro會話管理的另一個用途是可以針對不同的模塊采取不同的會話處理。以淘寶為例,用戶注冊淘寶以后可以選擇記住用戶名和密碼。之后再次訪問就無需登陸。但是如果你要訪問支付寶或購物車等鏈接依然需要用戶確認身份。當然,Shiro也可以創建使用容器提供的Session最為實現。
三、與SpringMVC集成
有了注冊模塊和Realm模塊的支持,下面就是如何與SpringMVC集成開發。有過框架集成經驗的同學一定知道,所謂的集成基本都是一堆xml文件的配置,Shiro也不例外。
1、配置前端過濾器
先說一個題外話,Filter是過濾器,interceptor是攔截器。前者基于回調函數實現,必須依靠容器支持。因為需要容器裝配好整條FilterChain并逐個調用。后者基于代理實現,屬于AOP的范疇。
如果希望在WEB環境中使用Shiro必須首先在web.xml文件中配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Shiro_Project</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 將Shiro的配置文件交給Spring監聽器初始化 -->
<param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
</context-param>
<context-param>
<param-name>log4jConfigLoaction</param-name>
<param-value>classpath:log4j.properties</param-value>
</context-param>
<!-- shiro配置 開始 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<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配置 結束 -->
</web-app>
熟悉Spring配置的同學可以重點看有綠字注釋的部分,這里是使Shiro生效的關鍵。由于項目通過Spring管理,因此所有的配置原則上都是交給Spring。DelegatingFilterProxy的功能是通知Spring將所有的Filter交給ShiroFilter管理。
接著在classpath路徑下配置spring-shiro-web.xml文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 緩存管理器 使用Ehcache實現 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
<!-- 憑證匹配器 -->
<bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager" />
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="2" />
<property name="storedCredentialsHexEncoded" value="true" />
</bean>
<!-- Realm實現 -->
<bean id="userRealm" class="utils.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<!-- Shiro的Web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/" />
<property name="unauthorizedUrl" value="/" />
<property name="filterChainDefinitions">
<value>
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
</value>
</property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
需要注意filterChainDefinitions過濾器中對于路徑的配置是有順序的,當找到匹配的條目之后容器不會再繼續尋找。因此帶有通配符的路徑要放在后面。三條配置的含義是:
/authc/admin需要用戶有用admin權限
/authc/**用戶必須登錄才能訪問
/**其他所有路徑任何人都可以訪問
說了這么多,大家一定關心在Spring中引入Shiro之后到底如何編寫登錄代碼呢。
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping("login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject;
try {
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕獲密碼錯誤異常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "password error!");
return mv;
} catch (UnknownAccountException uae) {
// 捕獲未知用戶名異常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "username error!");
return mv;
} catch (ExcessiveAttemptsException eae) {
// 捕獲錯誤登錄過多的異常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "times error");
return mv;
}
User user = userService.findByUsername(username);
subject.getSession.setAttribute("user", user);
return new ModelAndView("success");
}
}
登錄完成以后,當前用戶信息被保存進Session。這個Session是通過Shiro管理的會話對象,要獲取依然必須通過Shiro。傳統的Session中不存在User對象。
@Controller
@RequestMapping("authc")
publicclassAuthcController{
// /authc/** = authc 任何通過表單登錄的用戶都可以訪問
@RequestMapping("anyuser")
public ModelAndView anyuser {
Subject subject = SecurityUtils.getSubject;
User user = (User) subject.getSession.getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
// /authc/admin = user[admin] 只有具備admin角色的用戶才可以訪問,否則請求將被重定向至登錄界面
@RequestMapping("admin")
public ModelAndView admin {
Subject subject = SecurityUtils.getSubject;
User user = (User) subject.getSession.getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
}
本篇內容大多總結自張開濤的《跟我學Shiro》原文地址:
http://jinnianshilongnian.iteye.com/blog/2018936
-END-
如果看到這里,說明你喜歡這篇文章,請 轉發、點贊。同時標星(置頂)本公眾號可以第一時間接受到博文推送。
1. 自己手擼一個 Spring MVC
最近整理一份面試資料《Java技術棧學習手冊》,覆蓋了Java技術、面試題精選、Spring全家桶、Nginx、SSM、微服務、數據庫、數據結構、架構等等。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.2</version>
</dependency>
</dependencies>
<!--
1. 配置 Shiro 的 shiroFilter.
2. DelegatingFilterProxy 實際上是 Filter 的一個代理對象. 默認情況下, Spring 會到 IOC 容器中查找和
<filter-name> 對應的 filter bean. 也可以通過 targetBeanName 的初始化參數來配置 filter bean 的 id.
-->
<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>
<ehcache updateCheck="false" name="shiroCache">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
</ehcache>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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.xsd">
<!--
1. 配置 SecurityManager!
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
</bean>
<!--
2. 配置 CacheManager.
2.1 需要加入 ehcache 的 jar 包及配置文件.
-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
</bean>
<!-- =========================================================
Shiro Spring-specific integration
========================================================= -->
<!-- Post processor that automatically invokes init() and destroy() methods
for Spring-configured Shiro objects so you don't have to
1) specify an init-method and destroy-method attributes for every bean
definition and
2) even know which Shiro objects require these methods to be
called. -->
<!--
4. 配置 LifecycleBeanPostProcessor. 可以自動調用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!--
5. 啟用 IOC 容器中使用 shiro 的注解. 但必須在配置了 LifecycleBeanPostProcessor 之后才可以使用.
-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--
6. 配置 ShiroFilter.
6.1 id 必須和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
若不一致, 則會拋出: NoSuchBeanDefinitionException. 因為 Shiro 會來 IOC 容器中查找和 <filter-name> 名字對應的 filter bean.
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!--
配置哪些頁面需要受保護.
以及訪問這些頁面需要的權限.
1). anon 可以被匿名訪問
2). authc 必須認證(即登錄)后才可能訪問的頁面.
3). logout 登出.
4). roles 角色過濾器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
工作流程
在這里插入圖片描述
Shiro通過在web.xml配置文件中配置的ShiroFilter來攔截所有請求,并通過配置filterChainDefinitions來指定哪些頁面受保護以及它們的權限。
URL權限配置
[urls]部分的配置,其格式為:url=攔截器[參數];如果當前請求的url匹配[urls]部分的某個url模式(url模式使用Ant風格匹配),將會執行其配置的攔截器,其中:
例:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
# everything else requires authentication:
/** = authc
</value>
</property>
需要注意的是,url權限采取第一次匹配優先的方式,即從頭開始使用第一個匹配的url模式對應的攔截器鏈,如:
如果請求的url是/bb/aa,因為按照聲明順序進行匹配,那么將使用filter1進行攔截。
Shiro認證流程
下面具體實現一下,首先創建login.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h4>Login Page</h4>
<form action="shiroLogin" method="post">
username:<input type="text" name="username"/>
<br/>
<br/>
password:<input type="password" name="password"/>
<br/>
<br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
然后編寫控制器:
package com.wwj.shiro.handlers;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class ShiroHandler {
@RequestMapping("/shiroLogin")
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
//獲取當前的Subject
Subject currentUser = SecurityUtils.getSubject();
//校驗當前用戶是否已經被認證
if(!currentUser.isAuthenticated()){
//把用戶名和密碼封裝為UsernamePasswordToken對象
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
token.setRememberMe(true);
try {
//執行登錄
currentUser.login(token);
}catch (AuthenticationException ae){
System.out.println("登錄失敗" + ae.getMessage());
}
}
return "redirect:/list.jsp";
}
}
編寫自定義的Realm:
package com.wwj.shiro.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
public class ShiroRealm extends AuthenticatingRealm {
/**
* @param authenticationToken 該參數實際上是控制器方法中封裝用戶名和密碼后執行login()方法傳遞進去的參數token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//將參數轉回UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//從UsernamePasswordToken中取出用戶名
String username = token.getUsername();
//調用數據庫方法,從數據表中查詢username對應的記錄
System.out.println("從數據庫中獲取Username:" + username + "對應的用戶信息");
//若用戶不存在,則可以拋出異常
if("unknow".equals(username)){
throw new UnknownAccountException("用戶不存在!");
}
//根據用戶信息的情況,決定是否需要拋出其它異常
if("monster".equals(username)){
throw new LockedAccountException("用戶被鎖定!");
}
/* 根據用戶信息的情況,構建AuthenticationInfo對象并返回,通常使用的實現類是SimpleAuthenticationInfo
* 以下信息是從數據庫中獲取的:
* principal:認證的實體信息,可以是username,也可以是數據表對應的用戶實體類對象
* credentials:密碼
* realmName:當前realm對象的name,調用父類的getName()方法即可
*/
Object principal = username;
Object credentials = "123456";
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,realmName);
return info;
}
}
記得在Spring配置文件中攔截表單請求:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
<!-- 攔截表單請求 -->
/shiroLogin = anon
<!-- 登出 -->
/logout = logout
# everything else requires authentication:
/** = authc
</value>
</property>
登錄成功后跳轉至list.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h4>List Page</h4>
<a href="logout">logout</a>
</body>
</html>
這里實現了一個登出請求,是因為Shiro在登錄成功后會有緩存,此時無論用戶名是否有效,都將成功登錄,所以這里進行一個登出操作。
編寫完成,最后啟動項目即可。
在這里插入圖片描述
若沒有進行登錄,將無法訪問其它頁面,若輸入錯誤的用戶名,則無法成功登錄,也無法訪問其它頁面:
在這里插入圖片描述
若輸入正確的用戶名和密碼,則登錄成功,可以訪問其它頁面:
在這里插入圖片描述
重新來回顧一下上述的認證流程:
密碼校驗的過程
在剛才的例子中,我們實現了在用戶登錄前后對頁面權限的控制,事實上,在程序中我們并沒有去編寫密碼比對的代碼,而登錄邏輯顯然對密碼進行了校驗,可以猜想這一定是Shiro幫助我們完成了密碼的校驗。
我們在UserNamePasswordToken類中的getPassword()方法中打一個斷點:
在這里插入圖片描述
此時以debug的方式啟動項目,在表單中輸入用戶名和密碼,點擊登錄,程序就可以在該方法處暫停運行:
在這里插入圖片描述
我們往前找在哪執行了密碼校驗的邏輯,發現在doCredentialsMatch()方法:
在這里插入圖片描述
再觀察右邊的參數:
在這里插入圖片描述
這不正是我在表單輸入的密碼和數據表中查詢出來的密碼嗎?由此確認在此處Shiro幫助我們對密碼進行了校驗。
在往前找找可以發現:
在這里插入圖片描述
Shiro實際上是用CredentialsMatcher對密碼進行校驗的,那么為什么要大費周章地來找CredentialsMatcher呢?
CredentialsMatcher是一個接口,我們來看看它的實現類:
在這里插入圖片描述
那么相信大家已經知道接下來要做什么了,沒錯,密碼的加密,而加密就是通過CredentialsMatcher來完成的。
MD5加密
加密算法其實有很多,這里以md5加密為例。
修改Spring配置文件中對自定義Realm的配置:
<bean id="myRealm" class="com.wwj.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<!-- 指定加密次數 -->
<property name="hashIterations" value="5"/>
</bean>
</property>
</bean>
這里因為Md5CredentialsMatcher類已經過期了,Shiro推薦直接使用HashedCredentialsMatcher。
這樣配置以后,從表單中輸入的密碼就能夠自動地進行MD5加密,但是從數據表中獲取的密碼仍然是明文狀態,所以還需要對該密碼進行MD5加密:
public static void main(String[] args) {
String algorithmName = "MD5";
Object credentials = "123456";
Object salt = null;
int hashIterations = 5;
Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
該代碼可以參考Shiro底層實現,我們以Shiro同樣的方式對其進行MD5加密,兩份密碼都加密完成了,以debug運行項目,再次找到Shiro校驗密碼的地方:
在這里插入圖片描述
我在表單輸入的密碼是123456,經過校驗發現,兩份密碼的密文是一致的,所以登錄成功。
考慮密碼重復的情況
剛才對密碼進行了加密,進一步解決了密碼的安全問題,但又有一個新問題擺在我們面前,倘若有兩個用戶的密碼是一樣的,這樣即使進行了加密,因為密文是一樣的,這樣仍然會有安全問題,那么能不能夠實現即使密碼一樣,但生成的密文卻可以不一樣呢?
當然是可以的,這里需要借助一個credentialsSalt屬性(這里我們假設以用戶名為標識進行密文的重新加密):
public static void main(String[] args) {
String algorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("aaa");
//Object salt = ByteSource.Util.bytes("bbb");
int hashIterations = 5;
Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
通過該方式,我們生成了兩個不一樣的密文,即使密碼一樣:
c8b8a6de6e890dea8001712c9e149496
3d12ecfbb349ddbe824730eb5e45deca
既然這里對加密進行了修改,那么在表單密碼進行加密的時候我們也要進行修改:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//將參數轉回UsernamePasswordToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//從UsernamePasswordToken中取出用戶名
String username = token.getUsername();
//調用數據庫方法,從數據表中查詢username對應的記錄
System.out.println("從數據庫中獲取Username:" + username + "對應的用戶信息");
//若用戶不存在,則可以拋出異常
if("unknow".equals(username)){
throw new UnknownAccountException("用戶不存在!");
}
//根據用戶信息的情況,決定是否需要拋出其它異常
if("monster".equals(username)){
throw new LockedAccountException("用戶被鎖定!");
}
/* 根據用戶信息的情況,構建AuthenticationInfo對象并返回,通常使用的實現類是SimpleAuthenticationInfo
* 以下信息是從數據庫中獲取的:
* principal:認證的實體信息,可以是username,也可以是數據表對應的用戶實體類對象
* credentials:密碼
* realmName:當前realm對象的name,調用父類的getName()方法即可
*/
Object principal = username;
Object credentials = null;
//對用戶名進行判斷
if("aaa".equals(username)){
credentials = "c8b8a6de6e890dea8001712c9e149496";
}else if("bbb".equals(username)){
credentials = "3d12ecfbb349ddbe824730eb5e45deca";
}
String realmName = getName();
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
return info;
}
這樣就輕松解決了密碼重復的安全問題了。
多Relam的配置
剛才實現的是單個Relam的情況,下面來看看多個Relam之間的配置。
首先自定義第二個Relam:
package com.wwj.shiro.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
public class ShiroRealm2 extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("ShiroRealm2...");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
System.out.println("從數據庫中獲取Username:" + username + "對應的用戶信息");
if("unknow".equals(username)){
throw new UnknownAccountException("用戶不存在!");
}
if("monster".equals(username)){
throw new LockedAccountException("用戶被鎖定!");
}
Object principal = username;
Object credentials = null;
if("aaa".equals(username)){
credentials = "ba89744a3717743bef169b120c052364621e6135";
}else if("bbb".equals(username)){
credentials = "29aa55fcb266eac35a6b9c1bd5eb30e41d4bfd8d";
}
String realmName = getName();
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
return info;
}
public static void main(String[] args) {
String algorithmName = "SHA1";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("bbb");
int hashIterations = 5;
Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
}
這里簡單復制了第一個Relam的代碼,并將加密方式改為了SHA1。
接下來修改Spring的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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.xsd">
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<!-- 添加此處配置 -->
<property name="authenticator" ref="authenticator"/>
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
<!-- 添加此處配置 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="myRealm"/>
<ref bean="myRealm2"/>
</list>
</property>
</bean>
<bean id="myRealm" class="com.wwj.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="5"/>
</bean>
</property>
</bean>
<!-- 添加此處配置 -->
<bean id="myRealm2" class="com.wwj.shiro.realms.ShiroRealm2">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"/>
<property name="hashIterations" value="5"/>
</bean>
</property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiroLogin = anon
/logout = logout
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
注釋的地方就是需要修改的地方。
此時我們啟動項目進行登錄,查看控制臺信息:
在這里插入圖片描述
可以看到兩個Relam都被調用了。
認證策略
既然有多個Relam,那么就一定會有認證策略的區別,比如多個Relam中是一個認證成功即為成功還是要所有Relam都認證成功才算成功,Shiro對此提供了三種策略:
默認使用的策略是AtLeastOneSuccessfulStrategy,具體可以通過查看源碼來體會。
若要修改默認的認證策略,可以修改Spring的配置文件:
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="myRealm"/>
<ref bean="myRealm2"/>
</list>
</property>
<!-- 修改認證策略 -->
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
</property>
</bean>
授權
授權也叫訪問控制,即在應用中控制誰訪問哪些資源,在授權中需要了解以下幾個關鍵對象:
下面實現一個案例來感受一下授權的作用,新建aaa.jsp和bbb.jsp文件,并修改list.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h4>List Page</h4>
<a href="aaa.jsp">aaa Page</a>
<br/>
<br/>
<a href="bbb.jsp">bbb Page</a>
<br/>
<br/>
<a href="logout">logout</a>
</body>
</html>
現在的情況是登錄成功之后就能夠訪問aaa和bbb頁面了:
在這里插入圖片描述
但是我想實現這樣一個效果,只有具備當前用戶的權限才能夠訪問到指定頁面,比如我以aaa用戶的身份登錄,那么我將只能訪問aaa.jsp而無法訪問bbb.jsp;同樣地,若以bbb用戶的身份登錄,則只能訪問bbb.jsp而無法訪問aaa.jsp,該如何實現呢?
實現其實非常簡單,修改Sping的配置文件:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiroLogin = anon
/logout = logout
<!-- 添加角色過濾器 -->
/aaa.jsp = roles[aaa]
/bbb.jsp = roles[bbb]
# everything else requires authentication:
/** = authc
</value>
</property>
啟動項目看看效果:
在這里插入圖片描述
這里有一個坑,就是在編寫授權之前,你需要將Relam的引用放到securityManager中:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="realms">
<list>
<ref bean="myRealm"/>
<ref bean="myRealm2"/>
</list>
</property>
</bean>
否則程序將無法正常運行。
現在雖然把權限加上了,但無論你是aaa用戶還是bbb用戶,你都無法訪問到頁面了,Shiro都自動跳轉到了無權限頁面,我們還需要做一些操作,對ShiroRelam類進行修改:
package com.wwj.shiro.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.HashSet;
import java.util.Set;
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
System.out.println("從數據庫中獲取Username:" + username + "對應的用戶信息");
if("unknow".equals(username)){
throw new UnknownAccountException("用戶不存在!");
}
if("monster".equals(username)){
throw new LockedAccountException("用戶被鎖定!");
}
Object principal = username;
Object credentials = null;
if("aaa".equals(username)){
credentials = "c8b8a6de6e890dea8001712c9e149496";
}else if("bbb".equals(username)){
credentials = "3d12ecfbb349ddbe824730eb5e45deca";
}
String realmName = getName();
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
return info;
}
/**
* 授權時會被Shiro回調的方法
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//獲取登錄用戶的信息
Object principal = principalCollection.getPrimaryPrincipal();
//獲取當前用戶的角色
Set<String> roles = new HashSet<>();
roles.add("aaa");
if("bbb".equals(principal)){
roles.add("bbb");
}
//創建SimpleAuthorizationInfo,并設置其roles屬性
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
return info;
}
}
首先將繼承的類做了修改,改為繼承AuthorizingRealm類,可以通過實現該類的doGetAuthenticationInfo()方法完成認證,通過doGetAuthorizationInfo()方法完成授權,所以源代碼不用動,直接添加下面的doGetAuthorizationInfo()方法即可,看運行效果:
在這里插入圖片描述
可以看到aaa用戶只能訪問到aaa.jsp而無法訪問bbb.jsp,但是bbb用戶卻能夠訪問到兩個頁面,如果你仔細觀察剛才添加的方法你就能夠明白為什么。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//獲取登錄用戶的信息
Object principal = principalCollection.getPrimaryPrincipal();
//獲取當前用戶的角色
Set<String> roles = new HashSet<>();
roles.add("aaa");
if("bbb".equals(principal)){
roles.add("bbb");
}
//創建SimpleAuthorizationInfo,并設置其roles屬性
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
return info;
}
因為不管是什么用戶登錄,我都將aaa用戶添加到了roles中,所以bbb用戶是具有aaa用戶權限的,權限完全是由你自己控制的,想怎么控制你就怎么寫。
注解實現授權
先來看看關于授權的幾個注解:
把Spring配置文件中的角色過濾器刪掉,然后定義一個Service:
package com.wwj.shiro.service;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Service;
@Service
public class ShiroService {
@RequiresRoles({"aaa"})
public void test(){
System.out.println("test...");
}
}
在test()方法上添加注解@RequiresRoles({"aaa"}),意思是該方法只有aaa用戶才能訪問,接下來在ShiroHandler中添加一個方法:
@Autowired
private ShiroService shiroService;
@RequestMapping("/testAnnotation")
public String testAnnotation(){
shiroService.test();
return "redirect:/list.jsp";
}
此時當你訪問testAnnotation請求時,只有aaa用戶能夠成功訪問,bbb用戶就會拋出異常。
——翻譯 崔傳新
歡迎來到Apache Shiro的10分鐘教程!
通過閱讀這個快速簡單的教程,您將充分了解開發人員如何在其應用程序中使用Shiro。 而且你應該可以在10分鐘內做到這一點。
什么是Apache Shiro?
Apache Shiro是一個功能強大且易于使用的Java安全框架,為開發人員提供了一個直觀而全面的解決方案,用于身份驗證、授權、加密和會話管理。
實際上,它實現了管理應用程序安全性的所有方面,同時盡可能避免出現問題。 它建立在完善的接口驅動設計和面向對象的原則之上,可以在任何你想象得到的地方實現自定義行為。 但是,對于所有事情來說,默認情況下都是合理的,這與應用程序安全性是一樣的。 至少這是我們所追求的。
Apache Shiro能做什么?
很多 。 但我們不想擴張"快速入門"的內容。 如果您想了解它可以為您做什么,請查看我們的功能頁面。 此外,如果您對我們如何開始以及為什么存在感到好奇,請參閱Shiro History and Mission頁面。
ok,現在我們來做一些事情吧!
說明:Shiro可以在任何環境下運行,從最簡單的命令行應用程序到最大的企業Web和集群應用程序,但是我們將在這個快速入門(QuickStart)中使用一個簡單的"main"方法來完成一個最簡單的例子,以便可以獲得對API的應用體驗。
1)確保您安裝了JDK1.6+和Maven 3.0.3+。
2)從下載頁面下載最新的Shiro"源代碼分發"包。 在這個例子中,我們使用1.3.2發行版本。
3)解壓源代碼包
$ unzip shiro-root-1.3.2-source-release.zip
4)進入quickstart目錄
$ cd shiro-root-1.3.2/samples/quickstart
5)運行QuickStart
$ mvn compile exec:java
這個目標只會打印出一些日志消息,讓你知道發生了什么,然后退出——(主要Maven構建項目的過程,包括下載一些有關的組件,下載后,下次運行就不再次下載了)。 在閱讀本快速入門指南時,請隨時查看samples / quickstart / src / main / java / Quickstart.java下的代碼。 根據需要隨時更改該文件并運行上述mvn compile exec:java命令。
備注說明:
我這里在windows8平臺下運行體驗的,環境為JDK1.8+maven3.5;步驟同上。
為了知道運行結果,我在源代碼推出前位置加了一行代碼,以觀測效果:
log.info("\n====This is Quikstart Exampel.It is done!====");//增加代碼行
結果示意圖:
上面引用的Quickstart.java文件包含了所有可以幫助您熟悉API的代碼。 現在讓我們把它分成大塊,這樣你就可以很容易地理解發生了什么。
幾乎在所有環境中,您都可以通過以下調用獲取當前正在執行的用戶:
Subject currentUser = SecurityUtils.getSubject();
使用SecurityUtils.getSubject(),我們可以獲得當前正在執行的Subject。 主體只是應用程序用戶的特定安全"視圖"。 我們實際上想稱它為'User(用戶)',因為這"有道理",但我們決定不這么干:太多的應用程序都有現有的API,它們已經擁有自己的User類/框架,我們不想與這些API沖突。 另外,在安全領域,術語Subject實際上是公認的命名法。 ok,繼續...
獨立應用程序中的getSubject()調用,可能會根據特定于應用程序的位置中的用戶數據以及服務器環境(例如Web應用程序)返回相應Subject,并根據與當前線程或傳入請求關聯的用戶數據而獲取Subject 。
現在你有一個主題,你可以用它做什么?
如果您想在應用程序的當前會話期間向用戶提供可用的內容,則可以獲得他們的會話:
Session session = currentUser.getSession(); session.setAttribute( "someKey", "aValue" );
Session是一個Shiro特定的實例,它給你提供了大多數習慣的常規HttpSession實例,但有一些額外的好處和一個很大的區別:它不需要HTTP環境!
如果在Web應用程序內部署,默認情況下會話將基于HttpSession。 但是,在非Web環境中,就像這個簡單的快速入門一樣,Shiro默認會自動使用它的企業會話管理。 這意味著無論部署環境如何,您都可以在應用程序中的任何層中使用相同的API。 這將打開一個全新的應用程序世界,因為任何需要會話的應用程序都不需要強制使用HttpSession或EJB Stateful Session Beans。 而且,任何客戶端技術現在都可以共享會話數據。
所以現在你可以獲得一個Subject和他們的Session。 那些真正有用的東西比如檢查是否允許他們做事情,比如檢查角色和權限?
那么,我們只能對已知的用戶進行這些檢查。 上面的Subject實例代表當前用戶,但誰是當前用戶? 其實,他們是匿名的 - 也就是說,直到他們登錄至少一次。 所以,讓我們這樣做:
if ( !currentUser.isAuthenticated() ) {
//以gui特定方式收集用戶主體和憑證-principals and credentials
//如html表單的用戶名/密碼,X509證書,OpenID等。
//我們將在這里使用用戶名/密碼示例,因為它是最常見的。
//(你知道這是什么電影嗎?;)
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//this is all you have to do to support 'remember me' (no config - built in!):
//這就是你需要做的所有事情以便來支持'記住我'(沒有配置 - 內置!)
token.setRememberMe(true);
currentUser.login(token);
}
代碼截圖
就這樣! 應用起來不可能更容易了。
但是,如果他們的登錄嘗試失敗呢? 你可以捕捉各種具體的例外情況,告訴你到底發生了什么,并允許你相應地處理和做出反應:
try {
currentUser.login( token );
//如果沒有例外,就是這樣,搞定!
} catch ( UnknownAccountException uae ) {
//用戶名不在系統中,如何向他們顯示錯誤消息?
} catch ( IncorrectCredentialsException ice ) {
//密碼不匹配,是否再試?
} catch ( LockedAccountException lae ) {
//該用戶名的帳戶被鎖定 - 無法登錄。如何顯示一條消息?
}
... 更多類型的異常檢查——如果你想要 ...
} catch ( AuthenticationException ae ) {
//意外情況 - 怎么處理?
}
代碼截圖
您可以檢查許多不同類型的例外情況,或者拋出Shiro可能無法解釋的自定義異常情況。 有關更多信息,請參閱AuthenticationException JavaDoc。
ok,到現在為止,我們有一個登錄用戶。 我們還能做什么?
讓我們看看他們是誰:
//打印他們的標識主體(在這種情況下是用戶名)
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );
我們也可以測試它們是否具有特定的角色:
if ( currentUser.hasRole( "schwartz" ) ) {
log.info("May the Schwartz be with you!" );
} else {
log.info( "Hello, mere mortal." );
}
我們還可以看到他們是否有權對某種類型的實體采取行動:
if ( currentUser.isPermitted( "lightsaber:weild" ) ) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
另外,我們可以執行非常強大的實例級權限檢查 - 查看用戶是否有權訪問特定類型實例的功能:
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
小菜一碟,對吧?
最后,當用戶完成使用應用程序時,他們可以注銷:
currentUser.logout(); //刪除所有標識信息并使其會話無效。
那么,這就是在應用程序開發人員級別使用Apache Shiro的核心。 雖然有一些非常復雜的東西在引擎蓋下進行,使得這項工作如此優雅,但這確實是它的全部。
但是你可能會問自己,"但是誰負責在登錄時獲取用戶數據(用戶名和密碼,角色和權限等),以及誰在運行時真正執行這些安全檢查?",這么問就對了——你來做:通過實施 Shiro稱之為Realm的東西,并將該Realm插入到Shiro的配置中來完成。
但是,如何配置Realm很大程度上取決于您的運行時環境。 例如,如果運行獨立應用程序,或者如果您有基于Web的應用程序,或基于Spring或JEE容器的應用程序或其組合, 這種類型的配置不在本快速入門的范圍之內,因為它的目的是讓您對API和Shiro的概念感到滿意。
當您準備好了解更多細節時,您一定要閱讀認證指南和授權指南。 然后可以轉到其他文檔,特別是參考手冊中,以回答任何其他問題。 您也可能想要加入用戶郵件列表 - 您會發現我們有一個非常棒的社區,只要有可能,他們都愿意提供幫助。
感謝您的關注。 我們希望您喜歡使用Apache Shiro!
下次分享更高層級的實戰應用案例。
Shiro架構
*請認真填寫需求信息,我們會在24小時內與您取得聯系。