中心思想就是在JWT令牌中生成一个随机数,并记录这个令牌的第一次生成的时间。如果之后使用这个令牌进行登录但发现当前记录的最近一次的登录令牌不是这个,就要阻止登录。
下面的checkAndDealWithExistedLogin()
方法返回true说明允许登录,false则是令牌失效。
public class LoginManager {
private static final ConcurrentHashMap<Integer, DeviceInfo> LOGIN_USER_DEVICE_MAP = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Integer, Lock> USER_LOCK_MAP = new ConcurrentHashMap<>();
public static boolean checkAndDealWithExistedLogin() {
Integer currentUserId = getCurrentUserId();
String currentUserDeviceId = getCurrentUserDeviceId();
// 为每个用户加一个锁,防止一个用户同时在多个设备登录并避免阻塞过多线程
lock(currentUserId);
try {
// 没登录过直接放行,并记录到map
if (!LOGIN_USER_DEVICE_MAP.containsKey(currentUserId)) {
LOGIN_USER_DEVICE_MAP.put(currentUserId, new DeviceInfo(currentUserDeviceId, System.currentTimeMillis()));
return true;
}
Long currentUserLoginTimestamp = getCurrentUserLoginTimestamp();
DeviceInfo deviceInfo = LOGIN_USER_DEVICE_MAP.get(currentUserId);
// 仍然是上一个设备登录,直接放行
if (deviceInfo.id.equals(currentUserDeviceId)) {
return true;
} else {
// 当前登录新于已存在登录,修改信息并放行
if (deviceInfo.loginTimestamp < currentUserLoginTimestamp) {
LOGIN_USER_DEVICE_MAP.put(currentUserId, new DeviceInfo(getCurrentUserDeviceId(), getCurrentUserLoginTimestamp()));
return true;
}
// 当前为旧登录,拒绝访问
return false;
}
} finally {
unlock(currentUserId);
}
}
private static void lock(Integer userId) {
// 使用putIfAbsent实现原子性
USER_LOCK_MAP.putIfAbsent(userId, new ReentrantLock());
USER_LOCK_MAP.get(userId).lock();
}
private static void unlock(Integer userId) {
USER_LOCK_MAP.get(userId).unlock();
}
// 下面3个方法使用了ThreadLocal,
// 就是在LoginFilter过滤器中把请求头中携带的token数据进行解析,
// 并放入其中,供本次请求对应的线程在处理整个流程中获取
// 代码见下面
public static String getCurrentUserDeviceId() {
return LoginFilter.DECODED_JWT_THREADLOCAL.get().getClaim("deviceId").asString();
}
public static Integer getCurrentUserId() {
return LoginFilter.DECODED_JWT_THREADLOCAL.get().getClaim("userId").asInt();
}
public static Long getCurrentUserLoginTimestamp() {
return LoginFilter.DECODED_JWT_THREADLOCAL.get().getClaim("loginTimestamp").asLong();
}
@Data
@AllArgsConstructor
private static class DeviceInfo {
String id;
Long loginTimestamp;
}
}
LoginFilter中的片段:
String jwt = request.getHeader(LoginFilter.TOKEN_KEY);
try {
DECODED_JWT_THREADLOCAL.set(JwtUtils.verify(jwt));
boolean pass = LoginManager.checkAndDealWithExistedLogin();
if (pass) {
// 通过就继续处理
filterChain.doFilter(request, response);
} else {
// 否则抛异常和jwt无效一同处理
throw new RuntimeException();
}
} catch (Exception e) {
response.getOutputStream().write(gson.toJson(Resp.error(RespCode.JWT_TOKEN_INVALID,
e.getMessage())).getBytes());
} finally {
// 在这里一定要移除在ThreadLocal中存储的登录令牌
// 因为不手动移除,ThreadLocal的设计可能会导致内存泄漏
DECODED_JWT_THREADLOCAL.remove();
}