使用Redis实现分布式会话
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Redis实现分布式会话相关的知识,希望对你有一定的参考价值。
参考技术A传统的单体应用中,用户是否登录,通常是通过从Tomcat容器的session中获取登录用户信息判断的。
但在分布式的应用中,通常负载均衡了多台Tomcat,每台Tomcat都有自己独立的session,用户的每次请求都可能到达不同的Tomcat,因此可能会出现需要登录多次或者登录无效的情况出现。
为了解决这个问题,就需要使用一个缓存中间件,将登录用户的信息存入这个缓存中间件,所有的Tomcat都从缓存中间件中获取登录用户信息,从而判断登录状态。
Redis就是一个很好用的缓存中间件,接下来我们就来聊聊分布式应用该如何实现用户登录的逻辑。
传统的单体应用,通常只有一个Tomcat。
用户提交登录信息时,后台会根据用户登录名,得到用户信息,然后比对密码,如果正确,则将用户信息放入Tomcat的session中。
用户请求需要登录的接口时,先从Tomcat的session中得到用户信息,如果用户信息是null,则接口无法访问,提示用户登录。如果用户信息不为null,则使用用户信息完成接口逻辑。
用户提交登录信息时,后台根据用户登录名,得到用户信息,然后比对密码,如果正确,则生成一个随机数。
以这个随机数为key,用户信息为value,存入redis。
在cookie中存入一个固定值的key,例如:mySessionId,value为这个随机数。
用户请求需要登录的接口时,先从cookie中拿到随机数,然后以随机数为key,从redis中得到用户信息,如果用户信息不为null,则表示用户已登录。
用户注销登录时,将cookie中的随机数和redis中的用户信息都清空。
用户提交登录信息时,后台根据用户登录名,得到用户信息,然后比对密码,如果正确,则生成一个随机数。
将这个随机数放入用户信息中,在cookie中存入一个固定值的key,例如:curUser,value是脱敏后的用户信息。
以用户ID为key,这个随机数为value,存入redis。
用户请求需要登录的接口时,先从cookie中拿到用户信息,然后以用户信息中的用户ID为key,从redis中得到随机数。
如果redis中随机数不存在,则表示没有登录,如果存在,使用redis中得到的随机数与cookie中用户信息中的随机数做对比,如果不一致,则表示用户在其他地方登录,需要重新登录。
用户注销登录时,将cookie中的用户信息和redis中的随机数都清空。
今天简单聊了一下使用redis实现分布式会话,希望能对大家的工作有所帮助。
欢迎大家多多评论交流,共同成长。
关注追风人聊Java,每天更新Java干货。
分布式应用session会话管理-基于redis
session会话在单台服务器下不会出现共享问题,现在应用部署方式都是分布式,或者集群部署,这样必然会面临一个问题,session共享。
session共享的解决方案也有很多,
一、web服务器的粘性请求,比如采用nginx请求分发,使用ip_hash这种负载均衡方式,客户端请求只会被分发到相同的后台server,这样可以避免session共享的问题。但是缺点也很明显
二、基于数据库存储(网站用户量大的情况下,频繁dml数据,对db压力大)
三、基于cookie存储(安全问题、虽然可以加密存储、但是我觉得永远不能将敏感数据放在客户端,不信任啊O(∩_∩)O哈哈~)
四、服务器内置的session复制域(比如was下提供session复制功能、但这个损耗服务器内存)
五、基于nosql(memcache、redis都可以)
http请求是无状态的
这里要引入一个概念sessionid,session对象当客户端首次访问时,创建一个新的session对象.并同时生成一个sessionId,并在此次响应中将sessionId以响应报文的方式些回客户端浏览器内存或以重写url方式送回客户端,来保持整个会话
也就是说客户端request请求时候,如果获取了session,就默认分配一个jessionid,然后通过response响应到客户端cookie,然后客户端下一次请求,默认会携带这个jessionid请求到服务端,服务端拿到这个jessionid来区分不同的客户端。
说清楚这些,就可以从sessionid入手了,要实现将session信息存入redis,总结了下大概是三点:
1.实现httpsession接口,重写我们需要用到的方法,比如set get这些。。balabala一堆......
2.继承HttpServletRequestWrapper,这个类里面有getSession()方法,我们需要重写,来取自定义session
3.实现filter,用来拦截客户端请求,获取我们自定义的request,从而获得session
具体实现:
1.实现httpsession接口
public class HttpSessionWrapper implements HttpSession { protected final Logger logger = LoggerFactory.getLogger(getClass()); private String sid = ""; private HttpServletRequest request; private HttpServletResponse response; private final long creationTime = System.currentTimeMillis(); private final long lastAccessedTime = System.currentTimeMillis(); private SessionMeta meta; public HttpSessionWrapper() { } public HttpSessionWrapper(String sid,SessionMeta meta, HttpServletRequest request, HttpServletResponse response) { this.sid=sid; this.request=request; this.response=response; this.meta=meta; } public Object getAttribute(String name) { logger.info(getClass()+"getAttribute(),name:"+name); Jedis jedis =null; Object obj =null; String jsonStr = null; try { jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort()); jsonStr = jedis.get(name); if(jsonStr!=null||StringUtils.isNotEmpty(jsonStr)){ jedis.expire(name, meta.getSeconds());// 重置过期时间 obj =JSON.parseObject(jsonStr, User.class); //反序列对象 } if (jedis != null) { JedisPoolStore.getInstance().returnJedis(jedis); } return obj; } catch (JSONException je) { logger.error(je.getMessage()); if (null != jedis) JedisPoolStore.getInstance().returnJedis(jedis); return jsonStr; } catch (Exception e) { logger.error(e.getMessage()); if (e instanceof JedisException) { if (null != jedis) JedisPoolStore.getInstance().returnBrokenJedis(jedis); } else { if (null != jedis) JedisPoolStore.getInstance().returnJedis(jedis); } throw new HttpSessionException(" session 异常 getAttribute() name:"+name); } } public void setAttribute(String name, Object value) { logger.info(getClass()+"setAttribute(),name:"+name); Jedis jedis =null; try { jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort()); if(value instanceof String){ String value_ =(String) value; jedis.set(name,value_);//普通字符串对象 }else{ jedis.set(name, JSON.toJSONString(value));//序列化对象 } jedis.expire(name, meta.getSeconds());// 重置过期时间 if (jedis != null) { JedisPoolStore.getInstance().returnJedis(jedis); } } catch (Exception e) { logger.error(e.getMessage()); if (e instanceof JedisException) { if (null != jedis) JedisPoolStore.getInstance().returnBrokenJedis(jedis); } else { if (null != jedis) JedisPoolStore.getInstance().returnJedis(jedis); } throw new HttpSessionException(" session 异常 setAttribute() name:"+name+",value:"+value); } } /** * 不可用 * @deprecated * */ public void invalidate() { logger.info(getClass()+"invalidate()"); } public void removeAttribute(String name) { logger.info(getClass()+"removeAttribute(),name:"+name); if(StringUtils.isNotEmpty(name)){ Jedis jedis =null; try { jedis =JedisPoolStore.getInstance().getJedis(meta.getHost(), meta.getPort()); jedis.del(name); if (jedis != null) { JedisPoolStore.getInstance().returnJedis(jedis); } } catch (Exception e) { logger.error(e.getMessage()); if (e instanceof JedisException) { if (null != jedis) JedisPoolStore.getInstance().returnBrokenJedis(jedis); } else { if (null != jedis) JedisPoolStore.getInstance().returnJedis(jedis); } throw new HttpSessionException(" session 异常 removeAttribute() name:"+name); } } } /** * 不可用 * @deprecated * */ public Object getValue(String name) { return null; } /** * 不可用 * @deprecated * */ public Enumeration getAttributeNames() { return null; } /** * 不可用 * @deprecated * */ public String[] getValueNames() { return null; } /** * 不可用 * @deprecated * */ public void putValue(String name, Object value) { } /** * 不可用 * @deprecated * */ public void removeValue(String name) { } public long getCreationTime() { return creationTime; } public String getId() { logger.info(getClass()+"getId():"+sid); return sid; } public long getLastAccessedTime() { return lastAccessedTime; } /** * 不可用 * @deprecated * */ public ServletContext getServletContext() { return null; } /** * 不可用 * @deprecated * */ public void setMaxInactiveInterval(int interval) { } /** * 不可用 * @deprecated * */ public int getMaxInactiveInterval() { return 0; } /** * 不可用 * @deprecated * */ public HttpSessionContext getSessionContext() { return null; } /** * 不可用 * @deprecated * */ public boolean isNew() { logger.info(getClass()+"isNew()"); return false; } }
2.继承HttpServletRequestWrapper
/*** * * @author xiaoshuai * */ public class DefinedHttpServletRequestWrapper extends HttpServletRequestWrapper{ protected final Logger logger = LoggerFactory.getLogger(getClass()); private HttpSessionWrapper currentSession; private HttpServletRequest request; private HttpServletResponse response; private String sid = ""; private SessionMeta meta; public DefinedHttpServletRequestWrapper(HttpServletRequest request) { super(request); } public DefinedHttpServletRequestWrapper(String sid, HttpServletRequest request) { super(request); this.sid = sid; } public DefinedHttpServletRequestWrapper(String sid, SessionMeta meta,HttpServletRequest request, HttpServletResponse response) { super(request); this.request = request; this.response = response; this.sid = sid; this.meta=meta; } @Override public HttpSession getSession(boolean create) { if(currentSession != null) { return currentSession; } if(!create) { return null; } currentSession = new HttpSessionWrapper(sid,meta, request, response); return currentSession; } @Override public HttpSession getSession() { return getSession(true); } }
3.实现filter
public class SessionFilter implements Filter{ protected final Logger logger = LoggerFactory.getLogger(getClass()); private static SessionMeta meta = new SessionMeta(); private static final String host ="host"; private static final String port ="port"; private static final String seconds="seconds"; public void init(FilterConfig filterConfig) throws ServletException { logger.debug("init filterConfig info"); meta.setHost(filterConfig.getInitParameter(host)); meta.setPort(Integer.parseInt(filterConfig.getInitParameter(port))); meta.setSeconds(Integer.parseInt(filterConfig.getInitParameter(seconds))); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //从cookie中获取sessionId,如果此次请求没有sessionId,重写为这次请求设置一个sessionId HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String sid = CookieHelper.findCookieInfo(httpRequest,CookieHelper.COOKIE_DOMAIN_NAME); if(StringUtils.isEmpty(sid) ){ try { sid =CookieHelper.saveCookie(SessionId.doIds(), httpResponse); } catch (Exception e) { e.printStackTrace(); } } logger.info("JESSIONID:"+sid); chain.doFilter(new DefinedHttpServletRequestWrapper(sid,meta,httpRequest, httpResponse), response); } public void destroy() { } }
3.配置web.xml
<!-- session过滤器 --> <filter> <filter-name>sessionFilter</filter-name> <filter-class>cn.session.filter.SessionFilter</filter-class> <init-param> <param-name>host</param-name> <param-value>10.1.193.1</param-value> </init-param> <init-param> <param-name>port</param-name> <param-value>6372</param-value> </init-param> <init-param> <param-name>seconds</param-name> <param-value>1800</param-value> </init-param> </filter> <filter-mapping> <filter-name>sessionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
第一次产生sessionid访问:
系统登录后存用户信息至redis:
关于安全这块,因为不管登录系统与否,sessionid都会产生,这时候就会产生一个问题,因为cookie是可以被修改的,就会产生一个问题,撞session的分享。。。换成不同的sessionid去请求系统。。。总有一天会撞上。。
SO,我这边是这样处理的,
当登录成功之后,产生一个token令牌,产生规则的话自己定,一堆密钥比如系统名字+sessionid+userid+固定字符,产生一个加密的字符串,放入cookie。
这样当我们获取当前登录用户时,解密token,获取sessionid,然后取redis用户信息。。(切记这里不要直接通过sessionid取用户信息,有风险!!!)
用户没有登录成功,自然也没有这个令牌。。。
这里又有另外一个问题,用户如果禁用cookie呢????? so what..... 下次再说这个。。。
以上是关于使用Redis实现分布式会话的主要内容,如果未能解决你的问题,请参考以下文章
springCloud-依赖Spring Security使用 JWT实现无状态的分布式会话
帅气的 Spring Session 功能,基于 Redis 实现分布式会话,还可以整合 Spring Security!