SpringCloud分布式环境下Shiro通过redis实现权限管理控制

为什么要做Session共享

什么是session

我们都知道HTTP协议(1.1)是无状态的,所以服务器在需要识别用户访问的时候,就要做相应的记录用于跟踪用户操作,这个实现机制就是Session。当一个用户第一次访问服务器的时候,服务器就会为用户创建一个Session,每个Session都有一个唯一的SessionId(应用级别)用于标识用户。

Session通常不会单独出现,因为请求是无状态的,那么我们必须让用户在下次请求时带上服务器为其生成的Session的ID,通常的做法时使用Cookie实现(当然你要非要在请求参数中带上SessionId那也不是不行)。请求返回时会向浏览器的Cookie中写入SessionID,通常使用的键是JSESSIONID,这样下次用户再请求这台服务器时,服务器就能从Cookie中取出SessionId识别出该次请求的用户是谁。

例如:
在这里插入图片描述
左边红框部分是Cookie列表,当前服务器是:localhost:28080。右边红框部分从左到右依次是Cookie的键、值、主机、路径和过期时间。路径为/时表示全站有效,最后一个过期时间未设置的话是默认值为Session,表示浏览器关闭时该Cookie失效。我们也可以为Cookie指定过期时间,以做到会话保持。

什么是session共享

通过Session和Cookie,我们使得无状态的HTTP协议间接的变成了有状态的了,可以实现保持登录,存储用户信息,购物车等等功能。但是随着服务访问人数的增多,单台服务器已经不足以应付所有的请求了,必须部署集群环境。但是随着集群环境的出现,追踪用户状态的问题又开始出现问题,之前用户在A服务器登录,A服务器保存了用户信息,但是下一次请求发送到B服务器去了,这时候B服务器是不知道用户在A服务器登录的事情的,它虽然也能拿到用户请求Cookie中的SessionId,但是在B服务根据这个SessionId找不到对应的Session,B服务器就会认为用户没有登录,需要用户重新登录,这对用户来说是没办法接受的。

这时候常见的有两种方式解决这个问题,第一种是让这个用户所有的请求都发送到A服务器,比如根据IP地址做一些列算法将所有用户分配到不同的服务器上去,让每个用户只访问其中的一台服务器。这种做法可行,但是后续也会产生其它问题,更好的做法是第二种,将所有的服务器上的Session都做成共享的,A服务能拿到B服务器上的所有Session,同理B服务器也能获取A服务器所有的Session,这样上面的问题就不存在了。

Shiro结合Redis实现Session共享

上一篇已经通过Shiro实现了用户登录和权限管理,Shiro的登录也是基于Session的,默认情况下Session是保存在内存中。既然要做Session共享,那么肯定是将Session抽取出来,放到一个多个服务器都能访问到的地方。

在集群环境下,我们仅仅需要继承AbstractSessionDAO,实现一下Session的增删改查等几个方法就可以很方便的实现Session共享,Shiro已经将完整的流程都做好了。这里涉及到的设计模式是模板方法模式,我们仅需要参与部分业务就可以完善整个流程了,当然我们不参与这部分流程的话,Shiro也有默认的实现方式,那就是将Session管理在当前应用的内存中。

具体的Session管理(共享)怎么实现由我们自己决定,可以存放在数据库,也可以通过网络传输,甚至可以通过IO流写入文件都行,但就性能来讲,我们一般都将Session放入Redis中。

自定义RedisSessionDAO

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author somebody
 */
public class RedisSessionDao extends AbstractSessionDAO {
	

    /**
     * Session超时时间(秒)
     */
    private long expireTime;

    public RedisSessionDao(long expireTime) {
        this.expireTime = expireTime;
    }

    @SuppressWarnings("rawtypes")
	@Autowired
    private RedisTemplate redisTemplate;
    
//    private int redisTemplateCount = 0;

