1、介绍
1.1、session
-
在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。
-
服务器是如何实现一个session为一个用户浏览器服务的?
服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。
注:上文描述,参考自文末的参考链接中第1条链接。
1.2、延迟队列
- 1、
DelayQueue
队列中的元素必须是Delayed接口的实现类,该类内部实现了getDelay()
和compareTo()
方法,第一个方法是比较两个任务的延迟时间进行排序,第二个方法用来获取延迟时间。 - 2、
DelayQueue
队列没有大小限制,因此向队列插数据不会阻塞 - 3、
DelayQueue
中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。否则线程阻塞。 - 4、
DelayQueue
中的元素不能为null
- 5、
DelayQueue
内部是使用PriorityQueue
实现的。compareTo()
比较后越小的越先取出来。
注:上文描述,参考自文末的参考链接中第2条链接。
2、前后端分离方案
- 前端为
html
,利用ajax
(后期改为axios
)来请求json
交互,restful
风格 - 后端以
springboot
为基础框架,接口暴露为适应跨域要求,利用在控制层添加@CrossOrigin
注解实现 - 功能抽象:登陆、登陆后页面查看(需要鉴权)
3、存在的问题
- 因为跨域问题,
JSESSIONID
每次请求都会变化,导致后端无法维护一个合适的session
- 故需要简便、快速、低成本的一种方法,来实现
session
的存储与维护。
4、方案
4.1、session
的特点
- 1、以唯一键key来插入和获取对象
- 2、
session
有自动过期时间,到期后系统会自动清理。 - 3、每次新的请求过来获取
session
,该key
值过期时间重置
4.2、DelayQueue
的设计
- 1、采用
concurrentHashmap
来保存session
信息 - 2、采用
DelayQueue
延迟队列来存储concurrentHashmap
中的key
- 3、模拟会话监听器,即
sessionListener
,专门开启一个守护线程(阻塞式take
)从DelayQueue
队列中获取过期的指针,再根据指针删除concurrentHashmap
中对应元素。
4.3、登陆方案的设计
- 1、
sessionId
的设计,可利用uuid
或其他规则来实现。 - 2、在后台的登陆方法中,若用户名与密码正确,则生成该
sessionId
,按照一定格式返回给前台。 - 3、前台接收到该
sessionId
后,可存储到cookie
中,将其封装到http
的header
中,后续请求均附带该header
5、实际代码
5.1、前台请求的发送
- 前台
axios
中,具体请求示例如下:
var headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Credentials': 'true',
'Authorization': $.cookie("jsessionId"),
};
axios({
headers: headers,
method: method, //GET、PUT、POST、PATCH、DELETE等
url: url,
timeout: 50000, // 请求的超时时间
data: data,
})
.then(function (response) {
//TODO 正确返回后的处理或回调
})
.catch(function (error) {
if (error.response) {
console.log(error.response);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
});
5.2、后台DelayQueue
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @Auther: jiangcaijun
* @Date: 2018/4/17 15:15
* @Description: 延迟队列,单例模式。利用ConcurrentHashMap来存储信息
*/
public class CacheSingleton<K, V> {
/*session自动过期时间,单位:秒*/
private static int liveTime = 5;
//在类内部实例化一个实例
private static CacheSingleton instance = new CacheSingleton();
//私有的构造函数,外部无法访问
private CacheSingleton(){
Thread t = new Thread(){
@Override
public void run(){
dameonCheckOverdueKey();
}
};
t.setDaemon(true);
t.start();
}
//对外提供获取实例的静态方法
public static CacheSingleton getInstance() {
return instance;
}
public ConcurrentHashMap<K, V> concurrentHashMap = new ConcurrentHashMap<K, V>();
public DelayQueue<DelayedItem<K>> delayQueue = new DelayQueue<DelayedItem<K>>();
/**
* 根据key,获取相应的值
* @param k
* @return
*/
public Object get(K k){
V v = concurrentHashMap.get(k);
DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
if (v != null) {
delayQueue.remove(tmpItem);
delayQueue.put(tmpItem);
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "获取 "+ k + "成功,生命周期重新计算:"+ liveTime +"秒"));
}else{
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "获取"+ k +"失败,对象已过期"));
}
return v;
}
/**
* 移除相应的键值对
* @param k
*/
public void remove(K k){
V v = concurrentHashMap.get(k);
DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
if (v != null) {
delayQueue.remove(tmpItem);
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "主动删除 "+ k + "成功"));
}else{
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "删除失败,该 "+ k +"已被删除"));
}
}
/**
* 插入键值对
* @param k
* @param v
*/
public void put(K k,V v){
V v2 = concurrentHashMap.put(k, v);
DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
if (v2 != null) {
delayQueue.remove(tmpItem);
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "覆盖插入 "+ k + ",生命周期重新计算:"+ liveTime +"秒"));
}else{
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "新插入 "+ k + ",生命周期初始化:"+ liveTime +"秒"));
}
delayQueue.put(tmpItem);
}
/**
* 专门开启一个守护线程(阻塞式)从 delayQueue 队列中获取过期的指针,再根据指针删除hashmap中对应元素。
*/
public void dameonCheckOverdueKey(){
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "守护进程开启"));
while (true) {
DelayedItem<K> delayedItem = null;
try {
delayedItem = delayQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (delayedItem != null) {
concurrentHashMap.remove(delayedItem.getT());
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()),"自动删除过期key: "+delayedItem.getT()));
}
try {
Thread.sleep(300);
} catch (Exception e) {
// TODO: handle exception
}
}
}
/**
* TODO
*/
public static void main(String[] args) throws InterruptedException {
/*模拟客户端调用*/
CacheSingleton.getInstance().put("1", 1);
CacheSingleton.getInstance().put("2", 2);
Thread.sleep(4000);
CacheSingleton.getInstance().get("2");
Thread.sleep(2000);
CacheSingleton.getInstance().get("2");
Thread.sleep(2000);
CacheSingleton.getInstance().get("2");
Thread.sleep(5500);
CacheSingleton.getInstance().put("1", 2);
CacheSingleton.getInstance().get("2");
Thread.sleep(5000);
System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()),"main方法结束"));
}
}
class DelayedItem<T> implements Delayed{
private T t;
private long liveTime ;
private long removeTime;
public DelayedItem(T t,long liveTime){
this.setT(t);
this.liveTime = liveTime;
this.removeTime = TimeUnit.NANOSECONDS.convert(liveTime, TimeUnit.SECONDS) + System.nanoTime();
}
@Override
public int compareTo(Delayed o) {
if (o == null) return 1;
if (o == this) return 0;
if (o instanceof DelayedItem){
DelayedItem<T> tmpDelayedItem = (DelayedItem<T>)o;
if (liveTime > tmpDelayedItem.liveTime ) {
return 1;
}else if (liveTime == tmpDelayedItem.liveTime) {
return 0;
}else {
return -1;
}
}
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return diff > 0 ? 1:diff == 0? 0:-1;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(removeTime - System.nanoTime(), unit);
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
@Override
public int hashCode(){
return t.hashCode();
}
@Override
public boolean equals(Object object){
if (object instanceof DelayedItem) {
return object.hashCode() == hashCode() ?true:false;
}
return false;
}
}
在过期时间为5秒
的情况下,模拟session
,main
方法运行,输出为:
16:56:25 新插入 1,生命周期初始化:5秒
16:56:25 守护进程开启
16:56:25 新插入 2,生命周期初始化:5秒
16:56:29 获取 2成功,生命周期重新计算:5秒
16:56:30 自动删除过期key: 1
16:56:31 获取 2成功,生命周期重新计算:5秒
16:56:33 获取 2成功,生命周期重新计算:5秒
16:56:38 自动删除过期key: 2
16:56:38 新插入 1,生命周期初始化:5秒
16:56:38 获取2失败,对象已过期
16:56:43 自动删除过期key: 1
16:56:43 main方法结束
5.3、登陆成功后页面鉴权
利用aop
的环绕aroud
,在请求过来时,查看该sessionId
是否存在该delayQueue
中,简要代码如下:
import com.bigdata.weathercollect.constant.GlobalConstant;
import com.bigdata.weathercollect.exception.UnauthorizedException;
import com.bigdata.weathercollect.service.ServiceStatus;
import com.bigdata.weathercollect.session.CacheSingleton;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Auther: jiangcaijun
* @Date: 2018/4/16 15:58
* @Description:
* @Component:注册到Spring容器,必须加入这个注解
* @Aspect // 该注解标示该类为切面类,切面是由通知和切点组成的。
*/
@Component
@Aspect
public class ExceptionAspect {
private static Logger logger = LoggerFactory.getLogger(ExceptionAspect.class);
@Autowired
private HttpServletRequest request;
/**
* 这里会报错,但不影响运行
*/
@Autowired
private HttpServletResponse response;
@Pointcut("execution(public * com.bigdata.weathercollect.controller.*.*(..))")
public void exceptionAspect() {
}
@Around("exceptionAspect()")
public Object around(ProceedingJoinPoint joinPoint){
String url = request.getRequestURI();
ServiceStatus serviceStatus = null;
Boolean flag = false;
if(url != null){
String jsessionId = request.getHeader("Authorization");
if(StringUtils.isNotBlank(jsessionId)) {
//这里进行sessionId的校验
if(CacheSingleton.getInstance().get(jsessionId) != null){
// logger.info("该用户已登陆,id:{}", jsessionId);
flag = true;
}
}
if(!flag){
logger.error("该用户未登陆");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return new ServiceStatus(ServiceStatus.Status.Fail, "尚未登陆或会话已过期",401);
}
}
try {
return joinPoint.proceed();
} catch (UnauthorizedException e) {
logger.error("出现Exception:url为" + url + ";错误类型为"+e.getMessage()+"");
serviceStatus = new ServiceStatus(ServiceStatus.Status.Fail, "认证失败:" + e.getMessage(),401);
} catch (Exception e) {
logger.error("出现Exception:url为" + url + ";错误类型为"+e.getMessage()+"");
serviceStatus = new ServiceStatus(ServiceStatus.Status.Fail, "失败:" + e.getMessage(),500);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return serviceStatus;
}
}
注:其中,ServiceStatus
为自定义的json
返回封装的类,不影响阅读,故代码未贴出来。
6、其他
参考链接: