公司的web服务器经过分布式之后,对于会话保持的功能完全依赖于nginx的ip会话保持,然而生产上仍然会出现突然会话失效导致用户被踢出的问题,,经过多次问题分析初步怀疑nginx会话保持功能偶尔失效的可能。所以做一个分布式session的组件来保证即使路由到不同服务器也不会被强制下线。
整体思路采用所有redis的字段全部存于redis上,本机只保留sessionid指针的方式,并不会把整个session拉下来缓存,而是以redis的hash结构,对于需要的sessionkey做单点查询。
实现方式是在web.xml中定义第一拦截器将Request对象替换为自定义的request,采用装饰模式重写构造方法与getSession方法。采用的是隐性替换的方式。。这里说一下装饰模式的原因是源码追踪时候发现原本过来的servletRequest就已经有了一个装饰器总线HttpServletRequestWrapper,那么还是延用了之前的设计思想。
先上一些代码 : 注:Ballon是公司封装了一层redis的外观,其实就是jedis连接池。到时候替换下就行了。
首先在web.xml中加入第一拦截器
RedisSessionFilter
cn.com.yitong.ares.filter.RedisSessionFilter
拦截器RedisSessionFilter.java
注* :登录时重新刷新session是因为要保证即使用户退出时并未成功销毁session,也会写到不同session进行下次登录。因为是采用的会话作为用户身份标识,所以对会话安全要求较高
package cn.com.yitong.ares.filter;
import java.io.IOException;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cfca.org.slf4j.MDC;
import cn.com.yitong.ares.consts.AresR;
import cn.com.yitong.modules.session.RedisHttpServletRequestWrapper;
import cn.com.yitong.modules.session.RedisHttpSession;
/**
* 转换request为对象类型为redis依赖request
* @author Mordred
*
*/
public class RedisSessionFilter implements Filter{
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("redis session filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
MDC.clear();
HttpServletRequest re = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
RedisHttpServletRequestWrapper wrapper = new RedisHttpServletRequestWrapper(re,res);
RedisHttpSession session = (RedisHttpSession)wrapper.getSession();
String sessionId = session.getId();
String url = wrapper.getRequestURL().toString();
//登录时刷新session
if(checkUrlPattern(url, Pattern.compile("/login/HTLogin.do"))){
String loginName = session.getAttributeObject(AresR.MDC_USER_ID, String.class);
if(StringUtils.isNotBlank(loginName)){
logger.info("登录时携带上一次会话,给予新sessionId loginName={}", loginName);
session.invalidate();
sessionId = wrapper.getSession().getId();
}
}
logger.info("request : url={}, sessionId={}", url, sessionId);
chain.doFilter(wrapper, response);
}
@Override
public void destroy() {
logger.info("redis session filter destroy");
}
private boolean checkUrlPattern(String url, Pattern pattern) {
java.util.regex.Matcher m = pattern.matcher(url);
return m.find();
}
}
RedisHttpServletRequestWrapper.java
package cn.com.yitong.modules.session;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.com.yitong.ares.channel.Resource;
import cn.com.yitong.modules.dmb.util.CommonSeqUtil;
import cn.com.yitong.modules.reload.ReloadUtil;
import cn.com.yitong.util.security.MD5Encrypt;
/**
* redis存session入口request类,负责创建与刷新session
* @author Mordred
*
*/
public class RedisHttpServletRequestWrapper extends HttpServletRequestWrapper{
private RedisHttpSession session;
private HttpServletResponse response;
private HttpServletRequest request;
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 存于cookie中的sessionId键值
*/
public static final String session_key = "JSESSIONID";//redisSessionId
/**
* 存于redis中的sessionId前缀
*/
public static final String session_prefix = "sys_session:";
public static final String session_exist_key = "sys_exist_symbol";
public RedisHttpServletRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
super(request);
this.request = request;
this.response = response;
}
@Override
public HttpSession getSession(boolean create) {
if(session != null && session.isExist()){
return session;
}
Cookie[] cookies = request.getCookies();
String redisSessionId = null;
if(cookies != null){
for(Cookie cookie : cookies){
if(session_key.equals(cookie.getName())){
redisSessionId = cookie.getValue();
logger.info("cookie : {}--{} ", session_key, redisSessionId);
break;
}
}
}
if(create){
if(StringUtils.isBlank(redisSessionId)){
String newSessionId = saveNewSession();
writeSidToCookie(newSessionId);
}else if(isExist(redisSessionId)){
refreshSession(redisSessionId);
}else{
String newSessionId = saveNewSession();
writeSidToCookie(newSessionId);
}
}else{
if(!StringUtils.isBlank(redisSessionId)){
if(isExist(redisSessionId)){
refreshSession(redisSessionId);
}
}
}
return session;
}
@Override
public HttpSession getSession() {
return getSession(true);
}
/**
* 保存新session
* @return
*/
protected String saveNewSession(){
String redisSessionId = MD5Encrypt.MD5(CommonSeqUtil.getSeqNo());
saveSession(redisSessionId);
return redisSessionId;
}
/**
* 判断session是否存在于redis
* @param redisSessionId
* @return
*/
protected boolean isExist(String redisSessionId){
return "Y".equals(ReloadUtil.getBallon().hget(session_prefix + redisSessionId, session_exist_key));
}
/**
* 保存session于redis
* @param redisSessionId
*/
protected void saveSession(String redisSessionId){
ReloadUtil.getBallon().hset(session_prefix + redisSessionId, session_exist_key, "Y");
refreshSession(redisSessionId);
}
/**
* 刷新session失效时间
* @param redisSessionId
*/
protected void refreshSession(String redisSessionId){
ReloadUtil.getBallon().expire(session_prefix + redisSessionId, Resource.getInt("SESSION_TIME_OUT"));
setSession(redisSessionId);
}
/**
* 设置session于类内部,只记录sessionId,内存中不存其余任何属性
* @param redisSessionId
*/
protected void setSession(String redisSessionId){
session = new RedisHttpSession(null, redisSessionId);
}
/**
* 返回sessionId给浏览器
* @param sid
*/
protected void writeSidToCookie(String sid) {
Cookie mycookies = new Cookie(session_key, sid);
String project = request.getContextPath();
if(StringUtils.isNotBlank(project)){
mycookies.setPath(project);
}
mycookies.setMaxAge(-1); //60*60*24*365
response.addCookie(mycookies);
}
}
RedisHttpSession.java
其实只是一个壳子,里面只缓存了sessionid,剩下的字段都没有。所有数据都是去redis获取
注* :有的地方没有做assertExist校验是因为跑起来会报错,进去看了一下是一个listener类,会间歇性的调用我重写的方法,但是本身这个类就不存在实体字段所以过不去校验,会抛出异常。
package cn.com.yitong.modules.session;
import java.lang.reflect.ParameterizedType;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import javax.servlet.http.HttpSession;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSON;
import cn.com.yitong.modules.reload.ReloadUtil;
/**
* session存入redis核心类,重写session中取值及赋值逻辑
* @author Mordred
*
*/
public class RedisHttpSession extends HttpSessionWrapper {
private String sid;
public static boolean invalidate(String sid){
long flag = ReloadUtil.getBallon().del(RedisHttpServletRequestWrapper.session_prefix + sid);
return flag == 1;
}
public RedisHttpSession(HttpSession session, String sid) {
super(session);
this.sid = sid;
}
@Override
public Enumeration getAttributeNames() {
assertNotExist();
Vector keys = new Vector(ReloadUtil.getBallon().hkeys(RedisHttpServletRequestWrapper.session_prefix + sid));
keys.remove(RedisHttpServletRequestWrapper.session_exist_key);
return keys.elements();
}
@Override
public void setAttribute(String name, Object value) {
ReloadUtil.getBallon().hset(RedisHttpServletRequestWrapper.session_prefix + sid, name, JSON.toJSONString(value));
}
@Override
public Object getAttribute(String name) {
String valueString = ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, name);
return JSON.parse(valueString);
}
/**
* 外部调用,无需强转
* @param name
* @return
*/
public T getAttributeObject(String name , Class clazz){
assertNotExist();
Assert.notNull(name, "Attribute name must not be null");
String valueString = ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, name);
return (T)JSON.parseObject(valueString, clazz);
}
public void setAttributes(Map map){
assertNotExist();
Assert.notNull(map, "Attributes map must not be null");
Set keys = map.keySet();
for(String key : keys) {
setAttribute(key, map.get(key));
}
// ReloadUtil.getBallon().hmset(RedisHttpServletRequestWrapper.session_prefix + sid, map);
}
@Override
public void removeAttribute(String name){
Assert.notNull(name, "Attribute name must not be null");
ReloadUtil.getBallon().hdel(RedisHttpServletRequestWrapper.session_prefix + sid, name);
};
@Override
public String getId() {
return sid;
}
@Override
public void invalidate(){
RedisHttpSession.invalidate(sid);
}
protected boolean isExist(){
return "Y".equals(ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, RedisHttpServletRequestWrapper.session_exist_key));
}
protected void assertNotExist(){
if(!isExist()){
throw new IllegalStateException("The session does not exist");
}
}
}
HttpSessionWrapper.java 模拟requestWrapper,提供装饰器总线
package cn.com.yitong.modules.session;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
/**
* 装饰器总类,提供可override入口
* @author Mordred
*
*/
public class HttpSessionWrapper implements HttpSession {
private HttpSession session;
public HttpSessionWrapper(HttpSession session) {
this.session = session;
}
@Override
public long getCreationTime() {
return this.session.getCreationTime();
}
@Override
public String getId() {
return this.session.getId();
}
@Override
public long getLastAccessedTime() {
return this.session.getLastAccessedTime();
}
@Override
public ServletContext getServletContext() {
return this.session.getServletContext();
}
@Override
public void setMaxInactiveInterval(int interval) {
this.session.setMaxInactiveInterval(interval);
}
@Override
public int getMaxInactiveInterval() {
return this.session.getMaxInactiveInterval();
}
@Override
public HttpSessionContext getSessionContext() {
return this.session.getSessionContext();
}
@Override
public Object getAttribute(String name) {
return this.session.getAttribute(name);
}
@Override
public Object getValue(String name) {
return this.session.getValue(name);
}
@Override
public Enumeration getAttributeNames() {
return this.session.getAttributeNames();
}
@Override
public String[] getValueNames() {
return this.session.getValueNames();
}
@Override
public void setAttribute(String name, Object value) {
this.session.setAttribute(name,value);
}
@Override
public void putValue(String name, Object value) {
this.session.putValue(name,value);
}
@Override
public void removeAttribute(String name) {
this.session.removeAttribute(name);
}
@Override
public void removeValue(String name) {
this.session.removeValue(name);
}
@Override
public void invalidate() {
this.session.invalidate();
}
@Override
public boolean isNew() {
return this.session.isNew();
}
}
最后因为只重写了一部分方法,如果需要真正的隐式替换,那么要不保证全部方法重写,要不然就在上层做更高的外观,让业务接口调不到session,只能使用我们外放的方法。项目采用的第二种。
其实最开始上测试环境之前cookie里面定义的sessionid字段是叫做redisSessionId。但是IOS会出现问题,导致非JSESSIONID的cookie无法携带。
只是一个简单的request替换,肯定还是会有一些问题,但是暂时还是可以使用的。