Shiro源码分析① :简单项目搭建

一、前言

由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。(个人来说并不喜欢使用Shiro )


Shiro 源码分析全集:

  1. Shiro源码分析① :简单项目搭建
  2. Shiro源码分析② :AbstractShiroFilter
  3. Shiro源码分析③ :认证流程
  4. Shiro源码分析④ :鉴权流程

本文会搭建一个最简单的Springboot + Shiro项目,目的在于为后续的Shiro 源码分析做准备,故不会详细介绍搭建过程。下面先介绍Shiro 中两个比较关键的类的实现。


1. SessionDao

在这里插入图片描述

  • SessionDAO :定义了从持久层操作session的标准;
  • AbstractSessionDAO :提供了SessionDAO的基础实现,如生成会话ID等;
  • CachingSessionDAO :提供了对开发者透明的session缓存的功能,只需要设置相应的 CacheManager 即可;
  • MemorySessionDAO :直接在内存中进行session维护;
  • EnterpriseCacheSessionDAO :提供了缓存功能的session维护,默认情况下使用 MapCache 实现,内部使用ConcurrentHashMap保存缓存的会话。

2. SessionManager

  • DefaultSessionManager :JavaSE环境
  • ServletContainerSessionManager: Web环境,直接使用servlet容器会话。DefaultWebSecurityManager中默认使用的是ServletContainerSessionManager
  • DefaultWebSessionManager:用于Web环境的实现,可以替第二个,自己维护着会话,直接废弃了Servlet容器的会话管理

3. 各种过滤器

在这里插入图片描述
详细释义如下:
在这里插入图片描述

二、项目搭建

1. ShiroConfig

这是Shiro 的主要配置类

@Configuration
public class ShiroConfig {
    /**
     * 配置Shiro生命周期处理器。
     * 在 Bean创建时调用 init 方法,在 销毁时调用 destroy 方法
     *
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 定制的Realm,来完成认证和鉴权的部分功能
     *
     * @return
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    /**
     * SecurityManager :核心类,具有极高的扩展性
     *
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置定制的 Realm
        securityManager.setRealm(customRealm());
        // 配置 Session管理器
        securityManager.setSessionManager(sessionManager());
//        // 设置缓存管理器
//        securityManager.setCacheManager();
//        // 设置 RememberMeManager 来管理 RememberMeManager
//        securityManager.setRememberMeManager();

        return securityManager;
    }


    /**
     * 设置 ShiroFilter
     * 这里可以配置 Shiro 对接口的控制权限
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 使用Map映射,key是接口路径,value为指定的Filter 名称,具体对应名称参考上图
        // 这里的配置是有顺序的,如果一个路径匹配多个过滤,那么只有先配置的过滤器生效
        Map<String, String> map = new HashMap<>();
        //配置登出接口
        map.put("/logout", "logout");
        // 放行登录接口
        map.put("/shiro/login", "anon");
        // 拦截其他接口
        map.put("/**", "authc");
        //设置登录路径,session失效会跳转该页面
        shiroFilterFactoryBean.setLoginUrl("/shiro/login");
        //登录成功页面
        shiroFilterFactoryBean.setSuccessUrl("/success");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 这里指定了动态代理的方式使用了 Cglib
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        //        设置是否直接代理目标类,而不是仅代理特定的接口。 默认值为“ false”。
        //        将此设置为“ true”可强制代理TargetSource的公开目标类。 如果该目标类是接口,则将为给定接口创建一个JDK代理。 如果该目标类是任何其他类,则将为给定类创建CGLIB代理。
        //        注意:如果未指定接口(并且未激活接口自动检测),则根据具体代理工厂的配置,也会应用代理目标类行为
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启注解支持,包括 RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class。
     * 和Aop相同的逻辑,通过注入 Advisor 来增强一些类的和方法
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 在 DefaultWebSessionManager 情况下可以自定义Cookies 的一些信息
     * @return
     */
    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;
    }

    /**
     * 设置SessionManager,由我们自己来控制Session
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        //配置监听
//        listeners.add(sessionListener());
//        sessionManager.setSessionListeners(listeners);
//        sessionManager.setSessionIdCookie(sessionIdCookie());
//        sessionManager.setSessionDAO(sessionDAO());
//        sessionManager.setCacheManager(ehCacheManager());

        //全局会话超时时间(单位毫秒),默认30分钟  暂时设置为10秒钟 用来测试
        sessionManager.setGlobalSessionTimeout(1800000);
        //是否开启删除无效的session对象  默认为true
        sessionManager.setDeleteInvalidSessions(true);
        //是否开启定时调度器进行检测过期session 默认为true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
        //设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
        //暂时设置为 5秒 用来测试
        sessionManager.setSessionValidationInterval(3600000);
        //取消url 后面的 JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

}

2. CustomRealm

CustomRealm 继承了AuthorizingRealm 类, 用来进行认证和鉴权的操作

public class CustomRealm extends AuthorizingRealm {

    /**
     * 权限认证
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        //查询用户名称
        User user = UserConstants.map.get(name);
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //添加权限
            for (Permissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 认证认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        //获取用户信息
        String name = authenticationToken.getPrincipal().toString();
        User user = UserConstants.map.get(name);
        if (user == null) {
            //这里返回后会报出对应异常
            return null;
        } else {
            //这里验证authenticationToken和simpleAuthenticationInfo的信息
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
            return simpleAuthenticationInfo;
        }
    }
}

3. ShiroDemoController

这里是测试用的控制层,为了简单,这里直接没有业务层,使用模拟数据

/**
 * @Email : kingfishx@163.com
 * @Data: 2020/11/19 14:04
 * @Des:
 */
