上一篇文章,我们讲述了如何使用 redis 缓存用户的数据和 session 的数据,它使用 shiro 自带的 RedisManager 类即可实现。但是,现在我们想要实现用户并发登录限制和用户登录错误次数的缓存,这个就需要特殊处理了。我们的 redis 需要单独配置;而且还借鉴了开源项目中的一些类。
首先在我们的项目中单独引入 redis ,方便以后的优化,redis 客户端使用的是 RedisTemplate,我们先自己写了一个序列化工具类继承 RedisSerializer ,如下所示:
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.*;
/**
*
* 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;
}
}
编写 redis 的配置类 RedisConfig ,用于初始化 redis 的相关配置,千万记得要在配置文件里面配置 redis 的 ip 地址和用户名密码。
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;
@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
*/
@SuppressWarnings("deprecation")
@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;
}
}
编写 redis 的管理类 RedisManager ,封装一些常用的方法,方便对缓存中 key 和 value 进行操作。
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;
/**
* 基于spring和redis的redisTemplate工具类
*/
public class RedisManager {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
* @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
*/
@SuppressWarnings("unchecked")
public void del(@SuppressWarnings("rawtypes") 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 scan(String key){
Set execute = this.redisTemplate.execute(new RedisCallback>() {
@Override
public Set doInRedis(RedisConnection connection) throws DataAccessException {
Set binaryKeys = new HashSet<>();
Cursor 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() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
long count = 0L;
Cursor cursor = connection.scan(ScanOptions.scanOptions().match(key).count(1000).build());
while (cursor.hasNext()) {
cursor.next();
count++;
}
return count;
}
});
return dbSize;
}
}
如果想要使用 redis 作为缓存,需要重写 shiro 里面的 cache 、cacheManager 和 SessionDAO,下面分别将这三个类的代码贴出。首先贴出的是 RedisCache 类的内容,
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.crazycake.shiro.exception.PrincipalIdNullException;
import org.crazycake.shiro.exception.PrincipalInstanceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.redis.RedisManager;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* 参考 shiro-redis 开源项目 Git地址 https://github.com/alexxiyang/shiro-redis
*/
public class RedisCache implements Cache {
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 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 keys() {
Set 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 convertedKeys = new HashSet();
for (String key:keys) {
try {
convertedKeys.add((K) key);
} catch (Exception e) {
logger.error("deserialize keys error", e);
}
}
return convertedKeys;
}
@Override
public Collection values() {
Set 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 values = new ArrayList(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 实体类中有一个 userName 字段,也是登录时候使用的用户名,在 redis 中缓存的权限信息 key 如下, 这个 zhangsan 就是 通过 getUserName() 获得的。如下图所示:
接下来贴出的是 RedisCacheManager 类的代码,如下所示:
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 com.redis.RedisManager;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 参考 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 caches = new ConcurrentHashMap();
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 Cache getCache(String name) throws CacheException {
logger.debug("get cache, name={}",name);
Cache cache = caches.get(name);
if (cache == null) {
cache = new RedisCache(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 类的代码,如下所示:
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.crazycake.shiro.SessionInMemory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.redis.RedisManager;
import java.io.Serializable;
import java.util.*;
/**
* 参考 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 getActiveSessions() {
Set sessions = new HashSet();
try {
Set 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 sessionMap = (Map) sessionsInThread.get();
if (sessionMap == null) {
sessionMap = new HashMap();
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 sessionMap = (Map) 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;
}
}
接下来贴出的是 ShiroSession 类的代码,如下所示:
import org.apache.shiro.session.mgt.SimpleSession;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
/**
* 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,
* 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回
*/
public class ShiroSession extends SimpleSession implements Serializable {
// 除lastAccessTime以外其他字段发生改变时为true
private boolean isChanged = false;
public ShiroSession() {
super();
this.setChanged(true);
}
public ShiroSession(String host) {
super(host);
this.setChanged(true);
}
@Override
public void setId(Serializable id) {
super.setId(id);
this.setChanged(true);
}
@Override
public void setStopTimestamp(Date stopTimestamp) {
super.setStopTimestamp(stopTimestamp);
this.setChanged(true);
}
@Override
public void setExpired(boolean expired) {
super.setExpired(expired);
this.setChanged(true);
}
@Override
public void setTimeout(long timeout) {
super.setTimeout(timeout);
this.setChanged(true);
}
@Override
public void setHost(String host) {
super.setHost(host);
this.setChanged(true);
}
@Override
public void setAttributes(Map
读取用户信息的时候,还会用到两个异常类 PrincipalInstanceException 和 PrincipalIdNullException ,代码如下所示:
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);
}
}
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);
}
}
整个的 shiroConfig 的代码如下所示:
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
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.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
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 com.cache.RedisCacheManager;
import com.cache.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.filter.ClearSessionCacheFilter;
import com.filter.KickoutSessionControlFilter;
import com.filter.RetryLimitHashedCredentialsMatcher;
import com.redis.RedisManager;
import com.session.ShiroSessionListener;
import com.shiro.CustomRealm;
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
// 将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
// 告诉realm,使用credentialsMatcher加密算法类来验证密文
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
/* 开启支持缓存,需要配置如下几个参数 */
customRealm.setCachingEnabled(true);
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
customRealm.setAuthenticationCachingEnabled(true);
// 缓存AuthenticationInfo信息的缓存名称 在 ehcache-shiro.xml 中有对应缓存的配置
customRealm.setAuthenticationCacheName("authenticationCache");
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false
customRealm.setAuthorizationCachingEnabled(true);
// 缓存AuthorizationInfo 信息的缓存名称 在 ehcache-shiro.xml 中有对应缓存的配置
customRealm.setAuthorizationCacheName("authorizationCache");
return customRealm;
}
// 权限管理,配置主要是Realm的管理认证
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 将 CookieRememberMeManager 注入到 SecurityManager 中,否则不会生效
securityManager.setRememberMeManager(rememberMeManager());
// 将 sessionManager 注入到 SecurityManager 中,否则不会生效
securityManager.setSessionManager(sessionManager());
// 将 RedisCacheManager 注入到 SecurityManager 中,否则不会生效
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
// Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
shiroFilter.setSecurityManager(securityManager);
// 不输入地址的话会自动寻找项目web项目的根目录下的/page/login.jsp页面。
shiroFilter.setLoginUrl("/login");
//登录成功默认跳转页面,不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。
shiroFilter.setSuccessUrl("/shiro_index");
// 自定义拦截器
LinkedHashMap filtersMap = new LinkedHashMap<>();
// 限制同一帐号同时在线的个数
filtersMap.put("kickout", kickoutSessionControlFilter());
// 清除过期缓存的拦截器
filtersMap.put("clearSession", clearSessionCacheFilter());
shiroFilter.setFilters(filtersMap);
//没有权限默认跳转的页面
//shiroFilter.setUnauthorizedUrl("");
//filterChainDefinitions的配置顺序为自上而下,以最上面的为准
//shiroFilter.setFilterChainDefinitions("");
// Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时),配置不会被拦截的链接 顺序判断
Map map = new LinkedHashMap<>();
// 不能对login方法进行拦截,若进行拦截的话,这辈子都登录不上去了,这个login是LoginController里面登录校验的方法
map.put("/login", "anon"); //
map.put("/unlockAccount", "anon");
map.put("/verificationCode","anon");
map.put("/static/**", "anon");
//map.put("/", "anon");
//对所有用户认证
map.put("/**", "kickout,clearSession,authc");//user,
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher();
retryLimitHashedCredentialsMatcher.setRedisManager(redisManager());
// 散列算法:这里使用MD5算法;
retryLimitHashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
retryLimitHashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return retryLimitHashedCredentialsMatcher;
}
@Bean
public SimpleCookie rememberMeCookie(){
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("myCookie");
//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
// setcookie()的第七个参数
// 设为true后,只能通过http访问,javascript无法访问
// 防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//
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("myCookie");
return formAuthenticationFilter;
}
/**
* 让某个实例的某个方法的返回值注入为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();
}
/**
* 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
* @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() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 为了解决输入网址地址栏出现 jsessionid 的问题
sessionManager.setSessionIdUrlRewritingEnabled(false);
Collection listeners = new ArrayList();
// 配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setCacheManager(redisCacheManager());
// 全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
// sessionManager.setGlobalSessionTimeout(10000);
sessionManager.setGlobalSessionTimeout(1800000);
// 是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
// 是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
// 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
// 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
// 暂时设置为 5秒 用来测试
sessionManager.setSessionValidationInterval(3600000);
// sessionManager.setSessionValidationInterval(5000);
return sessionManager;
}
/**
* 校验当前缓存是否失效的拦截器
*
* */
@Bean
public ClearSessionCacheFilter clearSessionCacheFilter() {
ClearSessionCacheFilter clearSessionCacheFilter = new ClearSessionCacheFilter();
return clearSessionCacheFilter;
}
/**
* 并发登录控制
* @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");
return kickoutSessionControlFilter;
}
/**
* shiro缓存管理器;
* 需要添加到securityManager中
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// redis中针对不同用户缓存
redisCacheManager.setPrincipalIdFieldName("userName");
// 用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
//redisManager.setHost("127.0.0.1");
//redisManager.setPort(6379);
//redisManager.setPassword("123456");
return redisManager;
}
}
ShiroRealm 的代码内容如下所示:
import java.util.ArrayList;
import java.util.List;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
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.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.entity.Permission;
import com.entity.Role;
import com.entity.User;
import com.service.UserService;
import com.util.MyByteSource;
public class CustomRealm extends AuthorizingRealm{
@Autowired
UserService userService;
/*
* 权限配置类
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("开始查询数据库");
// 获取登录用户名
//User sysuser = (User)principalCollection.getPrimaryPrincipal();
User sysuser;
Object object = principalCollection.getPrimaryPrincipal();
if (object instanceof User) {
sysuser = (User) object;
} else {
sysuser = JSON.parseObject(JSON.toJSON(object).toString(), User.class);
}
// 查询用户名称
User user = userService.selectByUserName(sysuser.getUserName());
// 添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List roleNameList = new ArrayList<>();
List permissionNameList = new ArrayList<>();
for (Role role : user.getRoles()) {
roleNameList.add(role.getRoleName());
for (Permission permission : role.getPermissions()) {
permissionNameList.add(role.getRoleName()+":"+permission.getPermissionName());
}
}
// 添加角色
simpleAuthorizationInfo.addRoles(roleNameList);
// 添加权限
simpleAuthorizationInfo.addStringPermissions(permissionNameList);
System.out.println("查询数据库结束");
return simpleAuthorizationInfo;
}
/*
* 认证配置类
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
if(StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
// 获取用户信息
String userName = authenticationToken.getPrincipal().toString();
User user = userService.selectByUserName(userName);
// 用户是否存在
if(user == null) {
throw new UnknownAccountException();
}
// 是否激活
/*if(user !=null && user.getStatus().equals("0")){
throw new DisabledAccountException();
}*/
// 是否锁定
if(user!=null && user.getStatus().equals("1")){
throw new LockedAccountException();
}
// 若存在将此用户存放到登录认证info中,无需做密码比对shiro会为我们进行密码比对校验
if(user !=null && user.getStatus().equals("0")){
// ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUserName()+ "salt");
ByteSource credentialsSalt = new MyByteSource(user.getUserName()+ "salt");
/** 这里验证authenticationToken和simpleAuthenticationInfo的信息,构造方法支持三个或者四个参数,
* 第一个参数传入userName或者是user对象都可以。
* 第二个参数传入数据库中该用户的密码(记得是加密后的密码)
* 第三个参数传入加密的盐值,若没有则可以不加
* 第四个参数传入当前Relam的名字
**/
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword().toString(),credentialsSalt, getName());
return simpleAuthenticationInfo;
}
return null;
}
/**
* 重写方法,清除当前用户的的 授权缓存
* @param principals
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principal) {
super.clearCachedAuthorizationInfo(principal);
}
/**
* 重写方法,清除当前用户的 认证缓存
* @param principals
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principal) {
super.clearCachedAuthenticationInfo(principal);
}
/**
* 重写方法,清除当前用户的 认证缓存和授权缓存
* */
@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 代码如下所示:
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 javax.servlet.http.HttpServletResponse;
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 com.redis.RedisManager;
public class KickoutSessionControlFilter extends AccessControlFilter{
// 踢出后到的地址
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 String getKickoutUrl() {
return kickoutUrl;
}
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public boolean isKickoutAfter() {
return kickoutAfter;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public int getMaxSession() {
return maxSession;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public SessionManager getSessionManager() {
return sessionManager;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
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;
}
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;
}
Session session = subject.getSession();
// 传的是userName 这里拿到的就是 userName
// new SimpleAuthenticationInfo(userName, password, getName());
User sysuser;
Object object = subject.getPrincipal();
if (object instanceof User) {
sysuser = (User) object;
} else {
sysuser = JSON.parseObject(JSON.toJSON(object).toString(), User.class);
}
String username = sysuser.getUserName();
Serializable sessionId = session.getId();
// 初始化用户的队列放到缓存里
Deque deque = (Deque) redisManager.get(getRedisKickoutKey(username));
if(deque == null) {
deque = new LinkedList();
}
// 如果队列里没有此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);
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (isAjax(request)) {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.setHeader("session-status", "two-user");
} else {
httpServletResponse.sendRedirect(kickoutUrl);
}
return false;
}
return true;
}
private boolean isAjax(ServletRequest request){
String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
if("XMLHttpRequest".equalsIgnoreCase(header)){
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
用于控制用户登录错误次数超限锁定的类 RetryLimitHashedCredentialsMatcher 代码如下所示:
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import com.dao.UserMapper;
import com.entity.User;
import com.redis.RedisManager;
/**
*
* 登陆次数限制
*/
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher{
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() > 3) {
// 如果用户登陆失败次数大于3次 抛出锁定用户异常 并修改数据库字段
User user = userMapper.selectByUserName(username);
if (user != null && "0".equals(user.getStatus())){
// 数据库字段 默认为 0 就是正常状态 所以 要改为1
// 修改数据库的状态字段为锁定
user.setStatus("1");
userMapper.updateByPrimaryKey(user);
}
logger.info("锁定用户" + user.getUserName());
// 抛出用户锁定异常
throw new LockedAccountException();
}
// 判断用户账号和密码是否正确
boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
// 如果正确,从缓存中将用户登录计数 清除
redisManager.del(getRedisKickoutKey(username));
}else {
redisManager.set(getRedisKickoutKey(username), retryCount);
}
return matches;
}
/**
* 根据用户名 解锁用户
* @param username
* @return
*/
public void unlockAccount(String username){
User user = userMapper.selectByUserName(username);
if (user != null){
// 修改数据库的状态字段为锁定
user.setStatus("0");
userMapper.updateByPrimaryKey(user);
redisManager.del(getRedisKickoutKey(username));
}
}
}
启动工程,输入网址,用两个用户分别使用不同的浏览器进行登录,一个登录成功,一个故意输错密码,查看 redis 中的缓存如下: