文章目錄
- 二、Controller層接收登入請求
- 三、自定義的Realm
- 四、密碼驗證器增加登入次數校驗功能
- 五、ShiroConfig的配置類
- 六、EhCache 的配置
- 七、全局異常的配置
####### 一、 Shiro的執行流程

1、核心介紹
1)Application Code使用者編寫代碼
2)Subject就是shiro管理的使用者
3)SecurityManager安全管理器,就是shiro權限控制核心對象,在程式設計時,隻需要操作Subject方法,底層調用SecurityManager方法,無需直接程式設計操作SecurityManager
4)Realm應用程式和安全資料之間連接配接器,應用程式進行權限控制讀取安全資料(資料表、檔案、網絡…)通過Realm對象完成
2、Shiro執行流程
應用程式(就是你自己的項目)—>Subject—>SecurityManager—>Realm—>安全資料
3、Shiro進行權限控制的四種主要方式
1)在程式中通過Subject程式設計方式進行權限控制
2)配置Filter實作URL級别粗粒度權限控制
3)配置代理,基于注解實作細粒度權限控制
4)在頁面中使用shiro自定義标簽實作,頁面顯示權限控制
Shiro執行登入的流程如下圖.
大緻的思路如下, 在Controller層接收前端輸入的使用者名和密碼. 調用Shiro的SecurityUtils.getSubject()方法擷取Subject對象.
之後用Subject對象調用login方法,其Shiro底層會進行密碼的驗證, 傳入UsernamePasswordToken對象,此對象封裝了前端傳入的使用者名和密碼.
接着Shiro的SecurityManager會去調用自定義的Realm的AuthenticationInfo方法進行登入的驗證, 此方法會傳回一個SimpleAuthenticationInfo對象,此對象封裝了ShiroUser ,資料庫中存儲的目前使用者的密碼, 密碼加鹽的值,Realm的名稱, 即把資料庫中的目前的使用者, 與使用者輸入的使用者名密碼即存儲在 UsernamePasswordToken進行比較,如果密碼正确,登入成功,密碼不正确登入失敗.
調用自定義Realm的AuthenticationInfo完了之後, 調用RetryLimitCredentialsMatcher類中的doCredentialsMatch方法, 進行密碼比對次數的記錄. 并用EhCache作為緩存, 把目前登入的使用者名作為key,key的過期時間按照需求設定即可, 把登入的次數作為值.首先通過使用者名,擷取登入次數,如果登入次數為0, 那麼先給目前使用者設定一個緩存,登入次數+1,之後判斷是否大于限定的登入錯誤次數,如果超過了限定次數,則抛出異常,用全局的異常攔截器,攔截此異常, 記錄登入錯誤次數的異常, 并封裝登入次數過多的提示,給用戶端. 具體的代碼在下面.
二、Controller層接收登入請求
Shiro的工具類ShiroKit
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import java.util.List;
/**
* shiro工具類
*
*
*/
public class ShiroKit {
/**
* 名稱分隔符
*/
private static final String NAMES_DELIMETER = ",";
/**
* 加鹽參數
*/
public final static String HASH_ALGORITHM_NAME = "MD5";
/**
* 循環次數
*/
public final static int HASHITERATIONS = 1024;
/**
* 驗證是否同一個賬号重新登入的屬性,為true代表是重新登入, 初始化為false,代表不是重新登入
*/
public static boolean ISPEATEDLOGIN =false;
/**
* shiro密碼加密工具類
*
* @param credentials 密碼
* @param saltSource 密碼鹽
* @return
*/
public static String md5(String credentials, String saltSource) {
ByteSource salt = new Md5Hash(saltSource);
return new SimpleHash(HASH_ALGORITHM_NAME, credentials, salt, HASHITERATIONS).toString();
}
/**
* 擷取随機鹽值
*
* @param length
* @return
*/
public static String getRandomSalt(int length) {
return ToolUtil.getRandomString(length);
}
/**
* 擷取目前 Subject.
* Subject表示單個應用程式使用者的狀态和安全操作。
* 這些操作包括身份驗證(登入/登出),授權(通路控制)和會話通路。
* 這是Shiro的單使用者安全功能的主要機制。
*
* @return Subject
*/
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
/**
* 擷取封裝的 ShiroUser
*
* @return ShiroUser
*/
public static ShiroUser getUser() {
if (isGuest()) {
return null;
} else {
return (ShiroUser) getSubject().getPrincipals().getPrimaryPrincipal();
}
}
/**
* 從shiro擷取session
*/
public static Session getSession() {
return getSubject().getSession();
}
/**
* 擷取shiro指定的sessionKey
*/
@SuppressWarnings("unchecked")
public static <T> T getSessionAttr(String key) {
Session session = getSession();
return session != null ? (T) session.getAttribute(key) : null;
}
/**
* 設定shiro指定的sessionKey
*/
public static void setSessionAttr(String key, Object value) {
Session session = getSession();
session.setAttribute(key, value);
}
/**
* 移除shiro指定的sessionKey
*/
public static void removeSessionAttr(String key) {
Session session = getSession();
if (session != null)
session.removeAttribute(key);
}
/**
* 驗證目前使用者是否屬于該角色?,使用時與lacksRole 搭配使用
*
* @param roleName 角色名
* @return 屬于該角色:true,否則false
*/
public static boolean hasRole(String roleName) {
return getSubject() != null && roleName != null
&& roleName.length() > 0 && getSubject().hasRole(roleName);
}
/**
* 與hasRole标簽邏輯相反,當使用者不屬于該角色時驗證通過。
*
* @param roleName 角色名
* @return 不屬于該角色:true,否則false
*/
public static boolean lacksRole(String roleName) {
return !hasRole(roleName);
}
/**
* 驗證目前使用者是否屬于以下任意一個角色。
*
* @param roleNames 角色清單
* @return 屬于:true,否則false
*/
public static boolean hasAnyRoles(String roleNames) {
boolean hasAnyRole = false;
Subject subject = getSubject();
if (subject != null && roleNames != null && roleNames.length() > 0) {
for (String role : roleNames.split(NAMES_DELIMETER)) {
if (subject.hasRole(role.trim())) {
hasAnyRole = true;
break;
}
}
}
return hasAnyRole;
}
/**
* 驗證目前使用者是否屬于以下所有角色。
*
* @param roleNames 角色清單
* @return 屬于:true,否則false
*/
public static boolean hasAllRoles(String roleNames) {
boolean hasAllRole = true;
Subject subject = getSubject();
if (subject != null && roleNames != null && roleNames.length() > 0) {
for (String role : roleNames.split(NAMES_DELIMETER)) {
if (!subject.hasRole(role.trim())) {
hasAllRole = false;
break;
}
}
}
return hasAllRole;
}
/**
* 驗證目前使用者是否擁有指定權限,使用時與lacksPermission 搭配使用
*
* @param permission 權限名
* @return 擁有權限:true,否則false
*/
public static boolean hasPermission(String permission) {
return getSubject() != null && permission != null
&& permission.length() > 0
&& getSubject().isPermitted(permission);
}
/**
* 與hasPermission标簽邏輯相反,目前使用者沒有制定權限時,驗證通過。
*
* @param permission 權限名
* @return 擁有權限:true,否則false
*/
public static boolean lacksPermission(String permission) {
return !hasPermission(permission);
}
/**
* 已認證通過的使用者。不包含已記住的使用者,這是與user标簽的差別所在。與notAuthenticated搭配使用
*
* @return 通過身份驗證:true,否則false
*/
public static boolean isAuthenticated() {
return getSubject() != null && getSubject().isAuthenticated();
}
/**
* 未認證通過使用者,與authenticated标簽相對應。與guest标簽的差別是,該标簽包含已記住使用者。。
*
* @return 沒有通過身份驗證:true,否則false
*/
public static boolean notAuthenticated() {
return !isAuthenticated();
}
/**
* 認證通過或已記住的使用者。與guset搭配使用。
*
* @return 使用者:true,否則 false
*/
public static boolean isUser() {
return getSubject() != null && getSubject().getPrincipal() != null;
}
/**
* 驗證目前使用者是否為“訪客”,即未認證(包含未記住)的使用者。用user搭配使用
*
* @return 訪客:true,否則false
*/
public static boolean isGuest() {
return !isUser();
}
/**
* 輸出目前使用者資訊,通常為登入帳号資訊。
*
* @return 目前使用者資訊
*/
public static String principal() {
if (getSubject() != null) {
Object principal = getSubject().getPrincipal();
return principal.toString();
}
return "";
}
}
三、自定義的Realm
import cn.stylefeng.roses.core.util.HttpContext;
import cn.stylefeng.roses.core.util.ToolUtil;
import cn.utry.govaffairs.core.shiro.service.UserAuthService;
import cn.utry.govaffairs.core.shiro.service.impl.UserAuthServiceServiceImpl;
import cn.utry.govaffairs.modular.system.model.User;
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.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ShiroDbRealm extends AuthorizingRealm {
@Autowired
private SessionDAO sessionDAO;
/**
* 登入認證 證明 鑒定
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
// 擷取shirorealm所需資料的Service層
UserAuthService shiroFactory = UserAuthServiceServiceImpl.me();
//擷取Controller層傳遞的token,包含了前端輸入的使用者名和密碼 一個簡單的使用者名/密碼身份驗證令牌
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
//擷取登入的使用者名
String username = token.getUsername();
//根據前端輸入的賬号, 去資料庫查詢使用者資訊. 此時如果賬号不存在(包括邏輯删除)或被當機,直接抛出異常,終止登入
User user = shiroFactory.user(token.getUsername());
//進行使用者的驗證
ShiroUser shiroUser = shiroFactory.shiroUser(user);
// 擷取需要登入的使用者在資料庫中存儲的加鹽的密碼
String credentials = user.getPassword();
// 擷取需要登入的使用者在資料庫中存儲的密碼的鹽值
String source = user.getSalt();
ByteSource credentialsSalt = new Md5Hash(source);
// 建立SimpleAuthenticationInfo 傳回給shiro的安全管理器去比較目前登入使用者輸入的密碼,與資料庫中存儲的加鹽的密碼是否一緻
// 即在登入的Controller層 UsernamePasswordToken 中存儲了目前輸入的使用者名與密碼, 與此SimpleAuthenticationInfo 進行比較
// 如果密碼一緻,代表登入成功, 密碼不一緻,則報密碼錯誤的異常
return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
}
/**
* 權限認證 授權,認可
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
四、密碼驗證器增加登入次數校驗功能
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 驗證器,增加了登入次數校驗功能
*/
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
/**
* 密碼輸入錯誤次數就被當機
*/
private Integer errorPasswordTimes=5;
private Cache<String, AtomicInteger> passwordRetryCache;
/**
* 構造方法 建立對象,傳入緩存的管理器
* @param cacheManager
*/
public RetryLimitCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
/**
* 方法名: doCredentialsMatch
* 方法描述: 使用者登入錯誤次數方法.
* 修改日期: 2019/2/26 20:19
* @param token
* @param info
* @return boolean
* @throws
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token,
AuthenticationInfo info) {
String username = (String) token.getPrincipal();
Set<String> keys = passwordRetryCache.keys();
// retry count + 1
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
if (retryCount.incrementAndGet() > errorPasswordTimes) {
// if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
// clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
}
五、ShiroConfig的配置類
在此配置類中, 要注意的是把ShiroDbRealm的bean中要調用set方法注入retryLimitCredentialsMatcher,否則密碼錯誤次數的校驗不會生效.
@Configuration
public class ShiroConfig {
/**
* Shiro生命周期處理器:
* 用于在實作了Initializable接口的Shiro bean初始化時調用Initializable接口回調(例如:UserRealm)
* 在實作了Destroyable接口的Shiro bean銷毀時調用 Destroyable接口回調(例如:DefaultSecurityManager)
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 方法名: getDefaultAdvisorAutoProxyCreator
* 方法描述: 開啟Shiro的注解模式
* 修改日期: 2019/2/25 16:03
* @param
* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
* @author taohongchao
* @throws
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 安全管理器
*/
@Bean
public DefaultWebSecurityManager securityManager(CookieRememberMeManager rememberMeManager,
CacheManager cacheShiroManager,
SessionManager sessionManager,
RetryLimitCredentialsMatcher retryLimitCredentialsMatcher) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//把自定義的Realm注入安全管理器中
securityManager.setRealm(this.shiroDbRealm(retryLimitCredentialsMatcher));
securityManager.setCacheManager(cacheShiroManager);
securityManager.setRememberMeManager(rememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 緩存管理器 使用Ehcache實作
*/
@Bean
public CacheManager getCacheShiroManager(EhCacheManagerFactoryBean ehcache) {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManager(ehcache.getObject());
ehCacheManager.setCacheManagerConfigFile("ehcache.xml");
return ehCacheManager;
}
@Bean
public RetryLimitCredentialsMatcher getRetryLimit(CacheManager cacheManager){
RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(cacheManager);
retryLimitCredentialsMatcher.setHashAlgorithmName(ShiroKit.HASH_ALGORITHM_NAME);
retryLimitCredentialsMatcher.setHashIterations(ShiroKit.HASHITERATIONS);
retryLimitCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return retryLimitCredentialsMatcher;
}
/**
* 項目自定義的Realm
*/
@Bean
public ShiroDbRealm shiroDbRealm(RetryLimitCredentialsMatcher retryLimitCredentialsMatcher) {
ShiroDbRealm shiroDbRealm = new ShiroDbRealm();
shiroDbRealm.setCredentialsMatcher(retryLimitCredentialsMatcher);
return shiroDbRealm;
}
}
六、EhCache 的配置
EhCache.xml中的配置, 其中設定了名稱為passwordRetryCache的緩存,用于當機密碼輸入錯誤次數多過的緩存.
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false" monitoring="autodetect"
dynamicConfig="true" >
<diskStore path="java.io.tmpdir/ehcache"/>
<defaultCache
maxElementsInMemory="50000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 登入記錄緩存 鎖定10分鐘 -->
<cache name="passwordRetryCache"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"
maxEntriesLocalHeap="0">
</cache>
</ehcache>
EhCacheConfig的配置類
import net.sf.ehcache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* ehcache配置
*
* @author
* @date 2017-05-20 23:11
*/
@Configuration
@EnableCaching
public class EhCacheConfig {
/**
* EhCache的配置
*/
@Bean
public EhCacheCacheManager cacheManager(CacheManager cacheManager) {
return new EhCacheCacheManager(cacheManager);
}
/**
* EhCache的配置
*/
@Bean
public EhCacheManagerFactoryBean ehcache() {
EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
return ehCacheManagerFactoryBean;
}
}
七、全局異常的配置
當密碼輸入錯誤次數過多時,抛出ExcessiveAttemptsException異常,被此異常攔截器攔截
(-1)
public class GlobalExceptionHandler {
/**
* 方法名: excessiveAttemptsException
* 方法描述: 登入錯誤次數過多異常
* @throws
*/
@ExceptionHandler(ExcessiveAttemptsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String excessiveAttemptsException(ExcessiveAttemptsException e, Model model) {
String username = getRequest().getParameter("username");
LogManager.me().executeLog(LogTaskFactory.loginLog(username, "登入錯誤次數超過五次", getIp()));
model.addAttribute("tips", "登入錯誤次數超過五次,請十分鐘後登入!");
return "/login.html";
}
}