1. 基于Cookie+Redis+Filter解决方案
用户登录之后,将Session Id和用户信息存储到Redis中,并添加一个Cookie,将该Session Id带到客户端。当发起其他请求之后,携带该Cookie,应用服务器获取到Session Id之后去Redis中查询是否存在,如果存在则继续进行相关业务,否则提示用户未登录。那种在Cookie中存放用户信息的方式直接Pass掉了。
实现过程
登录过程
public ServerResponse<User> login(String username,String password, HttpSession session, HttpServletResponse response){
//获取数据库中的用户信息
ServerResponse<User>serverResponse = iUserService.login(username,password);
if (serverResponse.isSuccess()){
StringsessionId = session.getId();
//添加Cookie
CookieUtil.writeCookie(response,sessionId);
//将sessionId和Json化的用户信息保存到分片的Redis中
RedisShardedUtil.setEx(sessionId,JsonUtil.obj2String(serverResponse.getData()),Const.RedisCache.SESSION_EXTIME);
}
return serverResponse;
}
刷新“Session”有效时间
假设“Session”的默认时间设置为半个小时,当我们登录之后,每次请求都应该将Session的有效期设置为半个小时。否则的话,一到半个小时“Session”失效了用户还得重新登录。为了解决这个问题,我们设置一个过滤器:
public class SessionExpireFilter implements Filter {
@Override
public void init(FilterConfigfilterConfig) throws ServletException {
}
@Override
public voiddoFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException {
//将ServletRequest转换为HttpServletRequest
HttpServletRequestrequest = (HttpServletRequest)servletRequest;
Stringtoken = CookieUtil.readCookieValue(request);
//如果token不为空的话,符合条件,则获取user信息,user不为空,则将redis缓存中的session时间重置为指定时时长
if(StringUtils.isNotBlank(token)){
StringuserJsonStr = RedisShardedUtil.get(token);
Useruser = JsonUtil.string2Obj(userJsonStr,User.class);
if(user!= null){
//如果user不为空,则重置session的时间,即调用expire命令
RedisShardedUtil.expire(token,Const.RedisCache.SESSION_EXTIME);
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
在web.xml中配置该过滤器:
<!--注册延长"session"有效时间的filter,注意:需要配置在其他业务过滤器之前-->
<filter>
<filter-name>sessionExpireFilter</filter-name>
<filter-class>com.lcmall.common.filter.SessionExpireFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sessionExpireFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
123456789
其中CookieUtil封装的代码如下,注释很详细,就不再解释了:
/**
* cookie操作工具类
* @author wlc
*/
@Slf4j
public class CookieUtil {
/**设置为一级域名,子域名就可以共享该一级域名下的cookie*/
private static final StringCOOKIE_DOMAIN = ".lcmall.com";
/**设置cookie的目录为根目录"/",子级目录可以共享*/
private static final StringCOOKIE_PATH = "/";
/**设置cookie的name,读写均使用它*/
private static final StringCOOKIE_NAME = "login_token";
/**
* 读取cookie中的value
* @param request
* @return
*/
public static StringreadCookieValue(HttpServletRequest request){
Map<String,Cookie>cookieMap = getCookieMap(request);
if(cookieMap.containsKey(COOKIE_NAME)){
Cookiecookie = cookieMap.get(COOKIE_NAME);
log.info("returncookieName:{},cookieValue:{}",cookie.getName(),cookie.getValue());
returncookie.getValue();
}
returnnull;
}
/**
* 写入cookie,下面解释一下domain与path
* //X:domain=".lcmall.com"
* //a:A.lcmall.com cookie:domain=A.lcmall.com;path="/"
* //b:B.lcmall.com cookie:domain=B.lcmall.com;path="/"
* //c:A.lcmall.com/test/cc cookie:domain=A.lcmall.com;path="/test/cc"
* //d:A.lcmall.com/test/dd cookie:domain=A.lcmall.com;path="/test/dd"
* //e:A.lcmall.com/test cookie:domain=A.lcmall.com;path="/test"
*
* //由于domain和path的设置以上的结果如下:
* //a,b,c,d,e都能拿到X这个domain下的cookie
* //a与b相互之间是拿不到之间的cookie的
* //c与d均能够共享a与e产生的cookie
* //a与b相互之间是拿不到之间的cookie的,c、d均拿不到b的
* @param response
* @param token
* @return
*/
public static voidwriteCookie(HttpServletResponse response, String token){
Cookiecookie = new Cookie(COOKIE_NAME,token);
cookie.setDomain(COOKIE_DOMAIN);
//设置cookie的访问仅通过http方式,可一定程度防止脚本攻击
cookie.setHttpOnly(true);
//如果不设置该值,则cookie不会保存到硬盘中,只存在于内存中,只在当前页面有效。
//单位为s,这里设置为一年,如果设置为-1,则代表永久
cookie.setMaxAge(60*60*24*365);
cookie.setPath(COOKIE_PATH);
log.info("wirtecookie name:{},value:{}",cookie.getName(),cookie.getValue());
response.addCookie(cookie);
}
/**
* 删除cookie
* @param response
* @return
*/
public static voiddelCookie(HttpServletRequest request,HttpServletResponse response){
Map<String,Cookie>cookieMap = getCookieMap(request);
if(cookieMap.containsKey(COOKIE_NAME)){
Cookiecookie = cookieMap.get(COOKIE_NAME);
cookie.setDomain(COOKIE_DOMAIN);
cookie.setPath(COOKIE_PATH);
//设置成0,代表删除此cookie
cookie.setMaxAge(0);
log.info("delcookieName:{},cookieValue:{}",cookie.getName(),cookie.getValue());
response.addCookie(cookie);
}
}
/**
* 将request中的cookie包装成一个map,实现代码复用
* @param request
* @return
*/
private staticMap<String,Cookie> getCookieMap(HttpServletRequest request){
Cookie[]cookies = request.getCookies();
Map<String,Cookie>cookieMap = new HashMap<>();
if(cookies != null){
for(Cookie cookie: cookies) {
cookieMap.put(cookie.getName(),cookie);
}
}
returncookieMap;
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
RedisShardedUtil的封装过程可以参见博客:分布式redis连接池工具类的封装
JsonUtil可以自己写,也可以使用现成的Json工具类,如FastJson等,这里是对Jackson进行的二次封装。
上面可以看出,这个方案其实并不是使用真正的Session。而且不论是明文还是加密之后的用户信息并没有放到Cookie中,所以一般情况下也不存在用户数据泄露的问题。测试方法可参见:Tomcat集群的Debug方法
优缺点
优点
代码灵活,基于分布式Redis,可以实现对高并发请求的支持。
缺点
需要修改的代码较多,涉及到Session的地方都需要更改。不太适合对老系统的改造,比较适合于新开发的系统。但是如果我们提前将用户接口抽离成了一个单独的服务,那么改造起来还是比较好处理的。
踩坑
An invalid domain [.lcmall.com] was specified for thiscookie
原因是Tomcat8.5以后,Cookie的校验规则更改了,只允许以数字和字母开头。解决方法如下:
如果项目使用的外置Tomcat,需要更改Tomcat的配置文件,步骤:
1. Edit the Tomcat/conf/context.xml
2. Add the statement in betweeen the <context> and</context> tags:
<CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor"/>
3. Restart Tomcat.
1234
当项目为SpringBoot的时候,由于使用的内嵌Tomcat,需要更改代码:
@Configuration
public class CookieConfig {
/**
* 解决问题:
* An invalid domain [.localhost.com]was specified for this cookie
*/
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory>cookieProcessorCustomizer() {
return(factory) -> factory.addContextCustomizers(
(context)-> context.setCookieProcessor(new LegacyCookieProcessor()));
}
}
1234567891011121314