共计 3401 个字符,预计需要花费 9 分钟才能阅读完成。
公众号:MarkerHub,网站:https://markerhub.com
小 Hub 领读:
作者聊了两种剔除形式:1、比拟 jwt 创立工夫戳,2、应用队列保障 token 惟一,但须要用锁保障线程平安。你们个别用哪种?
作者:殷地理
https://www.jianshu.com/p/b6f…
通常零碎都会限度同一个账号的登录人数,多人登录要么限度后者登录,要么踢出前者,Spring Security 提供了这样的性能,本文解说一下在没有应用 Security 的时候如何手动实现这个性能
本文借鉴了 https://jinnianshilongnian.iteye.com/blog/2039760, 如果你是应用 Shiro + Session 的模式,举荐浏览此文
demo 技术选型
- SpringBoot
- JWT
- Filter
- Redis + Redisson
JWT(token)存储在 Redis 中,相似 JSessionId-Session 的关系,用户登录后每次申请在 Header 中携带 jwt
如果你是应用 session 的话,也齐全能够借鉴本文的思路,只是代码上须要加些改变
两种实现思路
比拟工夫戳
保护一个 username: jwtToken
这样的一个 key-value
在 Reids 中, Filter 逻辑如下
public class CompareKickOutFilter extends KickOutFilter {
@Autowired
private UserService userService;
@Override
public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) {String token = request.getHeader("Authorization");
String username = JWTUtil.getUsername(token);
String userKey = PREFIX + username;
RBucket<String> bucket = redissonClient.getBucket(userKey);
String redisToken = bucket.get();
if (token.equals(redisToken)) {return true;} else if (StringUtils.isBlank(redisToken)) {bucket.set(token);
} else {Long redisTokenUnixTime = JWTUtil.getClaim(redisToken, "createTime").asLong();
Long tokenUnixTime = JWTUtil.getClaim(token, "createTime").asLong();
if (tokenUnixTime.compareTo(redisTokenUnixTime) > 0) {bucket.set(token);
} else {userService.logout(token);
sendJsonResponse(response, 4001, "您的账号已在其余设施登录");
return false;
}
}
return true;
}
}
队列踢出
public class QueueKickOutFilter extends KickOutFilter {
private boolean kickoutAfter = false;
private int maxSession = 1;
public void setKickoutAfter(boolean kickoutAfter) {this.kickoutAfter = kickoutAfter;}
public void setMaxSession(int maxSession) {this.maxSession = maxSession;}
@Override
public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) throws Exception {String token = request.getHeader("Authorization");
UserBO currentSession = CurrentUser.get();
Assert.notNull(currentSession, "currentSession cannot null");
String username = currentSession.getUsername();
String userKey = PREFIX + "deque_" + username;
String lockKey = PREFIX_LOCK + username;
RLock lock = redissonClient.getLock(lockKey);
lock.lock(2, TimeUnit.SECONDS);
try {RDeque<String> deque = redissonClient.getDeque(userKey);
if (!deque.contains(token) && currentSession.isKickout() == false) {deque.push(token);
}
while (deque.size() > maxSession) {
String kickoutSessionId;
if (kickoutAfter) {kickoutSessionId = deque.removeFirst();
} else {kickoutSessionId = deque.removeLast();
}
try {RBucket<UserBO> bucket = redissonClient.getBucket(kickoutSessionId);
UserBO kickoutSession = bucket.get();
if (kickoutSession != null) {kickoutSession.setKickout(true);
bucket.set(kickoutSession);
}
} catch (Exception e) {}}
if (currentSession.isKickout()) {
try {userService.logout(token);
sendJsonResponse(response, 4001, "您的账号已在其余设施登录");
} catch (Exception e) { }
return false;
}
} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();
LOGGER.info(Thread.currentThread().getName() + "unlock");
} else {LOGGER.info(Thread.currentThread().getName() + "already automatically release lock");
}
}
return true;
}
}
比拟两种办法
- 第一种办法逻辑简略粗犷, 只保护一个 key-value 不须要应用锁,非要说毛病的话没有第二种办法灵便。
- 第二种办法我很喜爱,代码很优雅灵便,然而逻辑绝对麻烦一些,而且为了保障线程平安地操作队列,要应用分布式锁。目前咱们我的项目中应用的是第一种办法
演示
下载地址: https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/login-control
- 运行我的项目,拜访 localhost:8887 demo 中没有存储用户信息,随便输出用户名明码,用户名雷同则被踢出
- 拜访 localhost:8887/index.html 弹出用户信息, 代表以后用户无效
- 另一个浏览器登录雷同用户名,回到第一个浏览器刷新页面,提醒被踢出
- application.properties 中抉择开启哪种过滤器模式,默认是比拟工夫戳踢出,开启队列踢出
queue-filter.enabled=true
(完)
举荐浏览
太赞了,这个 Java 网站,什么我的项目都有!https://markerhub.com
这个 B 站的 UP 主,讲的 java 真不错!
太赞了!最新版 Java 编程思维能够在线看了!
正文完