springboot + shiro 整合 redis 缓存用户并发登录限制和用户登录错误次数

背景:

       上一篇文章,我们讲述了如何使用 redis 缓存用户的数据和 session 的数据,它使用 shiro 自带的 RedisManager 类即可实现。但是,现在我们想要实现用户并发登录限制用户登录错误次数的缓存,这个就需要特殊处理了。我们的 redis 需要单独配置;而且还借鉴了开源项目中的一些类。

整合 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 的相关配置,千万记得要在配置文件里面配置 redisip 地址和用户名密码。

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 ,封装一些常用的方法,方便对缓存中 keyvalue 进行操作。

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 缓存:

       如果想要使用 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() 方法,它是获取缓存的用户身份信息和用户权限信息。 它里面有一个属性 principalIdFieldNameRedisCacheManager 中也有这个属性。设置其中一个就可以。设置它是为了让缓存用户身份和权限信息在 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 attributes) {
        super.setAttributes(attributes);
        this.setChanged(true);
    }

    @Override
    public void setAttribute(Object key, Object value) {
        super.setAttribute(key, value);
        this.setChanged(true);
    }

    @Override
    public Object removeAttribute(Object key) {
        this.setChanged(true);
        return super.removeAttribute(key);
    }

    /**
     * 停止
     */
    @Override
    public void stop() {
        super.stop();
        this.setChanged(true);
    }

    /**
     * 设置过期
     */
    @Override
    protected void expire() {
        this.stop();
        this.setExpired(true);
    }

    public boolean isChanged() {
        return isChanged;
    }

    public void setChanged(boolean isChanged) {
        this.isChanged = isChanged;
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Override
    protected boolean onEquals(SimpleSession ss) {
        return super.onEquals(ss);
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public String toString() {
        return super.toString();
    }
}

       读取用户信息的时候,还会用到两个异常类 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:

       整个的 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:

       用于控制用户并发登录的类 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:

       用于控制用户登录错误次数超限锁定的类 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 中的缓存如下:

springboot + shiro 整合 redis 缓存用户并发登录限制和用户登录错误次数_第1张图片

你可能感兴趣的:(shiro,redis)