最近再做一个小程序项目,在这个项目中需要有一个管理员用户在线数实时刷新的功能,一开始用的是网上广为流传的做法,即创建一个session监听器,在用户登录时即创建一个session,监听器记录下来并且把count加一,当用户点击注销时把session给remove掉,count减一。但是这个方案只适合估计一个值,而不适合做精确的在线人数判断,譬如,当用户关闭浏览器时并不会触发session监听,当下一次登录时仍然会让count加一;或者在session过期时,session监听并不能做一个实时的响应去将在线数减一,当用户在次登陆,由于cookie中含有的session_id不同而导致session监听器记录下session创建,而使count加一。
一、使用JWT做用户实时在线数判断的原理
本文不仔细地讲解什么是JWT,因为关于这个的文章已经写了很多了,但是在这里还是需要提一下什么是JWT。
JWT即Json Web token(Json网络令牌),它由一种特殊的加密方式生成一串包含令牌状态(令牌过期时间、令牌签发时间、令牌的受众等)、用户数据等的字符串。前端在每次请求接口时都需要将这串令牌传送给后端校验这次请求是否正确,数据是否被篡改,用户是否已过期等。
关于如何在SpringBoot下使用JWT的代码,都在这篇文章中:
* SpringBoot学习笔记(16)-整合JWT做身份验证_arong2048的博客-CSDN博客
实现思路:
根据时序图的这套方案,用户如果60s内没有任何操作(不调用接口去传递token)则判定该用户为下线状态,当用户重新登陆或者再次操作则判定为在线状态。这其实是心跳机制思想的一种实现,类似于Redis集群中的哨兵对Master主观下线的过程:每10s对Master发送一个心跳包,10s内没有响应则说明Master已经下线了。这里采用的是60s作为一个生存值,如果60s内该用户没有在此页面(如果在此页面,前端会间隔10s发送一次心跳包对Token进行续期+60s过期时间)上执行任何操作,也就不会携带Token发送请求到后端接口中,那么就无法给map中的token过期时间续期,所以该用户就处于过期状态。
二、具体的代码实现
- OnlineCounter
@Component
public class OnlineCounter {
//每次打开此类只初始化一次countMap
private static Map countMap = new ConcurrentHashMap<String,Object>();
/**
* @auther: Arong
* @description: 解析token并且将数据插入CountMap中
* @param token
* @return: void
* @date: 2019/1/22 17:44
*/
public void insertToken(String token){
//获得当前时间(毫秒)
long currentTime = System.currentTimeMillis();
//解析token,获得签发时间
Claims claims = null;
try {
claims = JWTUtils.parseJWT(token);
} catch (Exception e) {
throw new RuntimeException("token不存在或已过期");
}
Date issuedAt = claims.getIssuedAt();
//以签发时间为key。当前时间+60s为value存入countMap中
countMap.put(issuedAt.toString(),currentTime+60*1000);
}
/**
* @auther: Arong
* @description: 获取当前在线用户数
* @param
* @return: java.lang.Integer
* @date: 2019/1/22 17:51
*/
public Integer getOnlineCount(){
int onlineCount = 0;
//获取countMap的迭代器
Iterator iterator = countMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String,Object> entry = (Map.Entry<String, Object>) iterator.next();
Long value = (Long) entry.getValue();
if (value > System.currentTimeMillis()) {
//过期时间大于当前时间则没有过期
onlineCount++;
}
}
return onlineCount;
}
}
- AdminController
@Autowired
private OnlineCounter onlineCounter;
/**
* @auther: Arong
* @description: 获取当前实时在线人数(精确度为60s范围内)
* @param
* @return: int
* @date: 2019/1/19 17:44
*/
@GetMapping(value = "/getOnlineCount")
public int getRealOnlineCount() {
Integer onlines = onlineCounter.getOnlineCount();
return onlines;
}
- JWTInterceptor
@Autowired
private OnlineCounter onlineCounter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
//后台管理页面产生的token
String token = request.getHeader("authorization");
//判断是否过期
Optional.ofNullable(token)
.map(n -> {
try {
return JWTUtils.parseJWT(n);
} catch (Exception e) {
throw new RuntimeException("token不存在");
}
})
.map(n->{
//存储该token方便记录在线人数
try {
onlineCounter.insertToken(token);
} catch (Exception e) {
e.printStackTrace();
}
return n.getExpiration();
})
.orElseThrow(()->new RuntimeException("token已过期!请用户重新登陆!"));
return true;
}
- 获取实时在线人数
/**
* @auther: Arong
* @description: 获取当前实时在线人数(精确度为60s范围内)
* @param
* @return: int
* @date: 2019/1/19 17:44
*/
@GetMapping(value = "/getOnlineCount")
public int getRealOnlineCount() {
Integer onlines = onlineCounter.getOnlineCount();
return onlines;
}