多端登录功能实现
This commit is contained in:
@@ -6,7 +6,15 @@ package com.bao.dating.context;
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
/**
|
||||
* 当前线程的用户ID
|
||||
*/
|
||||
|
||||
private static final ThreadLocal<Long> USER_HOLDER = new ThreadLocal<>();
|
||||
/**
|
||||
* 当前线程的设备ID
|
||||
*/
|
||||
private static final ThreadLocal<String> DEVICE_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前线程的用户ID
|
||||
@@ -24,12 +32,27 @@ public class UserContext {
|
||||
return USER_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前线程的设备ID
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
public static void setDeviceId(String deviceId) {
|
||||
DEVICE_HOLDER.set(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前线程的设备ID
|
||||
* @return 当前设备ID,如果未设置则返回null
|
||||
*/
|
||||
public static String getDeviceId() {
|
||||
return DEVICE_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前线程的用户ID和token
|
||||
*/
|
||||
public static void clear() {
|
||||
USER_HOLDER.remove();
|
||||
DEVICE_HOLDER.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ public class UserController {
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(HttpServletRequest request) {
|
||||
String token = request.getHeader("token");
|
||||
userService.logout(token);
|
||||
String deviceId = request.getHeader("deviceId");
|
||||
userService.logout(token, deviceId);
|
||||
return Result.success(ResultCode.SUCCESS,"退出登录成功",null);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import com.bao.dating.context.UserContext;
|
||||
import com.bao.dating.util.JwtUtil;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* HttpToken拦截器类
|
||||
* 用于拦截请求并验证JWT token的有效性,同时从token中解析用户信息
|
||||
@@ -25,6 +28,7 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
|
||||
/**
|
||||
* 在请求处理之前进行拦截
|
||||
* 从请求头或URL参数中获取token,验证其有效性,并将用户ID保存到ThreadLocal中
|
||||
@@ -42,28 +46,35 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
//当前拦截到的不是动态方法,直接放行
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从 header 获取 token
|
||||
String token = request.getHeader("token");
|
||||
if (StringUtils.isBlank(token)) {
|
||||
write401(response, "未登录,请先登录");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取 deviceId(多设备关键)
|
||||
String deviceId = request.getHeader("deviceId");
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
write401(response, "设备标识缺失");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
log.info("jwt校验: {}", token);
|
||||
log.info("HTTP鉴权 token={}, deviceId={}", token, deviceId);
|
||||
|
||||
// 验证 token 是否有效(包括是否过期)
|
||||
if (!JwtUtil.validateToken(token)) {
|
||||
log.error("Token无效或已过期");
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("Token无效或已过期");
|
||||
write401(response, "Token无效或已过期");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查 token 是否在黑名单中
|
||||
Object blacklistToken = redisTemplate.opsForValue().get("jwt:blacklist:" + token);
|
||||
if (blacklistToken != null) {
|
||||
log.error("Token已在黑名单中");
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("登录已失效, 请重新登录");
|
||||
write401(response, "登录已失效,请重新登录");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -74,7 +85,6 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
String banKey = "user:ban:" + userId;
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(banKey))) {
|
||||
String reason = String.valueOf(redisTemplate.opsForValue().get(banKey));
|
||||
log.error("用户 {} 已被封禁,原因:{}", userId, reason);
|
||||
|
||||
response.setStatus(403);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
@@ -82,24 +92,19 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从Redis获取存储的token进行比对
|
||||
Object redisTokenObj = redisTemplate.opsForValue().get("login:token:" + userId);
|
||||
String redisToken = redisTokenObj != null ? redisTokenObj.toString() : null;
|
||||
// 多设备 token 校验
|
||||
String redisTokenKey = "login:token:" + userId + ":" + deviceId;
|
||||
Object redisTokenObj = redisTemplate.opsForValue().get(redisTokenKey);
|
||||
|
||||
// 验证Redis中的token是否存在且匹配
|
||||
if (redisToken == null || !redisToken.equals(token)) {
|
||||
log.error("登录已失效");
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("登录已失效");
|
||||
if (redisTokenObj == null || !token.equals(redisTokenObj.toString())) {
|
||||
write401(response, "登录状态已失效");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("用户: {}", userId);
|
||||
// 保存 userId 到 ThreadLocal
|
||||
// 保存 登录信息 到 ThreadLocal
|
||||
UserContext.setUserId(userId);
|
||||
// 保存 token 到 ThreadLocal
|
||||
UserContext.setToken(token);
|
||||
UserContext.setDeviceId(deviceId);
|
||||
log.info("token验证成功 userId={}, deviceId={}", userId, deviceId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Token 校验失败: {}", e.getMessage());
|
||||
@@ -122,4 +127,16 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应错误信息
|
||||
* @param response
|
||||
* @param msg
|
||||
* @throws IOException
|
||||
*/
|
||||
private void write401(HttpServletResponse response, String msg) throws IOException {
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,9 +42,10 @@ public class WsAuthInterceptor implements HandshakeInterceptor {
|
||||
|
||||
// 从URL参数中获取token
|
||||
String token = servletRequest.getParameter("token");
|
||||
|
||||
if (StringUtils.isBlank(token)) {
|
||||
log.error("WebSocket握手失败:令牌丢失");
|
||||
String deviceId = servletRequest.getParameter("deviceId");
|
||||
|
||||
if (StringUtils.isBlank(token) || StringUtils.isBlank(deviceId)) {
|
||||
log.error("WebSocket认证失败:token或deviceId缺失");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -81,21 +82,19 @@ public class WsAuthInterceptor implements HandshakeInterceptor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从Redis获取存储的token进行比对
|
||||
String redisTokenKey = "login:token:" + userId;
|
||||
// 多设备 token 校验
|
||||
String redisTokenKey = "login:token:" + userId + ":" + deviceId;
|
||||
Object redisTokenObj = redisTemplate.opsForValue().get(redisTokenKey);
|
||||
String redisToken = redisTokenObj != null ? redisTokenObj.toString() : null;
|
||||
log.info("Redis中存储的token: {}", redisToken != null ? "存在" : "不存在");
|
||||
|
||||
// 验证Redis中的token是否存在且匹配
|
||||
if (redisToken == null || !redisToken.equals(token)) {
|
||||
log.error("登录已失效 - Redis中token不存在或不匹配");
|
||||
if (redisTokenObj == null || !token.equals(redisTokenObj.toString())) {
|
||||
log.error("登录已失效");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("WebSocket认证成功,用户ID: {}", userId);
|
||||
// 将用户ID保存到attributes中
|
||||
// 将信息保存到attributes中
|
||||
attributes.put("userId", userId);
|
||||
attributes.put("deviceId", deviceId);
|
||||
log.info("WebSocket认证成功 userId={}, deviceId={}", userId, deviceId);
|
||||
return true;
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
|
||||
@@ -12,4 +12,10 @@ import java.io.Serializable;
|
||||
public class UserLoginDTO implements Serializable {
|
||||
private String username;
|
||||
private String password;
|
||||
/** 设备唯一标识(前端生成)*/
|
||||
private String deviceId;
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
/** 设备名称 */
|
||||
private String deviceName;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,5 @@ public class UserLoginVO implements Serializable {
|
||||
private Long userId;
|
||||
private String nickname;
|
||||
private String token;
|
||||
private String deviceId;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public interface UserService {
|
||||
* @param token 登录凭证
|
||||
* @return 注册结果
|
||||
*/
|
||||
void logout(String token);
|
||||
void logout(String token, String deviceId);
|
||||
|
||||
/**
|
||||
* 查询个人信息
|
||||
|
||||
@@ -82,6 +82,9 @@ public class UserServiceImpl implements UserService {
|
||||
if (userLoginDTO == null || userLoginDTO.getUsername() == null || userLoginDTO.getPassword() == null) {
|
||||
throw new RuntimeException("用户名或密码不能为空");
|
||||
}
|
||||
if (userLoginDTO.getDeviceId() == null || userLoginDTO.getDeviceName() == null || userLoginDTO.getDeviceType() == null){
|
||||
throw new RuntimeException("未获取到设备");
|
||||
}
|
||||
// 查询用户
|
||||
User user = userMapper.getByUsername(userLoginDTO.getUsername());
|
||||
if (user == null) {
|
||||
@@ -101,19 +104,39 @@ public class UserServiceImpl implements UserService {
|
||||
// 生成token
|
||||
String token = JwtUtil.generateToken(String.valueOf(user.getUserId()));
|
||||
|
||||
String redisKey = "login:token:" + user.getUserId();
|
||||
Long userId = user.getUserId();
|
||||
String deviceId = userLoginDTO.getDeviceId();
|
||||
|
||||
String tokenKey = "login:token:" + userId+ ":" + deviceId;
|
||||
redisTemplate.opsForValue().set(
|
||||
redisKey,
|
||||
tokenKey,
|
||||
token,
|
||||
7,
|
||||
TimeUnit.DAYS
|
||||
);
|
||||
|
||||
// 设备信息 Hash
|
||||
String deviceKey = "user:device:" + userId+ ":" + deviceId;
|
||||
Map<String, Object> deviceInfo = new HashMap<>();
|
||||
deviceInfo.put("token", token);
|
||||
deviceInfo.put("deviceType", userLoginDTO.getDeviceType());
|
||||
deviceInfo.put("deviceName", userLoginDTO.getDeviceName());
|
||||
deviceInfo.put("loginTime", System.currentTimeMillis());
|
||||
|
||||
// 存储设备信息
|
||||
redisTemplate.opsForHash().putAll(deviceKey, deviceInfo);
|
||||
redisTemplate.expire(deviceKey, 7, TimeUnit.DAYS);
|
||||
|
||||
// 缓存用户设备信息
|
||||
String deviceSetKey = "user:devices:" + userId;
|
||||
redisTemplate.opsForSet().add(deviceSetKey, deviceId);
|
||||
|
||||
// 封装返回
|
||||
UserLoginVO userLoginVO = new UserLoginVO();
|
||||
userLoginVO.setUserId(user.getUserId());
|
||||
userLoginVO.setUserId(userId);
|
||||
userLoginVO.setNickname(user.getNickname());
|
||||
userLoginVO.setToken(token);
|
||||
userLoginVO.setDeviceId(deviceId);
|
||||
return userLoginVO;
|
||||
}
|
||||
|
||||
@@ -122,10 +145,10 @@ public class UserServiceImpl implements UserService {
|
||||
* @param token 登录凭证
|
||||
*/
|
||||
@Override
|
||||
public void logout(String token) {
|
||||
public void logout(String token, String deviceId) {
|
||||
Claims claims = JwtUtil.getClaimsFromToken(token);
|
||||
// 获取token信息
|
||||
String subject = claims.getSubject();
|
||||
String userId = claims.getSubject();
|
||||
// 获取token的过期时间
|
||||
Date expiration = claims.getExpiration();
|
||||
// 判断 token 是否已过期
|
||||
@@ -135,10 +158,15 @@ public class UserServiceImpl implements UserService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从Redis中删除登录token记录
|
||||
String loginTokenKey = "login:token:" + subject;
|
||||
// 从Redis中删除当前设备登录的token记录
|
||||
String loginTokenKey = "login:token:" + userId + ":" + deviceId;
|
||||
redisTemplate.delete(loginTokenKey);
|
||||
|
||||
// 删除设备信息
|
||||
String deviceKey = "user:device:" + userId + ":" + deviceId;
|
||||
redisTemplate.delete(deviceKey);
|
||||
|
||||
// 将token加入黑名单
|
||||
String logoutKey = "jwt:blacklist:" + token;
|
||||
redisTemplate.opsForValue().set(
|
||||
logoutKey,
|
||||
@@ -499,6 +527,11 @@ public class UserServiceImpl implements UserService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否在线
|
||||
* @param userId 用户ID
|
||||
* @return true: 在线,false: 离线
|
||||
*/
|
||||
@Override
|
||||
public boolean isUserOnline(Long userId) {
|
||||
if (userId == null) {
|
||||
|
||||
Reference in New Issue
Block a user