实现简单的同域名下的单点登录,适合分布式系统下使用,因为在分布式系统中,用户的请求可能被不同的服务器处理,而不同的服务器自动生成的cookie–JSESSIONID是不一致的,不能通过这个cookie去定位用户的登录状态,因此需要一个自定义的cookie注入到用户浏览器,不管用户的请求被哪个服务器处理,均能通过该cookie定位用户的信息是否存在缓存中;用户首次访问网页时,需要验证登录,用户登录成功后,服务器会生成自定义的cookie注入用户浏览器,并将用户信息写入Redis缓存中,具有一定的有效期;用户再次访问时,通过cookie中存储的SessionID去Redis中查找用户信息,若用户信息存在,可直接访问网页,不需要再次登录。
原理分析时序图如下
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.0</version>
</dependency>
public class RedisPool {
//static --> 保证jedispool在Tomcat启动时加载
private static JedisPool pool; //jedis连接池
private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total", "20")); //控制连接池与Redis Server最大的连接数
private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle", "10")); //连接池中最大的空闲的jedis实例,想用的时候可以立刻使用
private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle", "2")); //连接池中最小的空闲的jedis实例,想用的时候可以立刻使用
private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow", "true")); //在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值为true,则得到的jedis实例肯定是可以用的。
private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return", "true")); //在return一个jedis实例的时候,是否要进行验证操作,如果赋值为true,则放回jedispool的jedis实例肯定是可以用的。
private static String redisIp = PropertiesUtil.getProperty("redis.ip");//ip地址
private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"));//端口
private static void initPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
config.setBlockWhenExhausted(true); //连接耗尽时是否阻塞,false会抛出异常,true阻塞直到超时。默认值为true
pool = new JedisPool(config, redisIp, redisPort, 1000*2);
}
// JVM 加载的时候就初始化jedispool
static {
initPool();
}
//从连接池获取一个jedis
public static Jedis getJedis() {
return pool.getResource();
}
//放回一个坏的jedis连接
public static void returnBrokenResource(Jedis jedis) {
pool.returnBrokenResource(jedis);
}
//将一个jedis放回连接池
public static void returnResource(Jedis jedis) {
pool.returnResource(jedis);
}
//测试
public static void main(String[] args) {
Jedis jedis = pool.getResource();
jedis.set("dazoukey", "dazouvalue");
returnResource(jedis);
pool.destroy();//临时调用,销毁连接池中的所有连接池;
System.out.println("program is end");
}
}
public class RedisPoolUtil {
//设置key的有效期,单位是秒
//封装expire
public static Long expire(String key, int exTime) {
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();//从jedispool中获取一个jedis连接
result = jedis.expire(key, exTime);
} catch (Exception e) {
log.error("expire key:{} error", key, e);
RedisPool.returnBrokenResource(jedis);//获取jedis连接出错时,放回一个坏的连接
return result;
}
RedisPool.returnResource(jedis);//获取jedis成功并使用完毕后,放回一个好的连接
return result;
}
//exTime的单位是秒
//封装setex
public static String setEx(String key, int exTime, String value) {
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.setex(key, exTime, value);
} catch (Exception e) {
log.error("setex key:{} value:{} error", key, value, e);
RedisPool.returnBrokenResource(jedis);
return result;
}
RedisPool.returnResource(jedis);
return result;
}
//封装set
public static String set(String key, String value) {
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.set(key, value);
} catch (Exception e) {
log.error("set key:{} value:{} error", key, value, e);
RedisPool.returnBrokenResource(jedis);
return result;
}
RedisPool.returnResource(jedis);
return result;
}
//封装get
public static String get(String key) {
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.get(key);
} catch (Exception e) {
log.error("set key:{} error", key, e);
RedisPool.returnBrokenResource(jedis);
return result;
}
RedisPool.returnResource(jedis);
return result;
}
//封装del
public static Long del(String key) {
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.del(key);
} catch (Exception e) {
log.error("del key:{} error",key, e);
RedisPool.returnBrokenResource(jedis);
return result;
}
RedisPool.returnResource(jedis);
return result;
}
//测试
public static void main(String[] args) {
Jedis jedis = RedisPool.getJedis();
RedisPoolUtil.set("keyTest", "value");
String value = RedisPoolUtil.get("keyex");
RedisPoolUtil.setEx("keyex", 60*10, "valueex");
RedisPoolUtil.expire("keyTest", 60*20);
RedisPoolUtil.del("keyTest");
System.out.println("end") ;
}
}
public class CookieUtil {
private final static String COOKIE_DOMAIN = ".happymmall.com";//cookie域名
private final static String COOKIE_NAME = "mmall_login_token";//cookie名
//从HttpServletRequest中读取名为mmall_login_token的cookie
public static String readLoginToken(HttpServletRequest request){
Cookie[] cks = request.getCookies();//获取cookie数组
if(cks != null){
for(Cookie ck : cks){
log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
log.info("return cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
return ck.getValue();
}
}
}
return null;
}
//读取cookie的域名规则
//X:domain=".happymmall.com"
//a:A.happymmall.com cookie:domain=A.happymmall.com;path="/"
//b:B.happymmall.com cookie:domain=B.happymmall.com;path="/"
//c:A.happymmall.com/test/cc cookie:domain=A.happymmall.com;path="/test/cc"
//d:A.happymmall.com/test/dd cookie:domain=A.happymmall.com;path="/test/dd"
//e:A.happymmall.com/test cookie:domain=A.happymmall.com;path="/test"
// c,d能读取e的cookie,但是c,d之间的cookie不能相互读取,a,b也不能互相读取,但是都可以读取X的
//往HttpServletResponse中写入cookie,名字为mmall_login_token,域名为.happymmall.com,value值为token--采用SessionID
public static void writeLoginToken(HttpServletResponse response,String token){
Cookie ck = new Cookie(COOKIE_NAME,token);
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");//代表设置在根目录
ck.setHttpOnly(true);
//单位是秒。
//如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存。只在当前页面有效。
ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久
log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
response.addCookie(ck);
}
//删除名为mmall_login_token的cookie
public static void delLoginToken(HttpServletRequest request,HttpServletResponse response){
Cookie[] cks = request.getCookies();
if(cks != null){
for(Cookie ck : cks){
if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");
ck.setMaxAge(0);//设置成0,代表删除此cookie。
log.info("del cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
response.addCookie(ck);
return;
}
}
}
}
}
public ServiceResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse) {
ServiceResponse<User> response = iUserService.login(username, password);
//登录成功后,在session中加入该用户
if (response.isSuccess()) {
//在浏览器中写入cookie,键值为mmall_login_token,value值为sessionId
CookieUtil.writeLoginToken(httpServletResponse, session.getId());
//在Redis中写入用户信息,并将User对象序列化为字符串形式写入Redis,获取用户时再反序列化成User对象:其中键值为sessionID,value值为User对象序列化结果
RedisPoolUtil.setEx(session.getId(), Const.RedisCacheExtime.REDIS_SESSION_EXTIME, JsonUtil.obj2String(response.getData()));
}
return response;
}
public ServiceResponse<User> getUserInfo(HttpServletRequest httpServletRequest) {
//从httpServletRequest中获取Token Cookie值,根据名为:mmall_login_token的键值获取对应Cookie的value值,获得SessionId
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
//查看浏览器中是否有cookie,没有的话就是用户还没有登录
if (StringUtils.isEmpty(loginToken)) {
return ServiceResponse.createByErrorMessage("用户未登录,无法获取当前用户信息");
}
//根据读取的SessionId从redis中获取cookie对应的值,即用户信息
String userJsonStr = RedisPoolUtil.get(loginToken);
//通过反序列化将Redis中获取的用户信息字符串转化为User对象
User user = JsonUtil.string2Obj(userJsonStr, User.class);
if (user != null) {//用户信息存在,说明该用户已经登录了
return ServiceResponse.createBySuccess(user);
}
return ServiceResponse.createByErrorMessage("用户未登录,无法获取当前用户信息");
}
public ServiceResponse<String> logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
//删除session
//从httpServletRequest中获取Token Cookie值,根据名为:mmall_login_token的键值获取对应Cookie
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
//删除浏览器中的cookie
CookieUtil.delLoginToken(httpServletRequest, httpServletResponse);
//在Redis中删除退出用户信息
RedisPoolUtil.del(loginToken);
return ServiceResponse.createBySuccess();
}
当我们在登录状态的时候,如果我们再访问同一个域名下的网页时,需要将Session有效期重置;要不然我们需要一个长时间登录状态访问时,当Session有效期到了,就又需要重新登录,影响用户体验。
public class SessionExpireFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//强制类型转换为HttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
//判断logintoken是否为空或者“”;如果不为空的话,符合条件,继续拿User信息
if (StringUtils.isNotEmpty(loginToken)) {
String userJsonStr = RedisPoolUtil.get(loginToken);
User user = JsonUtil.string2Obj(userJsonStr, User.class);
//判断User是否为空,如果user不为空,则重置session的时间,即expire命令
if (user != null) {
RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
在web.xml中添加filter拦截器
<!-- 使用自定义的session共享方法,如果用户重新使用URL,就重置expire时间-->
<filter>
<filter-name>sessionExpireFilter</filter-name>
<filter-class>com.mmall.controller.common.SessionExpireFilter</filter-class>
</filter>
<!-- 拦截后缀为.do的请求 -->
<filter-mapping>
<filter-name>sessionExpireFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>