一個開源項目,實現了redis做爲緩存 緩存用戶的權限 和 session信息,還有兩個功能沒有修改,一個是用戶併發登陸限制,一個是用戶密碼錯誤次數.本篇中幾個類 也是使用的開源項目中的類,只不過是拿出來了,redis單獨作的配置,方便進行優化。javascript
原文:https://blog.csdn.net/qq_34021712/article/details/80791219 ©王賽超css
有想法的文章:https://blog.csdn.net/qq_20954959/article/details/55260255html
https://blog.csdn.net/why15732625998/article/details/78729254前端
https://www.cnblogs.com/Luke-Me/p/8941110.htmljava
http://www.javashuo.com/article/p-bhuemukb-de.htmlgit
https://blog.csdn.net/qq_16055765/article/details/79298834github
https://www.cnblogs.com/UncleWang001/articles/9779245.htmlweb
http://www.javashuo.com/article/p-rtauisfz-nw.htmlredis
https://blog.csdn.net/xieliaowa9231/article/details/78995465
算法
目錄:
序列化工具SerializeUtils.java繼承RedisSerializer
RedisConfig.java
RedisManager.java
即:
RedisCacheManager.java
RedisSessionDAO.java
KickoutSessionControlFilter.java(限制併發登陸人數)
RetryLimitHashedCredentialsMatcher.java(登陸錯誤次數限制)
ShiroSessionListener.java(session監聽)
上面的類中有一些依賴類,並無貼出來,該些類是爲了解決Shiro整合Redis 頻繁獲取或更新 Session
整合具體流程:
Redis客戶端使用的是RedisTemplate,本身寫了一個序列化工具SerializeUtils.java繼承RedisSerializer
package com.springboot.test.shiro.global.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.io.*; /** * @author: wangsaichao * @date: 2018/6/20 * @description: redis的value序列化工具 */ public class SerializeUtils implements RedisSerializer { private static Logger logger = LoggerFactory.getLogger(SerializeUtils.class); public static boolean isEmpty(byte[] data) { return (data == null || data.length == 0); } /** * 序列化 * @param object * @return * @throws SerializationException */ @Override public byte[] serialize(Object object) throws SerializationException { byte[] result = null; if (object == null) { return new byte[0]; } try ( ByteArrayOutputStream byteStream = new ByteArrayOutputStream(128); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream) ){ if (!(object instanceof Serializable)) { throw new IllegalArgumentException(SerializeUtils.class.getSimpleName() + " requires a Serializable payload " + "but received an object of type [" + object.getClass().getName() + "]"); } objectOutputStream.writeObject(object); objectOutputStream.flush(); result = byteStream.toByteArray(); } catch (Exception ex) { logger.error("Failed to serialize",ex); } return result; } /** * 反序列化 * @param bytes * @return * @throws SerializationException */ @Override public Object deserialize(byte[] bytes) throws SerializationException { Object result = null; if (isEmpty(bytes)) { return null; } try ( ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteStream) ){ result = objectInputStream.readObject(); } catch (Exception e) { logger.error("Failed to deserialize",e); } return result; } }
RedisConfig.java
package com.springboot.test.shiro.config; import com.springboot.test.shiro.global.utils.SerializeUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPoolConfig; /** * @author: wangsaichao * @date: 2017/11/23 * @description: redis配置 */ @Configuration public class RedisConfig { /** * redis地址 */ @Value("${spring.redis.host}") private String host; /** * redis端口號 */ @Value("${spring.redis.port}") private Integer port; /** * redis密碼 */ @Value("${spring.redis.password}") private String password; /** * JedisPoolConfig 鏈接池 * @return */ @Bean public JedisPoolConfig jedisPoolConfig(){ JedisPoolConfig jedisPoolConfig=new JedisPoolConfig(); //最大空閒數 jedisPoolConfig.setMaxIdle(300); //鏈接池的最大數據庫鏈接數 jedisPoolConfig.setMaxTotal(1000); //最大創建鏈接等待時間 jedisPoolConfig.setMaxWaitMillis(1000); //逐出鏈接的最小空閒時間 默認1800000毫秒(30分鐘) jedisPoolConfig.setMinEvictableIdleTimeMillis(300000); //每次逐出檢查時 逐出的最大數目 若是爲負數就是 : 1/abs(n), 默認3 jedisPoolConfig.setNumTestsPerEvictionRun(10); //逐出掃描的時間間隔(毫秒) 若是爲負數,則不運行逐出線程, 默認-1 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000); //是否在從池中取出鏈接前進行檢驗,若是檢驗失敗,則從池中去除鏈接並嘗試取出另外一個 jedisPoolConfig.setTestOnBorrow(true); //在空閒時檢查有效性, 默認false jedisPoolConfig.setTestWhileIdle(true); return jedisPoolConfig; } /** * 配置工廠 * @param jedisPoolConfig * @return */ @Bean public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig){ JedisConnectionFactory jedisConnectionFactory=new JedisConnectionFactory(); //鏈接池 jedisConnectionFactory.setPoolConfig(jedisPoolConfig); //IP地址 jedisConnectionFactory.setHostName(host); //端口號 jedisConnectionFactory.setPort(port); //若是Redis設置有密碼 jedisConnectionFactory.setPassword(password); //客戶端超時時間單位是毫秒 jedisConnectionFactory.setTimeout(5000); return jedisConnectionFactory; } /** * shiro redis緩存使用的模板 * 實例化 RedisTemplate 對象 * @return */ @Bean("shiroRedisTemplate") public RedisTemplate shiroRedisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new SerializeUtils()); redisTemplate.setValueSerializer(new SerializeUtils()); //開啓事務 //stringRedisTemplate.setEnableTransactionSupport(true); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; } }
RedisManager.java
package com.springboot.test.shiro.config.shiro; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.*; import org.springframework.util.CollectionUtils; import java.util.*; import java.util.concurrent.TimeUnit; /** * * @author wangsaichao * 基於spring和redis的redisTemplate工具類 */ public class RedisManager { @Autowired private RedisTemplate<String, Object> redisTemplate; //=============================common============================ /** * 指定緩存失效時間 * @param key 鍵 * @param time 時間(秒) */ public void expire(String key,long time){ redisTemplate.expire(key, time, TimeUnit.SECONDS); } /** * 判斷key是否存在 * @param key 鍵 * @return true 存在 false不存在 */ public Boolean hasKey(String key){ return redisTemplate.hasKey(key); } /** * 刪除緩存 * @param key 能夠傳一個值 或多個 */ @SuppressWarnings("unchecked") public void del(String ... key){ if(key!=null&&key.length>0){ if(key.length==1){ redisTemplate.delete(key[0]); }else{ redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /** * 批量刪除key * @param keys */ public void del(Collection keys){ redisTemplate.delete(keys); } //============================String============================= /** * 普通緩存獲取 * @param key 鍵 * @return 值 */ public Object get(String key){ return redisTemplate.opsForValue().get(key); } /** * 普通緩存放入 * @param key 鍵 * @param value 值 */ public void set(String key,Object value) { redisTemplate.opsForValue().set(key, value); } /** * 普通緩存放入並設置時間 * @param key 鍵 * @param value 值 * @param time 時間(秒) time要大於0 若是time小於等於0 將設置無限期 */ public void set(String key,Object value,long time){ if(time>0){ redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); }else{ set(key, value); } } /** * 使用scan命令 查詢某些前綴的key * @param key * @return */ public Set<String> scan(String key){ Set<String> execute = this.redisTemplate.execute(new RedisCallback<Set<String>>() { @Override public Set<String> doInRedis(RedisConnection connection) throws DataAccessException { Set<String> binaryKeys = new HashSet<>(); Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(key).count(1000).build()); while (cursor.hasNext()) { binaryKeys.add(new String(cursor.next())); } return binaryKeys; } }); return execute; } /** * 使用scan命令 查詢某些前綴的key 有多少個 * 用來獲取當前session數量,也就是在線用戶 * @param key * @return */ public Long scanSize(String key){ long dbSize = this.redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { long count = 0L; Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(key).count(1000).build()); while (cursor.hasNext()) { cursor.next(); count++; } return count; } }); return dbSize; } }
package com.springboot.test.shiro.config.shiro; import com.springboot.test.shiro.global.exceptions.PrincipalIdNullException; import com.springboot.test.shiro.global.exceptions.PrincipalInstanceException; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; /** * @author: wangsaichao * @date: 2018/6/22 * @description: 參考 shiro-redis 開源項目 Git地址 https://github.com/alexxiyang/shiro-redis */ public class RedisCache<K, V> implements Cache<K, V> { private static Logger logger = LoggerFactory.getLogger(RedisCache.class); private RedisManager redisManager; private String keyPrefix = ""; private int expire = 0; private String principalIdFieldName = RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME; /** * Construction * @param redisManager */ public RedisCache(RedisManager redisManager, String prefix, int expire, String principalIdFieldName) { if (redisManager == null) { throw new IllegalArgumentException("redisManager cannot be null."); } this.redisManager = redisManager; if (prefix != null && !"".equals(prefix)) { this.keyPrefix = prefix; } if (expire != -1) { this.expire = expire; } if (principalIdFieldName != null && !"".equals(principalIdFieldName)) { this.principalIdFieldName = principalIdFieldName; } } @Override public V get(K key) throws CacheException { logger.debug("get key [{}]",key); if (key == null) { return null; } try { String redisCacheKey = getRedisCacheKey(key); Object rawValue = redisManager.get(redisCacheKey); if (rawValue == null) { return null; } V value = (V) rawValue; return value; } catch (Exception e) { throw new CacheException(e); } } @Override public V put(K key, V value) throws CacheException { logger.debug("put key [{}]",key); if (key == null) { logger.warn("Saving a null key is meaningless, return value directly without call Redis."); return value; } try { String redisCacheKey = getRedisCacheKey(key); redisManager.set(redisCacheKey, value != null ? value : null, expire); return value; } catch (Exception e) { throw new CacheException(e); } } @Override public V remove(K key) throws CacheException { logger.debug("remove key [{}]",key); if (key == null) { return null; } try { String redisCacheKey = getRedisCacheKey(key); Object rawValue = redisManager.get(redisCacheKey); V previous = (V) rawValue; redisManager.del(redisCacheKey); return previous; } catch (Exception e) { throw new CacheException(e); } } private String getRedisCacheKey(K key) { if (key == null) { return null; } return this.keyPrefix + getStringRedisKey(key); } private String getStringRedisKey(K key) { String redisKey; if (key instanceof PrincipalCollection) { redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key); } else { redisKey = key.toString(); } return redisKey; } private String getRedisKeyFromPrincipalIdField(PrincipalCollection key) { String redisKey; Object principalObject = key.getPrimaryPrincipal(); Method pincipalIdGetter = null; Method[] methods = principalObject.getClass().getDeclaredMethods(); for (Method m:methods) { if (RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME.equals(this.principalIdFieldName) && ("getAuthCacheKey".equals(m.getName()) || "getId".equals(m.getName()))) { pincipalIdGetter = m; break; } if (m.getName().equals("get" + this.principalIdFieldName.substring(0, 1).toUpperCase() + this.principalIdFieldName.substring(1))) { pincipalIdGetter = m; break; } } if (pincipalIdGetter == null) { throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName); } try { Object idObj = pincipalIdGetter.invoke(principalObject); if (idObj == null) { throw new PrincipalIdNullException(principalObject.getClass(), this.principalIdFieldName); } redisKey = idObj.toString(); } catch (IllegalAccessException e) { throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, e); } catch (InvocationTargetException e) { throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, e); } return redisKey; } @Override public void clear() throws CacheException { logger.debug("clear cache"); Set<String> keys = null; try { keys = redisManager.scan(this.keyPrefix + "*"); } catch (Exception e) { logger.error("get keys error", e); } if (keys == null || keys.size() == 0) { return; } for (String key: keys) { redisManager.del(key); } } @Override public int size() { Long longSize = 0L; try { longSize = new Long(redisManager.scanSize(this.keyPrefix + "*")); } catch (Exception e) { logger.error("get keys error", e); } return longSize.intValue(); } @SuppressWarnings("unchecked") @Override public Set<K> keys() { Set<String> keys = null; try { keys = redisManager.scan(this.keyPrefix + "*"); } catch (Exception e) { logger.error("get keys error", e); return Collections.emptySet(); } if (CollectionUtils.isEmpty(keys)) { return Collections.emptySet(); } Set<K> convertedKeys = new HashSet<K>(); for (String key:keys) { try { convertedKeys.add((K) key); } catch (Exception e) { logger.error("deserialize keys error", e); } } return convertedKeys; } @Override public Collection<V> values() { Set<String> keys = null; try { keys = redisManager.scan(this.keyPrefix + "*"); } catch (Exception e) { logger.error("get values error", e); return Collections.emptySet(); } if (CollectionUtils.isEmpty(keys)) { return Collections.emptySet(); } List<V> values = new ArrayList<V>(keys.size()); for (String key : keys) { V value = null; try { value = (V) redisManager.get(key); } catch (Exception e) { logger.error("deserialize values= error", e); } if (value != null) { values.add(value); } } return Collections.unmodifiableList(values); } public String getKeyPrefix() { return keyPrefix; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } public String getPrincipalIdFieldName() { return principalIdFieldName; } public void setPrincipalIdFieldName(String principalIdFieldName) { this.principalIdFieldName = principalIdFieldName; } }
getRedisKeyFromPrincipalIdField()是獲取緩存的用戶身份信息 和用戶權限信息。 裏面有一個屬性principalIdFieldName 在RedisCacheManager也有這個屬性,設置其中一個就能夠.是爲了給緩存用戶身份和權限信息在Redis中的key惟一,登陸用戶名多是username 或者 phoneNum 或者是Email中的一個,如 個人User實體類中 有一個 usernane字段,也是登陸時候使用的用戶名,在redis中緩存的權限信息key 以下, 這個admin 就是 經過getUsername得到的。
package com.springboot.test.shiro.global.exceptions; /** * @author: wangsaichao * @date: 2018/6/21 * @description: */ public class PrincipalInstanceException extends RuntimeException { private static final String MESSAGE = "We need a field to identify this Cache Object in Redis. " + "So you need to defined an id field which you can get unique id to identify this principal. " + "For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. " + "For example, getUserId(), getUserName(), getEmail(), etc.\n" + "Default value is authCacheKey or id, that means your principal object has a method called \"getAuthCacheKey()\" or \"getId()\""; public PrincipalInstanceException(Class clazz, String idMethodName) { super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE); } public PrincipalInstanceException(Class clazz, String idMethodName, Exception e) { super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE, e); } }
PrincipalIdNullException.java
package com.springboot.test.shiro.global.exceptions; /** * @author: wangsaichao * @date: 2018/6/21 * @description: */ public class PrincipalIdNullException extends RuntimeException { private static final String MESSAGE = "Principal Id shouldn't be null!"; public PrincipalIdNullException(Class clazz, String idMethodName) { super(clazz + " id field: " + idMethodName + ", value is null\n" + MESSAGE); } }
RedisCacheManager.java
package com.springboot.test.shiro.config.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * @author: wangsaichao * @date: 2018/6/22 * @description: 參考 shiro-redis 開源項目 Git地址 https://github.com/alexxiyang/shiro-redis */ public class RedisCacheManager implements CacheManager { private final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class); /** * fast lookup by name map */ private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>(); private RedisManager redisManager; /** * expire time in seconds */ private static final int DEFAULT_EXPIRE = 1800; private int expire = DEFAULT_EXPIRE; /** * The Redis key prefix for caches */ public static final String DEFAULT_CACHE_KEY_PREFIX = "shiro:cache:"; private String keyPrefix = DEFAULT_CACHE_KEY_PREFIX; public static final String DEFAULT_PRINCIPAL_ID_FIELD_NAME = "authCacheKey or id"; private String principalIdFieldName = DEFAULT_PRINCIPAL_ID_FIELD_NAME; @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { logger.debug("get cache, name={}",name); Cache cache = caches.get(name); if (cache == null) { cache = new RedisCache<K, V>(redisManager,keyPrefix + name + ":", expire, principalIdFieldName); caches.put(name, cache); } return cache; } public RedisManager getRedisManager() { return redisManager; } public void setRedisManager(RedisManager redisManager) { this.redisManager = redisManager; } public String getKeyPrefix() { return keyPrefix; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } public String getPrincipalIdFieldName() { return principalIdFieldName; } public void setPrincipalIdFieldName(String principalIdFieldName) { this.principalIdFieldName = principalIdFieldName; } }
RedisSessionDAO.java
package com.springboot.test.shiro.config.shiro; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.ValidatingSession; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.*; /** * @author: wangsaichao * @date: 2018/6/22 * @description: 參考 shiro-redis 開源項目 Git地址 https://github.com/alexxiyang/shiro-redis */ public class RedisSessionDAO extends AbstractSessionDAO { private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class); private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:"; private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX; private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L; /** * doReadSession be called about 10 times when login. * Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal. * The default value is 1000 milliseconds (1s). * Most of time, you don't need to change it. */ private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT; /** * expire time in seconds */ private static final int DEFAULT_EXPIRE = -2; private static final int NO_EXPIRE = -1; /** * Please make sure expire is longer than sesion.getTimeout() */ private int expire = DEFAULT_EXPIRE; private static final int MILLISECONDS_IN_A_SECOND = 1000; private RedisManager redisManager; private static ThreadLocal sessionsInThread = new ThreadLocal(); @Override public void update(Session session) throws UnknownSessionException { //若是會話過時/中止 不必再更新了 try { if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) { return; } if (session instanceof ShiroSession) { // 若是沒有主要字段(除lastAccessTime之外其餘字段)發生改變 ShiroSession ss = (ShiroSession) session; if (!ss.isChanged()) { return; } //若是沒有返回 證實有調用 setAttribute往redis 放的時候永遠設置爲false ss.setChanged(false); } this.saveSession(session); } catch (Exception e) { logger.warn("update Session is failed", e); } } /** * save session * @param session * @throws UnknownSessionException */ private void saveSession(Session session) throws UnknownSessionException { if (session == null || session.getId() == null) { logger.error("session or session id is null"); throw new UnknownSessionException("session or session id is null"); } String key = getRedisSessionKey(session.getId()); if (expire == DEFAULT_EXPIRE) { this.redisManager.set(key, session, (int) (session.getTimeout() / MILLISECONDS_IN_A_SECOND)); return; } if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) { logger.warn("Redis session expire time: " + (expire * MILLISECONDS_IN_A_SECOND) + " is less than Session timeout: " + session.getTimeout() + " . It may cause some problems."); } this.redisManager.set(key, session, expire); } @Override public void delete(Session session) { if (session == null || session.getId() == null) { logger.error("session or session id is null"); return; } try { redisManager.del(getRedisSessionKey(session.getId())); } catch (Exception e) { logger.error("delete session error. session id= {}",session.getId()); } } @Override public Collection<Session> getActiveSessions() { Set<Session> sessions = new HashSet<Session>(); try { Set<String> keys = redisManager.scan(this.keyPrefix + "*"); if (keys != null && keys.size() > 0) { for (String key:keys) { Session s = (Session) redisManager.get(key); sessions.add(s); } } } catch (Exception e) { logger.error("get active sessions error."); } return sessions; } public Long getActiveSessionsSize() { Long size = 0L; try { size = redisManager.scanSize(this.keyPrefix + "*"); } catch (Exception e) { logger.error("get active sessions error."); } return size; } @Override protected Serializable doCreate(Session session) { if (session == null) { logger.error("session is null"); throw new UnknownSessionException("session is null"); } Serializable sessionId = this.generateSessionId(session); this.assignSessionId(session, sessionId); this.saveSession(session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { if (sessionId == null) { logger.warn("session id is null"); return null; } Session s = getSessionFromThreadLocal(sessionId); if (s != null) { return s; } logger.debug("read session from redis"); try { s = (Session) redisManager.get(getRedisSessionKey(sessionId)); setSessionToThreadLocal(sessionId, s); } catch (Exception e) { logger.error("read session error. settionId= {}",sessionId); } return s; } private void setSessionToThreadLocal(Serializable sessionId, Session s) { Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); if (sessionMap == null) { sessionMap = new HashMap<Serializable, SessionInMemory>(); sessionsInThread.set(sessionMap); } SessionInMemory sessionInMemory = new SessionInMemory(); sessionInMemory.setCreateTime(new Date()); sessionInMemory.setSession(s); sessionMap.put(sessionId, sessionInMemory); } private Session getSessionFromThreadLocal(Serializable sessionId) { Session s = null; if (sessionsInThread.get() == null) { return null; } Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); SessionInMemory sessionInMemory = sessionMap.get(sessionId); if (sessionInMemory == null) { return null; } Date now = new Date(); long duration = now.getTime() - sessionInMemory.getCreateTime().getTime(); if (duration < sessionInMemoryTimeout) { s = sessionInMemory.getSession(); logger.debug("read session from memory"); } else { sessionMap.remove(sessionId); } return s; } private String getRedisSessionKey(Serializable sessionId) { return this.keyPrefix + sessionId; } public RedisManager getRedisManager() { return redisManager; } public void setRedisManager(RedisManager redisManager) { this.redisManager = redisManager; } public String getKeyPrefix() { return keyPrefix; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } public long getSessionInMemoryTimeout() { return sessionInMemoryTimeout; } public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) { this.sessionInMemoryTimeout = sessionInMemoryTimeout; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } }
package com.springboot.test.shiro.config; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import com.springboot.test.shiro.config.shiro.*; import org.apache.shiro.codec.Base64; import org.apache.shiro.session.SessionListener; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.session.mgt.eis.SessionIdGenerator; 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.mgt.SecurityManager; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.web.servlet.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import javax.servlet.Filter; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Properties; /** * @author: wangsaichao * @date: 2018/5/10 * @description: Shiro配置 */ @Configuration public class ShiroConfig { /** * ShiroFilterFactoryBean 處理攔截資源文件問題。 * 注意:初始化ShiroFilterFactoryBean的時候須要注入:SecurityManager * Web應用中,Shiro可控制的Web請求必須通過Shiro主過濾器的攔截 * @param securityManager * @return */ @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //必須設置 SecurityManager,Shiro的核心安全接口 shiroFilterFactoryBean.setSecurityManager(securityManager); //這裏的/login是後臺的接口名,非頁面,若是不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 shiroFilterFactoryBean.setLoginUrl("/"); //這裏的/index是後臺的接口名,非頁面,登陸成功後要跳轉的連接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未受權界面,該配置無效,並不會進行頁面跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); //自定義攔截器限制併發人數,參考博客: LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>(); //限制同一賬號同時在線的個數 filtersMap.put("kickout", kickoutSessionControlFilter()); //統計登陸人數 shiroFilterFactoryBean.setFilters(filtersMap); // 配置訪問權限 必須是LinkedHashMap,由於它必須保證有序 // 過濾鏈定義,從上向下順序執行,通常將 /**放在最爲下邊 --> : 這是一個坑,一不當心代碼就很差使了 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //配置不登陸能夠訪問的資源,anon 表示資源均可以匿名訪問 //配置記住我或認證經過能夠訪問的地址 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/druid/**", "anon"); //解鎖用戶專用 測試用的 filterChainDefinitionMap.put("/unlockAccount","anon"); filterChainDefinitionMap.put("/Captcha.jpg","anon"); //logout是shiro提供的過濾器 filterChainDefinitionMap.put("/logout", "logout"); //此時訪問/user/delete須要delete權限,在自定義Realm中爲用戶受權。 //filterChainDefinitionMap.put("/user/delete", "perms[\"user:delete\"]"); //其餘資源都須要認證 authc 表示須要認證才能進行訪問 user表示配置記住我或認證經過能夠訪問的地址 //若是開啓限制同一帳號登陸,改成 .put("/**", "kickout,user"); filterChainDefinitionMap.put("/**", "kickout,user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 配置核心安全事務管理器 * @return */ @Bean(name="securityManager") public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //設置自定義realm. securityManager.setRealm(shiroRealm()); //配置記住我 securityManager.setRememberMeManager(rememberMeManager()); //配置redis緩存 securityManager.setCacheManager(cacheManager()); //配置自定義session管理,使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 配置Shiro生命週期處理器 * @return */ @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 身份認證realm; (這個須要本身寫,帳號密碼校驗;權限等) * @return */ @Bean public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCachingEnabled(true); //啓用身份驗證緩存,即緩存AuthenticationInfo信息,默認false shiroRealm.setAuthenticationCachingEnabled(true); //緩存AuthenticationInfo信息的緩存名稱 在ehcache-shiro.xml中有對應緩存的配置 shiroRealm.setAuthenticationCacheName("authenticationCache"); //啓用受權緩存,即緩存AuthorizationInfo信息,默認false shiroRealm.setAuthorizationCachingEnabled(true); //緩存AuthorizationInfo信息的緩存名稱 在ehcache-shiro.xml中有對應緩存的配置 shiroRealm.setAuthorizationCacheName("authorizationCache"); //配置自定義密碼比較器 shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher()); return shiroRealm; } /** * 必須(thymeleaf頁面使用shiro標籤控制按鈕是否顯示) * 未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect * @return */ @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * 開啓shiro 註解模式 * 能夠在controller中的方法前加上註解 * 如 @RequiresPermissions("userInfo:add") * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 解決: 無權限頁面不跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 無效 * shiro的源代碼ShiroFilterFactoryBean.Java定義的filter必須知足filter instanceof AuthorizationFilter, * 只有perms,roles,ssl,rest,port纔是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter, * 因此unauthorizedUrl設置後頁面不跳轉 Shiro註解模式下,登陸失敗與沒有權限都是經過拋出異常。 * 而且默認並無去處理或者捕獲這些異常。在SpringMVC下須要配置捕獲相應異常來通知用戶信息 * @return */ @Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver(); Properties properties=new Properties(); //這裏的 /unauthorized 是頁面,不是訪問的路徑 properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized"); properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/unauthorized"); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; } /** * 解決spring-boot Whitelabel Error Page * @return */ @Bean public EmbeddedServletContainerCustomizer containerCustomizer() { return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer container) { ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthorized.html"); ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html"); ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html"); container.addErrorPages(error401Page, error404Page, error500Page); } }; } /** * cookie對象;會話Cookie模板 ,默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,從新定義爲sid或rememberMe,自定義 * @return */ @Bean public SimpleCookie rememberMeCookie(){ //這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); //setcookie的httponly屬性若是設爲true的話,會增長對xss防禦的安全係數。它有如下特色: //setcookie()的第七個參數 //設爲true後,只能經過http訪問,javascript沒法訪問 //防止xss讀取cookie simpleCookie.setHttpOnly(true); simpleCookie.setPath("/"); //<!-- 記住我cookie生效時間30天 ,單位秒;--> simpleCookie.setMaxAge(2592000); return simpleCookie; } /** * cookie管理對象;記住我功能,rememberMe管理器 * @return */ @Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //rememberMe cookie加密的密鑰 建議每一個項目都不同 默認AES算法 密鑰長度(128 256 512 位) cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } /** * FormAuthenticationFilter 過濾器 過濾記住我 * @return */ @Bean public FormAuthenticationFilter formAuthenticationFilter(){ FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter(); //對應前端的checkbox的name = rememberMe formAuthenticationFilter.setRememberMeParam("rememberMe"); return formAuthenticationFilter; } /** * shiro緩存管理器; * 須要添加到securityManager中 * @return */ @Bean public RedisCacheManager cacheManager(){ RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); //redis中針對不一樣用戶緩存 redisCacheManager.setPrincipalIdFieldName("username"); //用戶權限信息緩存時間 redisCacheManager.setExpire(200000); return redisCacheManager; } /** * 讓某個實例的某個方法的返回值注入爲Bean的實例 * Spring靜態注入 * @return */ @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean(){ MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean(); factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager"); factoryBean.setArguments(new Object[]{securityManager()}); return factoryBean; } /** * 配置session監聽 * @return */ @Bean("sessionListener") public ShiroSessionListener sessionListener(){ ShiroSessionListener sessionListener = new ShiroSessionListener(); return sessionListener; } /** * 配置會話ID生成器 * @return */ @Bean public SessionIdGenerator sessionIdGenerator() { return new JavaUuidSessionIdGenerator(); } @Bean public RedisManager redisManager(){ RedisManager redisManager = new RedisManager(); return redisManager; } @Bean("sessionFactory") public ShiroSessionFactory sessionFactory(){ ShiroSessionFactory sessionFactory = new ShiroSessionFactory(); return sessionFactory; } /** * SessionDAO的做用是爲Session提供CRUD並進行持久化的一個shiro組件 * MemorySessionDAO 直接在內存中進行會話維護 * EnterpriseCacheSessionDAO 提供了緩存功能的會話維護,默認狀況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。 * @return */ @Bean public SessionDAO sessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); //session在redis中的保存時間,最好大於session會話超時時間 redisSessionDAO.setExpire(12000); return redisSessionDAO; } /** * 配置保存sessionId的cookie * 注意:這裏的cookie 不是上面的記住我 cookie 記住我須要一個cookie session管理 也須要本身的cookie * 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,從新定義爲sid * @return */ @Bean("sessionIdCookie") public SimpleCookie sessionIdCookie(){ //這個參數是cookie的名稱 SimpleCookie simpleCookie = new SimpleCookie("sid"); //setcookie的httponly屬性若是設爲true的話,會增長對xss防禦的安全係數。它有如下特色: //setcookie()的第七個參數 //設爲true後,只能經過http訪問,javascript沒法訪問 //防止xss讀取cookie simpleCookie.setHttpOnly(true); simpleCookie.setPath("/"); //maxAge=-1表示瀏覽器關閉時失效此Cookie simpleCookie.setMaxAge(-1); return simpleCookie; } /** * 配置會話管理器,設定會話超時及保存 * @return */ @Bean("sessionManager") public SessionManager sessionManager() { ShiroSessionManager sessionManager = new ShiroSessionManager(); Collection<SessionListener> listeners = new ArrayList<SessionListener>(); //配置監聽 listeners.add(sessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO()); sessionManager.setCacheManager(cacheManager()); sessionManager.setSessionFactory(sessionFactory()); //全局會話超時時間(單位毫秒),默認30分鐘 暫時設置爲10秒鐘 用來測試 sessionManager.setGlobalSessionTimeout(1800000); //是否開啓刪除無效的session對象 默認爲true sessionManager.setDeleteInvalidSessions(true); //是否開啓定時調度器進行檢測過時session 默認爲true sessionManager.setSessionValidationSchedulerEnabled(true); //設置session失效的掃描時間, 清理用戶直接關閉瀏覽器形成的孤立會話 默認爲 1個小時 //設置該屬性 就不須要設置 ExecutorServiceSessionValidationScheduler 底層也是默認自動調用ExecutorServiceSessionValidationScheduler //暫時設置爲 5秒 用來測試 sessionManager.setSessionValidationInterval(3600000); //取消url 後面的 JSESSIONID sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } /** * 併發登陸控制 * @return */ @Bean public KickoutSessionControlFilter kickoutSessionControlFilter(){ KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); //用於根據會話ID,獲取會話進行踢出操做的; kickoutSessionControlFilter.setSessionManager(sessionManager()); //使用cacheManager獲取相應的cache來緩存用戶登陸的會話;用於保存用戶—會話之間的關係的; kickoutSessionControlFilter.setRedisManager(redisManager()); //是否踢出後來登陸的,默認是false;即後者登陸的用戶踢出前者登陸的用戶; kickoutSessionControlFilter.setKickoutAfter(false); //同一個用戶最大的會話數,默認1;好比2的意思是同一個用戶容許最多同時兩我的登陸; kickoutSessionControlFilter.setMaxSession(1); //被踢出後重定向到的地址; kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1"); return kickoutSessionControlFilter; } /** * 配置密碼比較器 * @return */ @Bean("credentialsMatcher") public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){ RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(); retryLimitHashedCredentialsMatcher.setRedisManager(redisManager()); //若是密碼加密,能夠打開下面配置 //加密算法的名稱 //retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5"); //配置加密的次數 //retryLimitHashedCredentialsMatcher.setHashIterations(1024); //是否存儲爲16進制 //retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return retryLimitHashedCredentialsMatcher; } }
ShiroRealm.java
package com.springboot.test.shiro.config.shiro; import com.springboot.test.shiro.modules.user.dao.PermissionMapper; import com.springboot.test.shiro.modules.user.dao.RoleMapper; import com.springboot.test.shiro.modules.user.dao.entity.Permission; import com.springboot.test.shiro.modules.user.dao.entity.Role; import com.springboot.test.shiro.modules.user.dao.UserMapper; import com.springboot.test.shiro.modules.user.dao.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; 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.springframework.beans.factory.annotation.Autowired; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * @author: wangsaichao * @date: 2018/5/10 * @description: 在Shiro中,最終是經過Realm來獲取應用程序中的用戶、角色及權限信息的 * 在Realm中會直接從咱們的數據源中獲取Shiro須要的驗證信息。能夠說,Realm是專用於安全框架的DAO. */ public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; /** * 驗證用戶身份 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //獲取用戶名密碼 第一種方式 //String username = (String) authenticationToken.getPrincipal(); //String password = new String((char[]) authenticationToken.getCredentials()); //獲取用戶名 密碼 第二種方式 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; String username = usernamePasswordToken.getUsername(); String password = new String(usernamePasswordToken.getPassword()); //從數據庫查詢用戶信息 User user = this.userMapper.findByUserName(username); //能夠在這裏直接對用戶名校驗,或者調用 CredentialsMatcher 校驗 if (user == null) { throw new UnknownAccountException("用戶名或密碼錯誤!"); } //這裏將 密碼對比 註銷掉,不然 沒法鎖定 要將密碼對比 交給 密碼比較器 //if (!password.equals(user.getPassword())) { // throw new IncorrectCredentialsException("用戶名或密碼錯誤!"); //} if ("1".equals(user.getState())) { throw new LockedAccountException("帳號已被鎖定,請聯繫管理員!"); } //調用 CredentialsMatcher 校驗 還須要建立一個類 繼承CredentialsMatcher 若是在上面校驗了,這個就不須要了 //配置自定義權限登陸器 參考博客: SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } /** * 受權用戶權限 * 受權的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>標籤的時候調用的 * 它會去檢測shiro框架中的權限(這裏的permissions)是否包含有該標籤的name值,若是有,裏面的內容顯示 * 若是沒有,裏面的內容不予顯示(這就完成了對於權限的認證.) * * shiro的權限受權是經過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo(); * 當訪問到頁面的時候,連接配置了相應的權限或者shiro標籤纔會執行此方法不然不會執行 * 因此若是隻是簡單的身份認證沒有權限的控制的話,那麼這個方法能夠不進行實現,直接返回null便可。 * * 在這個方法中主要是使用類:SimpleAuthorizationInfo 進行角色的添加和權限的添加。 * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission()); * * 固然也能夠添加set集合:roles是從數據庫查詢的當前用戶的角色,stringPermissions是從數據庫查詢的當前用戶對應的權限 * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions); * * 就是說若是在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[權限添加]"); * 就說明訪問/add這個連接必需要有「權限添加」這個權限才能夠訪問 * * 若是在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[權限添加]"); * 就說明訪問/add這個連接必需要有 "權限添加" 這個權限和具備 "100002" 這個角色才能夠訪問 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("查詢權限方法調用了!!!"); //獲取用戶 User user = (User) SecurityUtils.getSubject().getPrincipal(); //獲取用戶角色 Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid()); //添加角色 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); for (Role role : roles) { authorizationInfo.addRole(role.getRole()); } //獲取用戶權限 Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles); //添加權限 for (Permission permission:permissions) { authorizationInfo.addStringPermission(permission.getPermission()); } return authorizationInfo; } /** * 重寫方法,清除當前用戶的的 受權緩存 * @param principals */ @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 重寫方法,清除當前用戶的 認證緩存 * @param principals */ @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } /** * 自定義方法:清除全部 受權緩存 */ public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } /** * 自定義方法:清除全部 認證緩存 */ public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } /** * 自定義方法:清除全部的 認證緩存 和 受權緩存 */ public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
KickoutSessionControlFilter.java(限制併發登陸人數)
package com.springboot.test.shiro.config.shiro; import java.io.Serializable; import java.util.Deque; import java.util.LinkedList; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import com.springboot.test.shiro.modules.user.dao.entity.User; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.resource.ResourceUrlProvider; /** * @author: WangSaiChao * @date: 2018/5/23 * @description: shiro 自定義filter 實現 併發登陸控制 */ public class KickoutSessionControlFilter extends AccessControlFilter{ @Autowired private ResourceUrlProvider resourceUrlProvider; /** 踢出後到的地址 */ private String kickoutUrl; /** 踢出以前登陸的/以後登陸的用戶 默認踢出以前登陸的用戶 */ private boolean kickoutAfter = false; /** 同一個賬號最大會話數 默認1 */ private int maxSession = 1; private SessionManager sessionManager; private RedisManager redisManager; public static final String DEFAULT_KICKOUT_CACHE_KEY_PREFIX = "shiro:cache:kickout:"; private String keyPrefix = DEFAULT_KICKOUT_CACHE_KEY_PREFIX; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } public void setRedisManager(RedisManager redisManager) { this.redisManager = redisManager; } public String getKeyPrefix() { return keyPrefix; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } private String getRedisKickoutKey(String username) { return this.keyPrefix + username; } /** * 是否容許訪問,返回true表示容許 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } /** * 表示訪問拒絕時是否本身處理,若是返回true表示本身不處理且繼續攔截器鏈執行,返回false表示本身已經處理了(好比重定向到另外一個頁面)。 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if(!subject.isAuthenticated() && !subject.isRemembered()) { //若是沒有登陸,直接進行以後的流程 return true; } //若是有登陸,判斷是否訪問的爲靜態資源,若是是遊客容許訪問的靜態資源,直接返回true HttpServletRequest httpServletRequest = (HttpServletRequest)request; String path = httpServletRequest.getServletPath(); // 若是是靜態文件,則返回true if (isStaticFile(path)){ return true; } Session session = subject.getSession(); //這裏獲取的User是實體 由於我在 自定義ShiroRealm中的doGetAuthenticationInfo方法中 //new SimpleAuthenticationInfo(user, password, getName()); 傳的是 User實體 因此這裏拿到的也是實體,若是傳的是userName 這裏拿到的就是userName String username = ((User) subject.getPrincipal()).getUsername(); Serializable sessionId = session.getId(); // 初始化用戶的隊列放到緩存裏 Deque<Serializable> deque = (Deque<Serializable>) redisManager.get(getRedisKickoutKey(username)); if(deque == null || deque.size()==0) { deque = new LinkedList<Serializable>(); } //若是隊列裏沒有此sessionId,且用戶沒有被踢出;放入隊列 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { deque.push(sessionId); } //若是隊列裏的sessionId數超出最大會話數,開始踢人 while(deque.size() > maxSession) { Serializable kickoutSessionId = null; if(kickoutAfter) { //若是踢出後者 kickoutSessionId=deque.getFirst(); kickoutSessionId = deque.removeFirst(); } else { //不然踢出前者 kickoutSessionId = deque.removeLast(); } try { Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); if(kickoutSession != null) { //設置會話的kickout屬性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) {//ignore exception e.printStackTrace(); } } redisManager.set(getRedisKickoutKey(username), deque); //若是被踢出了,直接退出,重定向到踢出後的地址 if (session.getAttribute("kickout") != null) { //會話被踢出了 try { subject.logout(); } catch (Exception e) { } WebUtils.issueRedirect(request, response, kickoutUrl); return false; } return true; } private boolean isStaticFile(String path) { String staticUri = resourceUrlProvider.getForLookupPath(path); return staticUri != null; } }
RetryLimitHashedCredentialsMatcher.java(登陸錯誤次數限制)
package com.springboot.test.shiro.config.shiro; import java.util.concurrent.atomic.AtomicInteger; import com.springboot.test.shiro.modules.user.dao.UserMapper; import com.springboot.test.shiro.modules.user.dao.entity.User; import org.apache.log4j.Logger; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.springframework.beans.factory.annotation.Autowired; /** * @author: WangSaiChao * @date: 2018/5/25 * @description: 登錄次數限制 */ public class RetryLimitHashedCredentialsMatcher extends SimpleCredentialsMatcher { private static final Logger logger = Logger.getLogger(RetryLimitHashedCredentialsMatcher.class); public static final String DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX = "shiro:cache:retrylimit:"; private String keyPrefix = DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX; @Autowired private UserMapper userMapper; private RedisManager redisManager; public void setRedisManager(RedisManager redisManager) { this.redisManager = redisManager; } private String getRedisKickoutKey(String username) { return this.keyPrefix + username; } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //獲取用戶名 String username = (String)token.getPrincipal(); //獲取用戶登陸次數 AtomicInteger retryCount = (AtomicInteger)redisManager.get(getRedisKickoutKey(username)); if (retryCount == null) { //若是用戶沒有登錄過,登錄次數加1 並放入緩存 retryCount = new AtomicInteger(0); } if (retryCount.incrementAndGet() > 5) { //若是用戶登錄失敗次數大於5次 拋出鎖定用戶異常 並修改數據庫字段 User user = userMapper.findByUserName(username); if (user != null && "0".equals(user.getState())){ //數據庫字段 默認爲 0 就是正常狀態 因此 要改成1 //修改數據庫的狀態字段爲鎖定 user.setState("1"); userMapper.update(user); } logger.info("鎖定用戶" + user.getUsername()); //拋出用戶鎖定異常 throw new LockedAccountException(); } //判斷用戶帳號和密碼是否正確 boolean matches = super.doCredentialsMatch(token, info); if (matches) { //若是正確,從緩存中將用戶登陸計數 清除 redisManager.del(getRedisKickoutKey(username)); }{ redisManager.set(getRedisKickoutKey(username), retryCount); } return matches; } /** * 根據用戶名 解鎖用戶 * @param username * @return */ public void unlockAccount(String username){ User user = userMapper.findByUserName(username); if (user != null){ //修改數據庫的狀態字段爲鎖定 user.setState("0"); userMapper.update(user); redisManager.del(getRedisKickoutKey(username)); } } }
ShiroSessionListener.java(session監聽)
package com.springboot.test.shiro.config.shiro; import com.springboot.test.shiro.Application; import com.springboot.test.shiro.modules.user.dao.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionListener; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * @author: wangsaichao * @date: 2018/5/15 * @description: 配置session監聽器, */ public class ShiroSessionListener implements SessionListener{ /** * 統計在線人數 * juc包下線程安全自增 */ private final AtomicInteger sessionCount = new AtomicInteger(0); /** * 會話建立時觸發 * @param session */ @Override public void onStart(Session session) { //會話建立,在線人數加一 sessionCount.incrementAndGet(); } /** * 退出會話時觸發 * @param session */ @Override public void onStop(Session session) { //會話退出,在線人數減一 sessionCount.decrementAndGet(); } /** * 會話過時時觸發 * @param session */ @Override public void onExpiration(Session session) { //會話過時,在線人數減一 sessionCount.decrementAndGet(); } /** * 獲取在線人數使用 * @return */ public AtomicInteger getSessionCount() { return sessionCount; } }