先说下为什么要实现session共享,因为在普通的web项目中是不需要考虑这点的,因为都在一个项目当中,但是当分布式或者集群的时候,相同的项目部署在不同的服务器上,这时候就需要考虑这个问题,因为假如一个用户,在tomcat1登录成功,这时候他刷新了一个页面,然后负载均衡服务器nginx把他分配到了tomcat2上的项目中,这个项目中是没有刚才登录成功的session的,但是人家明明登录成功了,这给用户体验极差,所以得考虑session共享问题 .
解决方案一 tomcat可以配置session共享,配置好后,加入用户在tomcat1登录,这时1的tomcat就会发出一个广播,在toncat集群中,广播自己的session信息,其它的tomcat就会接收信息,放到自己的session当中,这时就可以解决session共享的问题,但是如果配置了session共享的话,tomcat节点就会有上限,当tomcat集群节点加到一定层度时,会形成内网的网络风暴,tomcat会把内网的宽带都占用掉,服务器的性能就会降低.这该怎么办呢?
方案二 看下图,可以专门建立一个session服务器,专门存储session的不管从哪台服务器登录都保存到这台服务器上,登录判断也在这台服务器判断.这时tomcat就不存在上限的问题也解决了session共享的问题.但是在这台服务器上不能使用tomcat的session了,可以在这台服务器模拟一个session,session都是key,value形式的,并且有过期时间,正好redis也是key,value形式,也可以设置过期时间,所以可以拿redis来做.
实现过程,在项目中新建一个处理登录的模块
service处理逻辑,就是从数据库查询用户名密码是否正确,正确就存入到redis当中去.然后把token返回回去,E3Result.ok(token)这个东西就是一个返回的自己封装的类.
package cn.e3mall.sso.service.impl;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import cn.e3mall.common.jedis.JedisClient;
import cn.e3mall.common.utils.E3Result;
import cn.e3mall.common.utils.JsonUtils;
import cn.e3mall.mapper.TbUserMapper;
import cn.e3mall.pojo.TbUser;
import cn.e3mall.pojo.TbUserExample;
import cn.e3mall.pojo.TbUserExample.Criteria;
import cn.e3mall.sso.service.LoginService;
/**
* 用户登录处理
* <p>Title: LoginServiceImpl</p>
* <p>Description: </p>
* <p>Company: www.itcast.cn</p>
* @version 1.0
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private TbUserMapper userMapper;
@Autowired
private JedisClient jedisClient;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
@Override
public E3Result userLogin(String username, String password) {
// 1、判断用户和密码是否正确
//根据用户名查询用户信息
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
//执行查询
List<TbUser> list = userMapper.selectByExample(example);
if (list == null || list.size() == 0) {
//返回登录失败
return E3Result.build(400, "用户名或密码错误");
}
//取用户信息
TbUser user = list.get(0);
//判断密码是否正确
if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) {
// 2、如果不正确,返回登录失败
return E3Result.build(400, "用户名或密码错误");
}
// 3、如果正确生成token。这个token就是个自定义令牌,其个以后标识的作用
String token = UUID.randomUUID().toString();
// 4、把用户信息写入redis,key:token value:用户信息
user.setPassword(null);
//存的时候加个前缀SESSION: 这种以冒号形式开头的就会在工具中形成文件夹的进行方便查找
jedisClient.set("SESSION:" + token, JsonUtils.objectToJson(user));
// 5、设置Session的过期时间
jedisClient.expire("SESSION:" + token, SESSION_EXPIRE);
// 6、把token返回
return E3Result.ok(token);
}
}
存的时候加个前缀SESSION: 这种以冒号形式开头的就会在工具中形成文件夹的进行方便查找
#session的过期时间
SESSION_EXPIRE=1800
controller层,把token存入到cookie中
package cn.e3mall.sso.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import cn.e3mall.common.utils.CookieUtils;
import cn.e3mall.common.utils.E3Result;
import cn.e3mall.sso.service.LoginService;
/**
* 用户登录处理
* <p>Title: LoginController</p>
* <p>Description: </p>
* <p>Company: www.itcast.cn</p>
* @version 1.0
*/
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@Value("${TOKEN_KEY}")
private String TOKEN_KEY;
@RequestMapping("/page/login")
public String showLogin() {
return "login";
}
@RequestMapping(value="/user/login", method=RequestMethod.POST)
@ResponseBody
public E3Result login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
E3Result e3Result = loginService.userLogin(username, password);
//判断是否登录成功
if(e3Result.getStatus() == 200) {
String token = e3Result.getData().toString();
//如果登录成功需要把token写入cookie
CookieUtils.setCookie(request, response, TOKEN_KEY, token);
}
//返回结果
return e3Result;
}
}
cookie工具类,这里面有解决cookie跨越的问题,其实cookie有域的限制的,一个cookie只能保存在相同的域名下,一个项目中拆分成许多小模块就会遇到这种问题,它这个解决方法,只要保证后面一部分尾缀相同就可以跨域名.比如尾缀是jd.cn都是以这个结尾就可以跨域名,前面不同无所谓.
package cn.e3mall.common.utils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*
* Cookie 工具类
*
*/
public final class CookieUtils {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
上面部分存就写完了接下来写取的功能,在一个电商项目中页面很多,每个页面头部都有请登录,登录成功后就得显示是哪个用户登录的,这该怎么解决呢页面那么多总不能每个页面都访问一下controller获取吧,关键每个页面在不同的项目中,这时候就得单独写一个服务,然后每个页面引入相同的js都调用这个服务就可以了,后台代码就不用动,只在前台代码加入js就可以了.
service层
package cn.e3mall.sso.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import cn.e3mall.common.jedis.JedisClient;
import cn.e3mall.common.utils.E3Result;
import cn.e3mall.common.utils.JsonUtils;
import cn.e3mall.pojo.TbUser;
import cn.e3mall.sso.service.TokenService;
/**
* 根据token取用户信息
* <p>Title: TokenServiceImpl</p>
* <p>Description: </p>
* <p>Company: www.itcast.cn</p>
* @version 1.0
*/
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private JedisClient jedisClient;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
@Override
public E3Result getUserByToken(String token) {
//根据token到redis中取用户信息
String json = jedisClient.get("SESSION:" + token);
//取不到用户信息,登录已经过期,返回登录过期
if (StringUtils.isBlank(json)) {
return E3Result.build(201, "用户登录已经过期");
}
//取到用户信息更新token的过期时间
jedisClient.expire("SESSION:" + token, SESSION_EXPIRE);
//返回结果,E3Result其中包含TbUser对象
TbUser user = JsonUtils.jsonToPojo(json, TbUser.class);
return E3Result.ok(user);
}
}
json工具类
package cn.e3mall.common.utils;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 淘淘商城自定义响应结构
*/
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* <p>Title: pojoToJson</p>
* <p>Description: </p>
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param clazz 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
* @param jsonData
* @param beanType
* @return
*/
public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
controller,这里面代码看不懂就先看下面的jsonp原理,这里面有两种实现方法,第一种适用任何版本,第二种适用spring4.1以上版本,第一种里面就是自己手动加了个response的响应类型,改成json形式
在第一种方法里设置响应信息,下面这三种都可以,一个是常量一个是自己写的字符串,这个常量也就是下面的字符串两个一个意思,还有一种就是不在注解参数设置,适用response对象设置响应类型.
produces=MediaType.APPLICATION_JSON_UTF8_VALUE
produces="application/json;charset=utf-8"
response.setContentType("application/json;charset=utf-8");
在第二种方法里可以使用spring的对象MappingJacksonValue,这个对象里就封装的jsonp的响应,参数放返回对象,然后再设置一个返回方法名.
package cn.e3mall.sso.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import cn.e3mall.common.utils.E3Result;
import cn.e3mall.common.utils.JsonUtils;
import cn.e3mall.sso.service.TokenService;
/**
* 根据token查询用户信息Controller
* <p>Title: TokenController</p>
* <p>Description: </p>
* <p>Company: www.itcast.cn</p>
* @version 1.0
*/
@Controller
public class TokenController {
@Autowired
private TokenService tokenService;
/*@RequestMapping(value="/user/token/{token}",
produces=MediaType.APPLICATION_JSON_UTF8_VALUE"application/json;charset=utf-8")
@ResponseBody
public String getUserByToken(@PathVariable String token, String callback) {
E3Result result = tokenService.getUserByToken(token);
//响应结果之前,判断是否为jsonp请求
if (StringUtils.isNotBlank(callback)) {
//把结果封装成一个js语句响应
return callback + "(" + JsonUtils.objectToJson(result) + ");";
}
return JsonUtils.objectToJson(result);
}*/
//4.1版本以后可以使用第二种方法
@RequestMapping(value="/user/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token, String callback) {
E3Result result = tokenService.getUserByToken(token);
//响应结果之前,判断是否为jsonp请求
if (StringUtils.isNotBlank(callback)) {
//把结果封装成一个js语句响应
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
}
}
在页面需要导这两个js,第一个是自己写的,第二个是jquery下面的,自己下载去吧
下载地址https://download.csdn.net/download/kxj19980524/10914570
这里面的逻辑就是页面加载从cookie取出token然后访问controller返回用户信息,然后把页面的内容替换掉,主要是看这个ajax的数据类型是jsonp,它是解决跨越问题的.
var E3MALL = {
checkLogin : function(){
var _ticket = $.cookie("token");
if(!_ticket){
return ;
}
$.ajax({
url : "http://localhost:8088/user/token/" + _ticket,
dataType : "jsonp",
type : "GET",
success : function(data){
if(data.status == 200){
var username = data.data.username;
var html = username + ",欢迎来到宜立方购物网!<a href=\"http://www.e3mall.cn/user/logout.html\" class=\"link-logout\">[退出]</a>";
$("#loginbar").html(html);
}
}
});
}
}
$(function(){
// 查看是否已经登录,如果已经登录查询登录信息
E3MALL.checkLogin();
});