javascript
SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号
文章目錄
- 二、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 {@Autowiredprivate SessionDAO sessionDAO;/*** 登錄認證 證明 鑒定*/@Overrideprotected 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);}/*** 權限認證 授權,認可*/@Overrideprotected 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*/@Overridepublic boolean doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info) {String username = (String) token.getPrincipal();Set<String> keys = passwordRetryCache.keys();// retry count + 1AtomicInteger retryCount = passwordRetryCache.get(username);if (retryCount == null) {retryCount = new AtomicInteger(0);passwordRetryCache.put(username, retryCount);}if (retryCount.incrementAndGet() > errorPasswordTimes) {// if retry count > 5 throwthrow new ExcessiveAttemptsException();}boolean matches = super.doCredentialsMatch(token, info);if (matches) {// clear retry countpasswordRetryCache.remove(username);}return matches;} }五、ShiroConfig的配置類
在此配置類中, 要注意的是把ShiroDbRealm的bean中要調用set方法注入retryLimitCredentialsMatcher,否則密碼錯誤次數的校驗不會生效.
@Configuration public class ShiroConfig {/*** Shiro生命周期處理器:* 用于在實現了Initializable接口的Shiro bean初始化時調用Initializable接口回調(例如:UserRealm)* 在實現了Destroyable接口的Shiro bean銷毀時調用 Destroyable接口回調(例如:DefaultSecurityManager)*/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*** 方法名: getDefaultAdvisorAutoProxyCreator* 方法描述: 開啟Shiro的注解模式* 修改日期: 2019/2/25 16:03* @param* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator* @author taohongchao* @throws*/@Beanpublic DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();autoProxyCreator.setProxyTargetClass(true);return autoProxyCreator;}/*** 安全管理器*/@Beanpublic 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實現*/@Beanpublic CacheManager getCacheShiroManager(EhCacheManagerFactoryBean ehcache) {EhCacheManager ehCacheManager = new EhCacheManager();ehCacheManager.setCacheManager(ehcache.getObject());ehCacheManager.setCacheManagerConfigFile("ehcache.xml");return ehCacheManager;}@Beanpublic 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*/@Beanpublic 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"/><defaultCachemaxElementsInMemory="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的配置*/@Beanpublic EhCacheCacheManager cacheManager(CacheManager cacheManager) {return new EhCacheCacheManager(cacheManager);}/*** EhCache的配置*/@Beanpublic EhCacheManagerFactoryBean ehcache() {EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));return ehCacheManagerFactoryBean;} }七、全局異常的配置
當密碼輸入錯誤次數過多時,拋出ExcessiveAttemptsException異常,被此異常攔截器攔截
@ControllerAdvice @Order(-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";} }最終的效果如圖所示
總結
以上是生活随笔為你收集整理的SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flowable 数据库表结构 ACT
- 下一篇: RabbitMQ开机启动 Centos7