@RequestMapping("shiro")
@RestController
public class ShiroDemoController {

    @PostMapping("admin")
    // 需要admin角色,这里就说明只有 “张三”的账号才能访问
    @RequiresRoles("admin")
    public String admin() {
        return "admin";
    }

    @PostMapping("user")
    // 需要user角色, 这里只有“李四的账号才能访问”
    @RequiresRoles("user")
    public String user(HttpServletRequest request) {
        HttpSession session = request.getSession(true);
        return "user";
    }

    @PostMapping("login")
    public String login(HttpServletRequest request, HttpServletResponse response) {
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("张三", "123456");
        Subject subject = SecurityUtils.getSubject();
        subject.login(usernamePasswordToken);
        return "login";
    }
}

4. pojo 类

	@Data
	public class Permissions {
	    private String id;
	    private String permissionsName;
	}

	@Data
	
	public class Role {
	
	    private String id;
	    private String roleName;
	    /**
	     * 角色对应权限集合
	     */
	    private Set<Permissions> permissions;
	
	    public Role() {
	    }
	
	    public Role(String id, String roleName, Set<Permissions> permissions) {
	        this.id = id;
	        this.roleName = roleName;
	        this.permissions = permissions;
	    }
	}

	@Data
	
	public class User {
	    private String id;
	    private String userName;
	    private String password;
	    /**
	     * 用户对应的角色集合
	     */
	    private Set<Role> roles;
	
	    public User(String id, String userName, String password, Set<Role> roles) {
	        this.id = id;
	        this.userName = userName;
	        this.password = password;
	        this.roles = roles;
	    }
	
	    public User() {
	    }
	}

5. UserConstants

这里对User 用户进行数据模拟

/**
 * @Email : kingfishx@163.com
 * @Data : 2021/1/28 10:54
 * @Desc : 模拟角色
 */
public class UserConstants {

    /**
     * 两个角色
     * 1. 张三 -》admin :具有 query,add 权限
     * 2. 李四 -》user : 具有 query 权限
     */
    public static Map<String, User> map = new HashMap<>();

    static {
        Permissions addPermissions = new Permissions("1", "query");
        Permissions queryPermissions = new Permissions("2", "add");

        Role adminRole = new Role("1", "admin", Sets.newHashSet(addPermissions, queryPermissions));
        Role userRole = new Role("2", "user", Sets.newHashSet(queryPermissions));

        User user = new User("1", "张三", "123456", Sets.newHashSet(adminRole));
        User user1 = new User("2", "李四", "123456", Sets.newHashSet(userRole));

        map.put(user.getUserName(), user);
        map.put(user1.getUserName(), user1);
    }

}

6. 异常拦截

全局异常拦截,方便看返回

@ControllerAdvice
@Slf4j
public class ShiroExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        log.error("没有通过权限验证!", e);
        return "没有通过权限验证! " + e.getMessage();
    }
}

三、验证