    @SuppressWarnings("unchecked")
	@Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
        return sessionId;
    }
    
    @Override
    protected Session doReadSession(Serializable sessionId) {
    	return sessionId == null ? null : (Session) redisTemplate.opsForValue().get(sessionId);
    }
    
    @SuppressWarnings("unchecked")
	@Override
    public void update(Session session) throws UnknownSessionException {
        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
    }

    @SuppressWarnings("unchecked")
	@Override
    public void delete(Session session) {
        if (session != null && session.getId() != null) {
            redisTemplate.opsForValue().getOperations().delete(session.getId());
        }
    }

    @SuppressWarnings("unchecked")
	@Override
    public Collection<Session> getActiveSessions() {
        return redisTemplate.keys("*");
    }

}

以上代码需要引入相关依赖,这里的版本为2.3.12.RELEASE

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

并添加redis服务相关配置信息

###redis连接配置
# dev env
spring.redis.host=192.168.8.225
spring.redis.port=6379
spring.redis.password=redis的密码

# Redis连接池最大连接数
# spring.redis.jedis.pool.max-active=1000
# Redis连接池最大空闲连接数
# spring.redis.jedis.pool.max-idle=100

# 连接超时时间
spring.redis.timeout=15000
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=3
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=2
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=3
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#在关闭客户端连接之前等待任务处理完成的最长时间,在这之后,无论任务是否执行完成,都会被执行器关闭,默认100ms
spring.redis.lettuce.shutdown-timeout=100

注入RedisSessionDao

上面只是我们自己实现的管理Session的方式,现在需要将其注入SessionManager中,并设置过期时间等相关参数。

这里有两种方式将RedisSessionDao注入进SessionManager:

  1. 直接注入RedisSessionDao
    • 优点:不会存在session的设置的值更新不同步问题
    • 缺点:系统的访问速度又很慢
  2. 将RedisSessionDao封装进shiro的缓存sessionDAO : CachingSessionDAO, 这样做的目的是大大提供系统的访问速度
    • 优点:系统访问速度快
    • 缺点:session的设置的值更新不同步
  3. 上述第2点中的CachingSessionDAO是继承自AbstractSessionDAO的。
    在这里插入图片描述

这里采用第2中方式,这种方式能保证系统能有较快的访问速度, 至于“session的设置的值更新不同步” 的问题待后续解决

CachingSessionDAO封装RedisSessionDao


import java.io.Serializable;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;

public class LocalRedisSessionDAO extends CachingSessionDAO {

	@Autowired
	private RedisSessionDao redisSessionDao;
	
	@Override
	protected void doUpdate(Session session) {
		redisSessionDao.update(session);
		cache(session,session.getId());
	}

	@Override
	protected void doDelete(Session session) {
		redisSessionDao.delete(session);
		uncache(session);
	}
	@Override
	protected Session doReadSession(Serializable sessionId) {
//		return redisSessionDao.readSession(sessionId);
		
		Session session = getCachedSession(sessionId);
        if (session == null) {
            session = redisSessionDao.readSession(sessionId);
            if (session != null) {
                cache(session, session.getId());
            }
        }
        return session;
		
	}
	
	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = generateSessionId(session);
		assignSessionId(session,sessionId);
		cache(session, sessionId);
		return sessionId;
	}

}

在ShiroConfig.java中配置RedisSessionDao 和LocalRedisSessionDAO

    @Bean
    public RedisSessionDao redisSessionDao() {
        return new RedisSessionDao(expireTime);
    }
    
    /**
     * John Abruzzi
     * @return
     */
    @Bean
    public LocalRedisSessionDAO localRedisSessionDAO() {
    	LocalRedisSessionDAO localRedisSessionDAO = new LocalRedisSessionDAO();
    	localRedisSessionDAO.setCacheManager(ehCacheManager());
    	localRedisSessionDAO.setActiveSessionsCacheName("shiroSessionCache");
    	return localRedisSessionDAO;
    }

