From 08c6481c51c44e524bac48228eb3d600e373b761 Mon Sep 17 00:00:00 2001 From: KilLze Date: Tue, 20 Jan 2026 17:07:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E7=AB=AF=E7=99=BB=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/bao/dating/context/UserContext.java | 23 +++++++ .../bao/dating/controller/UserController.java | 3 +- .../dating/interceptor/TokenInterceptor.java | 63 ++++++++++++------- .../dating/interceptor/WsAuthInterceptor.java | 23 ++++--- .../com/bao/dating/pojo/dto/UserLoginDTO.java | 6 ++ .../com/bao/dating/pojo/vo/UserLoginVO.java | 1 + .../com/bao/dating/service/UserService.java | 2 +- .../dating/service/impl/UserServiceImpl.java | 47 +++++++++++--- 8 files changed, 124 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/bao/dating/context/UserContext.java b/src/main/java/com/bao/dating/context/UserContext.java index 3d840a4..6d5bf24 100644 --- a/src/main/java/com/bao/dating/context/UserContext.java +++ b/src/main/java/com/bao/dating/context/UserContext.java @@ -6,7 +6,15 @@ package com.bao.dating.context; */ public class UserContext { + /** + * 当前线程的用户ID + */ + private static final ThreadLocal USER_HOLDER = new ThreadLocal<>(); + /** + * 当前线程的设备ID + */ + private static final ThreadLocal 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(); } } diff --git a/src/main/java/com/bao/dating/controller/UserController.java b/src/main/java/com/bao/dating/controller/UserController.java index 5da9084..a37a81c 100644 --- a/src/main/java/com/bao/dating/controller/UserController.java +++ b/src/main/java/com/bao/dating/controller/UserController.java @@ -48,7 +48,8 @@ public class UserController { @PostMapping("/logout") public Result 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); } diff --git a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java index 936f90e..2b95f6a 100644 --- a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java +++ b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java @@ -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); + } + } \ No newline at end of file diff --git a/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java index 1e0e99a..1b1320b 100644 --- a/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java +++ b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java @@ -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) { diff --git a/src/main/java/com/bao/dating/pojo/dto/UserLoginDTO.java b/src/main/java/com/bao/dating/pojo/dto/UserLoginDTO.java index 940b726..1ddcf49 100644 --- a/src/main/java/com/bao/dating/pojo/dto/UserLoginDTO.java +++ b/src/main/java/com/bao/dating/pojo/dto/UserLoginDTO.java @@ -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; } diff --git a/src/main/java/com/bao/dating/pojo/vo/UserLoginVO.java b/src/main/java/com/bao/dating/pojo/vo/UserLoginVO.java index bfeb93d..01b5e22 100644 --- a/src/main/java/com/bao/dating/pojo/vo/UserLoginVO.java +++ b/src/main/java/com/bao/dating/pojo/vo/UserLoginVO.java @@ -12,4 +12,5 @@ public class UserLoginVO implements Serializable { private Long userId; private String nickname; private String token; + private String deviceId; } diff --git a/src/main/java/com/bao/dating/service/UserService.java b/src/main/java/com/bao/dating/service/UserService.java index ab23da1..1f3c93f 100644 --- a/src/main/java/com/bao/dating/service/UserService.java +++ b/src/main/java/com/bao/dating/service/UserService.java @@ -25,7 +25,7 @@ public interface UserService { * @param token 登录凭证 * @return 注册结果 */ - void logout(String token); + void logout(String token, String deviceId); /** * 查询个人信息 diff --git a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java index 3f3b9ec..6108ac6 100644 --- a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java +++ b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java @@ -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 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) {