当我们使用张三账号登录后,是无权访问user接口的,但是却可以访问 admin接口
在这里插入图片描述
在这里插入图片描述


四、 shiro-redis 的集成

Shiro 集成 Redis 可以通过引入 shiro-redis 依赖实现

        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

使用方式也很简单,直接注入RedisSessionDao 即可:

    @Bean
    public SessionDAO sessionDao(JedisPool jedisPool) {
        // 使用redis
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        RedisManager redisManager = new RedisManager();
        redisManager.setJedisPool(jedisPool);
        sessionDAO.setExpire(10000);
        sessionDAO.setRedisManager(redisManager);
        return sessionDAO;
    }

1. 问题

但是由于其已经不再更新维护,以及jedis 2.x版本和 3.x版本的差异性(具体体现在哪个版本暂未考究),在某些情况下,如果项目引入了 jedis3.x 版本时会无法兼容,会出现如下错误

      <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>   
      </dependency>
***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

    org.crazycake.shiro.WorkAloneRedisManager.keys(WorkAloneRedisManager.java:149)

The following method did not exist:

    redis.clients.jedis.ScanResult.getStringCursor()Ljava/lang/String;

The method's class, redis.clients.jedis.ScanResult, is available from the following locations:

    jar:file:/F:/maven/maven-jar-home/redis/clients/jedis/3.3.0/jedis-3.3.0.jar!/redis/clients/jedis/ScanResult.class

The class hierarchy was loaded from the following locations:

    redis.clients.jedis.ScanResult: file:/F:/maven/maven-jar-home/redis/clients/jedis/3.3.0/jedis-3.3.0.jar


Action:

Correct the classpath of your application so that it contains a single, compatible version of redis.clients.jedis.ScanResult

                 
Disconnected from the target VM, address: '127.0.0.1:54508', transport: 'socket'

Process finished with exit code 1

究其原因是因为在 jedis 2.x 时 redis.clients.jedis.ScanResult#getStringCursor 在 jedis 3.x 中被替换成了 redis.clients.jedis.ScanResult#getCursor。而 org.crazycake.shiro.WorkAloneRedisManager#keys 中调用的仍是 ScanResult#getStringCursor
在这里插入图片描述

2. 解决方案

2.1 jedis 降级

这个是最直接的方案,将jedis 降级到 2.9 版本。

       <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

2.2 ShiroRedisManager

但是某些情况下,我们不能随意降级 jedis 的版本,便可以选择重写RedisManager。
我直接使用了反射来获取方法,获取到哪个执行哪个。

public class ShiroRedisManager extends RedisManager {
    private static Method cursorMethod;
    public ShiroRedisManager() {
        Class<ScanResult> scanResultClass = ScanResult.class;
        try {
            cursorMethod  = scanResultClass.getMethod("getStringCursor");
        } catch (NoSuchMethodException e) {
            try {
                cursorMethod = scanResultClass.getMethod("getCursor");
            } catch (NoSuchMethodException noSuchMethodException) {

            }
        }
        cursorMethod.setAccessible(true);

    }

    @Override
    public Set<byte[]> keys(byte[] pattern) {
        Set<byte[]> keys = new HashSet<byte[]>();
        Jedis jedis = getJedis();

        try {
            ScanParams params = new ScanParams();
            params.count(getCount());
            params.match(pattern);
            byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
            ScanResult<byte[]> scanResult;
            do {
                scanResult = jedis.scan(cursor, params);
                keys.addAll(scanResult.getResult());
                cursor = scanResult.getCursorAsBytes();
            } while (((String)cursorMethod.invoke(scanResult)).compareTo(ScanParams.SCAN_POINTER_START) > 0);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }  finally {
            jedis.close();
        }
        return keys;
    }
}

2.3 RedisSessionDAO

上面的两种方案都不适合解决我目前的问题,所以干脆重写了 RedisSessionDAO 。RedisSessionDAO 使用了 RedisTemplate 来完成Redis 的操作,其定义过程参考了 shiro-redis 的实现,仅在Session操作部分有所替换。使用的时候不再依赖 Jedis,而是依赖 RedisConnectionFactory。

package com.yunjiahealth.shiro.constants.config;

import com.google.common.collect.Lists;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.crazycake.shiro.SessionInMemory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;
import java.util.*;