将LocalRedisSessionDAO注入进SessionManager

    @Bean
    public MySessionManager mySessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        // 配置自定义SessionDao,直接使用redisSessionDao不会存在session的设置的值更新不同步问题,但是系统的访问速度又很慢
//        mySessionManager.setSessionDAO(redisSessionDao());
        // 将redisSessionDao改为本地缓存DAO: localRedisSessionDAO,使用本地缓存可以提高访问速度,但是存在session的设置的值更新不同步问题
        mySessionManager.setSessionDAO(localRedisSessionDAO());
//        mySessionManager.setGlobalSessionTimeout(expireTime * 1000);
        mySessionManager.setGlobalSessionTimeout(5 * 1000);
        return mySessionManager;
    }

最后再将SessionManager注入Shiro的安全管理器SecurityManager中,前面说过,我们围绕安全相关的所有操作,都需要与SecurityManager打交道,这位才是Shiro中真正的老大哥。

    @Bean(name = "SecurityManager")
    public SecurityManager securityManager(){
    	
    	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    	//设置realm.
        securityManager.setAuthenticator(modularRealmAuthenticator());
        List<Realm> realms = new ArrayList<>();
        //添加多个Realm
        realms.add(realm1);
        realms.add(realm2);
        realms.add(realm3);
		// 注入缓存管理器;
		securityManager.setCacheManager(ehCacheManager());
		securityManager.setRealms(realms);
		
		// 取消Cookie中的RememberMe参数
        securityManager.setRememberMeManager(null);
        // 配置自定义Session管理器
		securityManager.setSessionManager(mySessionManager());
		
        return securityManager;
    }

在springCloud分布式环境下,使用openfeign调用远程服务器的的微服务时,需要手动传递cookie

在feign的请求拦截器中,实现拦截并将 源请求的cookie传递给feign的请求

requestTemplate.header("Cookie", request.getHeader("Cookie")); // 新request

feign拦截器的完整代码:

import javax.servlet.http.HttpServletRequest;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.micrometer.core.instrument.util.StringUtils;
import io.seata.core.context.RootContext;

@Configuration
public class ComonConfig {

	@Bean
	public RequestInterceptor requestInterceptor() {
		return new RequestInterceptor() {
			@Override
			public void apply(RequestTemplate requestTemplate) {
				ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
						.getRequestAttributes();
				HttpServletRequest request = attributes.getRequest(); // 老 request
				
	            // 从Seata获取XID,并将其添加到Feign请求标头中
	            String xid = RootContext.getXID();
	            if (xid != null) {
	                requestTemplate.header("TX_XID", xid);
	            }
				
				requestTemplate.header("Cookie", request.getHeader("Cookie")); // 新request
				String pageSizeDefault = "20";
				String pageNumDefalut = "1";
				String pageSize = request.getParameter("pageSize");
				String pageNum = request.getParameter("pageNum");
				if (StringUtils.isNotEmpty(pageSize)) {
					pageSizeDefault = pageSize;
				}
				if (StringUtils.isNotEmpty(pageNum)) {
					pageNumDefalut = pageNum;
				}
				requestTemplate.header("pageSize", pageSizeDefault);
				requestTemplate.header("pageNum", pageNumDefalut);
//				requestTemplate.header("Accept", "application/json;charset=UTF-8");
//				requestTemplate.header("Content-Type", "application/json;charset=UTF-8");
			}
		};
	}
}

验证

分别使用2种浏览器登录2个不同的用户,在redis中查看对应的存储的sessionId
在这里插入图片描述

由上图可知,浏览器的SessionId均被存入到了Redis中。因此,其他应用/服务便可以通过Redis获取到对应的Session,来完成身份的认证!!

结束!

参考1:https://www.cnblogs.com/wffzk/p/15102435.html

参考2:Shiro权限管理框架(二):Shiro结合Redis实现分布式环境下的Session共享

参考3demo:https://gitee.com/guitu18/ShiroDemo

Gitee账号中 :已Star

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值