(SpringBoot)Shiro安全框架深入解析
最近在學習Shiro安全框架的使用,深入研究其原理,讓自己更得心應手的使用這個框架。
內容目錄
- Shiro的整體架構介紹
- 框架驗證流程與原理分析
- Url匹配模式
- 加密機制
- 快取機制
1.Shiro的整體架構介紹
1.1從使用者角度看Shiro架構
ApplicationCode為客戶端,在Web環境中為登入的Controller,使用者只需要建立一個Subject物件,呼叫其上的login()方法,即可完成登入。在使用者角度只需要在SpringIOC容器中配置ShiroSecurityManager注入Realm即可簡單使用,其中原理下面會提到。
•Subject:主體,代表了當前“使用者”,這個使用者不一定是一個具體的人,與當前應用互動的任何東西都是
•SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理著所有Subject;可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器;
•Realm:域,Shiro從從
在使用者的角度來說,只需要知道這三個東西即可完成簡單的身份認證。
1.2從架構師角度看Shiro架構
從上圖可以看到,Subject可以是任何東西建立的,而其中認證的心臟其實是SecurityManager,不管是Subject的login方法還是Session操作還是判斷是否有具體某個角色、許可權的方法,其中都是呼叫底層的SecurityManager,SecurityManager裡支援所有Shiro內建功能,包括認證器(可以在其中設定認證策略)、Reaml的設定和呼叫、SessionManager的管理、快取機制的實現,所以SecurityManager被稱為整個Shiro架構的心臟,大部分的功能全是在這裡實現。而Reaml更像是一個數據源,在這裡獲取身份認證,授權資訊,可以從各種方式獲取,例如Oracle資料庫Mysql資料庫亦或是ini配置檔案等等。Realm被配置在SecurityManager中,在登入授權等等操作時會呼叫配置的Realm(資料來源)。
2.框架驗證流程與原理分析
2.1身份認證流程
其實在上面已經講過了,結合上圖更清楚的可以看出,整個流程就是在客戶端呼叫Subject物件的login方法,裡面傳入一個引數token,這個token就是前端使用者輸入的賬號密碼,封裝成token物件傳入即可,底層還是SecurityManager呼叫認證器(第三步),在認證器中選擇具體認證策略(第四步),最後去Realms(資料來源)中驗證使用者是否存在等等。
在這裡值得一提的是,上圖Realms或許不止一個,這是因為Shiro可以支援多個Realms,即多個數據源,上面也提到,可以去Oracle找資料,也可以去Mysql找資料或者其他什麼途徑,使用者資料可能存在各個地方,這個時候就可以配置多個Realm,這裡就引申出認證策略的問題,也就是上圖中的第四步,Shiro中預設的認證策略為至少一個Realm認證成功即視為成功,使用場景是使用者資訊可能被存放在多個地方,此時只需要找到一個地方匹配了使用者的資訊就算登入成功。這裡認證策略還有AllSuccess即全部Realm認證成功才視為成功等等的認證策略。
2.2在SpringIOC需要配置的一些Bean
package com.shiro.shiroConfig;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import net.sf.ehcache.hibernate.EhCacheRegionFactory;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.shiro.filter.URLPathMatchingFilter;
import com.shiro.realm.DatabaseRealm;
@Configuration
public class ShiroConfiguration {
/**
* 這是管理Shiro生命週期的Bean
*
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* ShiroFilterFactoryBean 處理攔截資原始檔問題。
* 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,因為在
* 初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager
*
* Filter Chain定義說明 1、一個URL可以配置多個Filter,使用逗號分隔 2、當設定多個過濾器時,全部驗證通過,才視為通過
* 3、部分過濾器可指定引數,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
// SimpleHash sh = new SimpleHash("MD5", "123456", null, 3);
// System.out.println(sh);
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設定 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登入成功後要跳轉的連結
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授權介面;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
// 攔截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 自定義攔截器
Map<String, Filter> customisedFilter = new HashMap<>();
customisedFilter.put("url", getURLPathMatchingFilter());
// 配置對映關係
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/config/**", "anon");
filterChainDefinitionMap.put("/doLogout", "logout");
filterChainDefinitionMap.put("/deleteOrder", "roles[admin]");
//filterChainDefinitionMap.put("/**", "anon");
filterChainDefinitionMap.put("/**", "url");
shiroFilterFactoryBean.setFilters(customisedFilter);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
public URLPathMatchingFilter getURLPathMatchingFilter() {
return new URLPathMatchingFilter();
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設定realm.
securityManager.setRealm(getDatabaseRealm());
//設定快取
securityManager.setCacheManager(getCacheManager());
return securityManager;
}
@Bean
public DatabaseRealm getDatabaseRealm() {
DatabaseRealm myShiroRealm = new DatabaseRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
* 所以我們需要修改下doGetAuthenticationInfo中的程式碼; )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 雜湊演算法:這裡使用MD5演算法;
hashedCredentialsMatcher.setHashIterations(2);// 雜湊的次數,比如雜湊兩次,相當於
// md5(md5(""));
return hashedCredentialsMatcher;
}
/**
*
* 快取框架
* @return
*/
@Bean
public EhCacheManager getCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
return ehCacheManager;
}
/**
* 開啟shiro aop註解支援. 使用代理方式;所以需要開啟程式碼支援;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
大致講一下需要在SpringIOC容器需要配置的一些bean。
- LifecycleBeanPostProcessor:沒什麼好說,它管理Shiro的生命週期。
- ShiroFilterFactoryBean:運用了工廠模式,這個Bean需要注入一系列依賴,有Shiro的心臟SecurityManager、登入的頁面url設定、登入後要跳轉的url設定、未授權的頁面的url設定、攔截器鏈的設定。
- SecurityManager:Shiro的心臟,由上面架構介紹可以知道,認證器、快取器、Realm資料來源都在這裡配置注入,這裡我沒有用到認證策略,使用預設認證器即可,這裡我注入了快取器和Realm,以便之後認證底層使用到。
- DatabaseRealm:這個是我自定義的Realm,new出自定義類,因為我這裡還使用到了密碼機制(在下面會詳細介紹),所以注入一個Shiro自帶的密碼器。為什麼在Realm中配置密碼器呢,可以這樣理解,Realm是我們獲取身份資訊的資料來源,則每一個獲取身份資訊(例如賬號密碼)都是獨立的一種策略,如Oracle是用MD5加密儲存密碼,MySQL又是用其他加密演算法,而Realm負責提供資料,所以是在這裡配置的密碼演算法。配置此Realm用於注入上面宣告的“心臟”中。
- hashedCredentialsMatcher:Shiro自帶的密碼器,我這裡使用其中一種,雜湊加密演算法,其中設定其屬性為演算法名稱和雜湊次數,在上面註釋中應該寫的很清楚了。配置此密碼器用在注入上面我們宣告的自定義Realm中。
- CacheManager:快取管理器,用於注入與“心臟”中。這裡我使用的是Ehcache框架實現快取,Shiro也有對應的JAR包,new一個管理器,設定屬性為快取配置檔案用於讀取快取設定。
2.3具體底層的流程(部分原始碼分析)
那麼,具體的流程是怎麼樣的呢?我們可以啟動專案看看控制檯。
可以看出,在tomcat啟動時,自動給我們加上了一個過濾器---shiroFilter,攔截一切請求(/*),這個過濾器就是我們在SpringIOC容器中配置的過濾器鏈(在上面配置檔案中註釋可以看到filterChainDefinitions)具體攔截模式下面會介紹到。
由上圖應該大致可以看出來了吧?瀏覽器在訪問頁面時,總會被我們配置好了的ShiroFilter攔截,在這個攔截器中呼叫配置好了的對應的攔截器鏈,根據攔截模式呼叫具體的攔截器(filterChainDefinitions中配置的各種比如anon、roles等等都是攔截器)。如果是沒有經過認證的會重定向到我們配置好的登入URL。
登入控制器
package com.shiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* @Author:linyh
* @Date: 2018/8/11 19:24
* @Modified By:
*/
@Controller
@RequestMapping("")
public class LoginControlller {
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(Model model, String name, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
subject.login(token);
Session session = subject.getSession();
session.setAttribute("subject", subject);
return "redirect:index";
} catch(IncorrectCredentialsException e){
model.addAttribute("error", "密碼錯誤");
return "login";
} catch(AuthenticationException e) {
model.addAttribute("error", "驗證失敗");
return "login";
}
}
}
所有未經認證的路徑都能請求到這個Controller中,也就是我們之前配置好了的過濾鏈中的/login,這個路徑也是anon過濾器,其含義為可以匿名訪問,不需要進行認證。
這裡面的程式碼邏輯就是我們上面說過的,從Shiro的工具類呼叫靜態方法獲取一個Subject(這裡的Subject為執行緒獨有的),然後new一個UsernamePasswordToken,顧名思義就是存放使用者名稱和密碼的,在其構造器中傳入使用者名稱與密碼即可。接著呼叫Subject的login方法,傳入token完成登入。如果密碼錯誤底層丟擲IncorrectCredentialsException異常,可以捕捉異常然後重定向到你想要去的頁面,可以去一個錯誤頁面,也可以還在登入頁面中,在model存放一個錯誤資訊,在view中顯示此資訊即可。這裡值得一提的是Shiro封裝的一些異常類。如果身份驗證失敗請捕獲AuthenticationException或其子類,常見的如: DisabledAccountException(禁用的帳號)、LockedAccountException(鎖定的帳號)、UnknownAccountException(錯誤的帳號)、ExcessiveAttemptsException(登入失敗次數過多)、IncorrectCredentialsException (錯誤的憑證)、ExpiredCredentialsException(過期的憑證)等,具體請檢視其繼承關係;對於頁面的錯誤訊息展示,最好使用如“使用者名稱/密碼錯誤”而不是“使用者名稱錯誤”/“密碼錯誤”,防止一些惡意使用者非法掃描帳號庫;
底層login方法都幹了什麼?
我們進入login方法看一看~
這裡的login方法歸根結底還是呼叫了我們配置的Shiro心臟嘛!方法傳入兩個引數一個為當先執行緒的Subject,一個為使用者輸入的賬號密碼(封裝為token傳入)。接著進去看看~
這裡心臟的某個實現類呼叫了其上父類的一個認證方法,繼續進去看看~
它呼叫了認證器的一個認證方法,這裡認證器是new出來的一個認證器
繼續進去看看吧~
在這裡token如果是空則會丟擲一個異常。
這是一個認證器抽象類,繼續呼叫其實現了認證方法的子類的認證方法~進去看看
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
這就是上面new出的一個認證器中的認證方法,走了這麼多步,這裡才真正準備開始實現認證,這裡先呼叫this.getRealms獲取Realms,因為我們已經在心臟中依賴注入了一個Realms,所以這裡會自動獲取到我們的自定義Realm,接下來判斷size是否為1,其實就是在判斷是否是多Realm模式,我們這裡是單Realm,所以會進入doSingleRealm方法中,傳入兩個引數,第一個呼叫集合的迭代器傳入這個唯一的Realm, 第二個傳入我們之前封裝的token。繼續進去看看~
如果token不支援這個realm,會丟擲一個異常。
在這裡,呼叫realm中的獲取認證類方法,如果獲取不到具體認證類,也就是為null,丟擲一個異常為找不到對應的Realm獲取認證物件。下面繼續進去看看~
首先注意此類名,就是我們自定義Realm所繼承的類,也就是說此抽象類將會呼叫它的子類也就是我們的自定義類的doGetAuthenticationInfo方法。
值得一提的是看第二個紅框,這裡認證物件會先從快取中獲取,如果獲取不到才會去自定義Realm中獲取。繼續進去看看~
這裡的DatabaseRealm就是我們自定義的Realm,最終呼叫了我們自己寫的這個方法,返回一個認證的物件,此認證物件用SimpleAuthenticationInfo這個物件封裝,裡面放入資料庫中的賬號、密碼(如有用到密碼機制中的鹽,也可傳入鹽引數)、最後一個引數為本Realm的名稱,直接呼叫getName方法即可。
這裡封裝好了這個資料庫的使用者認證物件拿來幹什麼用的呢?回到我們上一個父類Realm中。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
if (info == null) {
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
在這裡可以看到,呼叫完我們自己定義的Realm的方法返回的一個認證例項,將會與前端輸入的token作為兩個引數傳入assertCredentialsMatch方法中,這個方法才是主要驗證的方法,進去看看吧
在這裡可以看到,在驗證資訊是否正確的時候,先獲取密碼器,也就是我們之前就已經在Realm中注入好了的那個密碼器(如果無需密碼器也可以,後面就只是明文比對,沒有加密的過程),接著呼叫這個密碼器去驗證賬號是否正確,進入驗證方法看看~
這裡即為最終BOSS,最後底層去呼叫equals把這個token(前端使用者輸入的資訊),和Realm資料庫得到的賬號資訊進行比對,如果為false即為不匹配,在上面的上面也可以看到,如果為false就會丟擲一個密碼錯誤的異常,這個異常在上面也提到過,如果匹配,返回true,則程式繼續執行下去,做一些必要的配置,我們只需要瞭解這一個過程即可瞭解Shiro底層實現。
原始碼總結
首先在客戶端呼叫Subject的login方法,其實就是在呼叫心臟中的login方法,而這個方法歷經千辛萬苦最終呼叫到自定義的Realm中的認證方法,返回一個認證例項,在用這個認證例項與token作對比,在這個過程中,認證失敗會直接丟擲異常,而認證如果成功則不會丟擲異常,程式正常走下去,所以可以在客戶端捕獲異常後重定向到index(你所想要登入之後跳轉的首頁)。
閱讀原始碼,我們可以知道,只要配置了Realm,其實就大致完成了整個的認證過程,其底層無非就是呼叫裡面的方法獲取認證例項,其餘工作都交給Shiro來完成。接著在SpringIOC容器中配置必要的bean,注入必要的依賴就可以說已經完成了認證。
從這個總結中就可以大致知道如何編寫我們的自定義Realm類了。
package com.shiro.realm;
import java.util.Set;
import org.apache.shiro.SecurityUtils;
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 org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.shiro.pojo.User;
import com.shiro.service.PermissionService;
import com.shiro.service.RoleService;
import com.shiro.service.UserService;
/**
* @Author:linyh
* @Date: 2018/8/11 18:26
* @Modified By:
*/
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
/**
* @Description:授權使用者角色與許可權
*
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();
// 通過Service獲取角色與許可權集合
Set<String> permissions = permissionService.listPermissions(userName);
Set<String> roles = roleService.listRoleNames(userName);
// 授權物件
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 把通過Service獲取到的角色和許可權放進去
authorizationInfo.setStringPermissions(permissions);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
/**
* @Description:
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 獲取賬號密碼
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//String userName = authenticationToken.getPrincipal().toString();
String userName = token.getUsername();
User user = userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
//這裡鹽值可以是主鍵
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
ByteSource.Util.bytes(salt), getName());
return authenticationInfo;
}
/**
* @Description:清除快取
* @Param:
* @return:
*/
public void clearCache(){
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
理解了原始碼,就可以知道首先就是繼承我們的AuthorizingRealm類(當然,如果你只想要做身份認證的話,可以繼承另一個身份認證Realm,我這裡需要做身份授權與身份認證),底層會呼叫這個抽象類的一系列方法,而我們只需要繼承它,實現兩個do方法即可,輕鬆簡單。而兩個do方法返回認證例項,為什麼返回認證例項就可以完成認證的原理大家也都知道了把,無非就是把這個例項和token在抽象父類中的一個方法中拿著注入的密碼器進行比較,返回比較結果。
3.Url匹配模式
3.1shiroFilter中過濾器鏈配置細節
•[urls]部分的配置,格式為:“url=攔截器[引數],攔截器[引數]…”
•如果當前請求的url匹配[urls]部分的某個url模式,將會執行其配置的攔截器。
•anon(anonymous)攔截器 表示匿名訪問,不需要登入即可訪問
•authc(authentication)攔截器表示需要身份認證通過後才能訪問
•還有一些shiro自帶的攔截器…
3.2shiro預設的過濾器
介紹比較常用的幾個把。
- anon:這個過濾器可以使對應url無需認證直接訪問。
- authc:這個過濾器表示需要進行認證(即登入之後才可以訪問的頁面)。
- logout:這個過濾器可以使你的賬號退出(如有快取清除快取)。
- roles:這個過濾器可以有引數,為具體角色名,而角色名在Realm中獲取,主要比較是否有此角色,擁有此角色才可訪問此url。
- perms:與roles大同小異,比較是否有此許可權,擁有此許可權才可訪問此url。
3.3Url匹配模式(Ant表示式)
•Ant路徑萬用字元支援?、*、**,注意萬用字元匹配不包括目錄分隔符“/”:
•-?:匹配一個字元,如/admin?可以匹配/admin1,但不匹配/admin或/admin/1;
•-*:匹配零個或多個字串,如/admin將匹配/admin、/admin12,但不匹配/admin/1;
•-**:匹配路徑中的零個或多個路徑,如/admin/**將匹配/admin/a或/admin/a/b
3.4Url匹配順序
•Url許可權採取第一次匹配優先的方式,即從頭開始使用第一個匹配的url模式對應的攔截器鏈。
•如:
•-/bb/**=filter1
•-/bb/aa=filter2
•-/**=filter3
•-如果請求的url是“/bb/aa”,因為按照宣告順序進行匹配。那麼將使用filter1進行攔截。
4.加密機制
在上文已經有提到,Shiro自帶一些加密的工具以供我們對密碼進行加密,我們只需要把加密工具配置在資料來源(Realm)中即可完成加密驗證。
@Bean
public DatabaseRealm getDatabaseRealm() {
DatabaseRealm myShiroRealm = new DatabaseRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
其將在認證時自動使用此密碼器將前端收到的密碼進行加密,而後與資料庫密碼進行比對,故此可以理解是配置在每個Realm中的原因了。Realm是獲取認證資訊的地方,所以每個Realm將會有不同的加密策略。
/**
* 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
* 所以我們需要修改下doGetAuthenticationInfo中的程式碼; )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 雜湊演算法:這裡使用MD5演算法;
hashedCredentialsMatcher.setHashIterations(2);// 雜湊的次數,比如雜湊兩次,相當於
// md5(md5(""));
return hashedCredentialsMatcher;
}
在這裡配置具體的加密工具,然後注入到對應的Realm中即可。這裡只需要選擇對應的工具類,設定屬性如加密演算法名稱、加密次數等。
4.1鹽的概念
為什麼要引入鹽的概念呢?鹽是什麼?假設我們用的加密工具沒有鹽,那麼在加密之後,相同的密碼在加密後是相同的,這會有什麼後果呢?比如密碼12345加密後為abcde,在資料庫存放的時候,每個12345的密碼他都是abcde,這是不合理的,這樣破解者就會知道,abcde就是對應12345,反加密變得透明。那麼鹽是什麼呢?它就像炒菜,同一個人用同一個食材做出來的菜其實味道是一樣的,關鍵就在調料放了多少,不同的調料(鹽)可以讓菜的味道就算是由同一個人做出的味道也可以是不同。就像我們的加密過程,同一個密碼(12345),但是它的鹽不同,就算加密演算法相同,加密出來的結果也會是不同,由鹽和演算法兩個決定,這樣反加密就變得不太好做了。
我們通常會用主鍵作為我們的鹽值,因為主鍵唯一且能標識。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 獲取賬號密碼
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//String userName = authenticationToken.getPrincipal().toString();
String userName = token.getUsername();
User user = userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
//這裡鹽值可以是主鍵
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
ByteSource.Util.bytes(salt), getName());
return authenticationInfo;
}
在Realm中,原本直接返回使用者名稱和密碼構造的驗證實體,現在可以傳入一個鹽值,利用ByteSource的工具類來處理鹽值。因為我們上面已經在Realm中配置過加密工具了,所以這裡不加鹽值的話會預設使用"MD5"演算法(上面配置加密工具時設定)進行密碼加密,如果加了鹽值,在演算法加密之後會再進行鹽值加密過程。
4.2註冊
註冊也是加密的一個過程,因為解密加密一定是用的同一個演算法才可以得到要的相同結果。
SimpleHash sh1 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("1"), 3);
SimpleHash sh2 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("1"), 3);
SimpleHash sh3 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("2"), 3);
System.out.println(sh1);
System.out.println(sh2);
System.out.println(sh3);
加密123456,第三個引數為鹽值(可以傳入主鍵作為鹽值),這裡是雜湊加密演算法,所以3為雜湊次數。
從結果可以看出,在使用相同鹽值1的情況下,123456加密的結果都是相同的,而在使用不同的鹽值2的情況下,123456加密的結果就不同了。
由此可以知道,只要在註冊的Service層使用SimpleHash這個類來加密密碼來進行註冊即可。在驗證賬號時Realm會將前端使用者輸入的密碼進行相同的加密演算法,加入相同的鹽(在註冊時加密的鹽,如為主鍵則在資料庫中取,所以也是相同的),匹配兩個加密過的結果是否相同來完成解密過程。
5.快取機制
為什麼要用快取呢?有興趣的可以在Realm中doGetAuthorizationInfo授權方法也就是給使用者配置擁有的角色與許可權的授權方法打一個斷點,然後在頁面上訪問一些需要角色或許可權才能訪問的url,你會發現每次訪問都會進入斷點,這個授權方法中呼叫的Service層的方法,去資料庫拿對應使用者擁有的角色許可權,試想一下,我每訪問一個url,都要去資料庫重複拿這些資料,顯然是一個不成熟的做法,如果有了快取,我們只需要查詢一次資料庫,之後訪問url都將從快取中拿資料而不是資料庫,這樣資料庫壓力明顯降低,看起來也相對成熟。
其次,使用者擁有的角色和許可權這部分資料是極少更改的,可能在配置時就已經定下來了,所以用快取是一個明智且必須的選擇。
5.1快取配置
<!-- shiro-ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.8</version>
</dependency>
ehcache與Shiro整合的依賴
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設定realm.
securityManager.setRealm(getDatabaseRealm());
//設定快取
securityManager.setCacheManager(getCacheManager());
return securityManager;
}
在上文也有提到,快取是配置在SecurityManager中的。這裡我使用了最簡單的用ehcache快取框架來實現快取。
/**
* 快取框架
* @return
*/
@Bean
public EhCacheManager getCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("src/main/java/com/shiro/shiroConfig/shiro-ehcache.xml");
return ehCacheManager;
}
在配置快取框架Bean中new一個EhCacheManager,設定配置檔案路徑。
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
配置完成後,可以再在Realm的授權方法中打上斷點,實驗快取是否有效。可以發現,使用者只在第一次訪問許可權url時才會進入斷點,之後訪問url都不會進入斷點。
5.2清除快取
在使用者正常退出(logout)時快取會自動清理,或是快取框架設定的快取時間過了之後快取也會清理。
如需手動清除快取,例如你修改了一個使用者的許可權,你想要他馬上不能訪問某某關鍵的地址,但有快取的存在即使你修改了許可權也不能立即生效,則可在Realm中定義方法clearCached(),在修改許可權或角色的Service層中的delete、update等等方法中呼叫清除快取方法即可實現修改許可權立即生效。由於角色和許可權的資料不常修改,所以也不會影響多少效能。
/**
* @Description:清除快取
* @Param:
* @return:
*/
public void clearCache(){
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
根本為呼叫父類的清除快取方法,傳入當前執行緒的使用者憑證,清除快取。