/**
 * @Data: 2021/4/1 10:23
 * @Des: 自定义 RedisSessionDAO 解决 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;

    private static final boolean DEFAULT_SESSION_IN_MEMORY_ENABLED = true;

    private boolean sessionInMemoryEnabled = DEFAULT_SESSION_IN_MEMORY_ENABLED;

    // 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 ThreadLocal sessionsInThread = new ThreadLocal();

    private RedisTemplate<Serializable, Session> redisTemplate;

    private RedisConnectionFactory redisConnectionFactory;

    public RedisConnectionFactory getRedisConnectionFactory() {
        return redisConnectionFactory;
    }

    public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
        redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
    }

    public RedisTemplate<Serializable, Session> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<Serializable, Session> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @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);
        redisTemplate.opsForValue().set(getRedisSessionKey(sessionId), session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            logger.warn("session id is null");
            return null;
        }

        if (this.sessionInMemoryEnabled) {
            Session session = getSessionFromThreadLocal(sessionId);
            if (session != null) {
                return session;
            }
        }
        logger.debug("read session from redis");
        Session session = redisTemplate.opsForValue().get(getRedisSessionKey(sessionId));
        if (this.sessionInMemoryEnabled) {
            setSessionToThreadLocal(sessionId, session);
        }

        return session;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        redisTemplate.opsForValue().set(getRedisSessionKey(session.getId()), session);
        if (this.sessionInMemoryEnabled) {
            this.setSessionToThreadLocal(session.getId(), session);
        }
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }

        redisTemplate.delete(getRedisSessionKey(session.getId()));

    }

    @Override
    public Collection<Session> getActiveSessions() {

        Set<Serializable> keys = redisTemplate.keys(this.keyPrefix);
        if (null != keys) {
            // 批量获取数据
            return redisTemplate.opsForValue().multiGet(keys);
        } else {
            return Lists.newArrayListWithCapacity(0);
        }
    }

    private void removeExpiredSessionInMemory(Map<Serializable, SessionInMemory> sessionMap) {
        Iterator<Serializable> it = sessionMap.keySet().iterator();
        while (it.hasNext()) {
            Serializable sessionId = it.next();
            SessionInMemory sessionInMemory = sessionMap.get(sessionId);
            if (sessionInMemory == null) {
                it.remove();
                continue;
            }
            long liveTime = getSessionInMemoryLiveTime(sessionInMemory);
            if (liveTime > sessionInMemoryTimeout) {
                it.remove();
            }
        }
    }

    private void setSessionToThreadLocal(Serializable sessionId, Session s) {
        Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
        if (sessionMap == null) {
            sessionMap = new HashMap<>();
            sessionsInThread.set(sessionMap);
        }

        removeExpiredSessionInMemory(sessionMap);

        SessionInMemory sessionInMemory = new SessionInMemory();
        sessionInMemory.setCreateTime(new Date());
        sessionInMemory.setSession(s);
        sessionMap.put(sessionId, sessionInMemory);
    }

    private Session getSessionFromThreadLocal(Serializable sessionId) {

        if (sessionsInThread.get() == null) {
            return null;
        }

        Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
        SessionInMemory sessionInMemory = sessionMap.get(sessionId);
        if (sessionInMemory == null) {
            return null;
        }
        long liveTime = getSessionInMemoryLiveTime(sessionInMemory);
        if (liveTime > sessionInMemoryTimeout) {
            sessionMap.remove(sessionId);
            return null;
        }

        logger.debug("read session from memory");
        return sessionInMemory.getSession();
    }

    private long getSessionInMemoryLiveTime(SessionInMemory sessionInMemory) {
        Date now = new Date();
        return now.getTime() - sessionInMemory.getCreateTime().getTime();
    }

    private String getRedisSessionKey(Serializable sessionId) {
        return this.keyPrefix + sessionId;
    }

    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;
    }

    public boolean getSessionInMemoryEnabled() {
        return sessionInMemoryEnabled;
    }

    public void setSessionInMemoryEnabled(boolean sessionInMemoryEnabled) {
        this.sessionInMemoryEnabled = sessionInMemoryEnabled;
    }

    public static ThreadLocal getSessionsInThread() {
        return sessionsInThread;
    }
}

以上:内容部分参考
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944
https://blog.csdn.net/qq_30643885/article/details/91886448
https://blog.csdn.net/u012437781/article/details/78356037
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值