diff --git a/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java b/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java index 0426f8e..b7f5214 100644 --- a/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java +++ b/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java @@ -1,6 +1,7 @@ package com.onekeycall.videotablet.config; import com.onekeycall.videotablet.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,12 +37,11 @@ public class SecurityConfig { .requestMatchers("/api/ws/**", "/topic/**").permitAll() .requestMatchers("/uploadFile/**").permitAll() .requestMatchers("/public/**").permitAll() - .requestMatchers("/sn/**").permitAll() - .requestMatchers("/user/**").permitAll() +// .requestMatchers("/sn/**").permitAll() +// .requestMatchers("/user/**").permitAll() .requestMatchers("/rtc/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") // .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") - .requestMatchers("/user/**").permitAll() .requestMatchers("/contact/**").permitAll() .anyRequest().authenticated() ); diff --git a/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java b/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java index a508813..9f7ae41 100644 --- a/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java +++ b/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java @@ -3,6 +3,7 @@ package com.onekeycall.videotablet.config; import com.onekeycall.videotablet.handler.CustomWebSocketHandler; import com.onekeycall.videotablet.interceptor.AuthHandshakeInterceptor; import com.onekeycall.videotablet.utils.JwtUtil; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; @@ -11,16 +12,13 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry @Configuration @EnableWebSocket +@RequiredArgsConstructor public class WebSocketConfig implements WebSocketConfigurer { - @Autowired - private JwtUtil jwtUtil; + + private final JwtUtil jwtUtil; private final CustomWebSocketHandler webSocketHandler; - public WebSocketConfig(CustomWebSocketHandler webSocketHandler) { - this.webSocketHandler = webSocketHandler; - } - @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler, "/ws") // 客户端连接端点 diff --git a/src/main/java/com/onekeycall/videotablet/controller/HelloController.java b/src/main/java/com/onekeycall/videotablet/controller/pub/HelloController.java similarity index 73% rename from src/main/java/com/onekeycall/videotablet/controller/HelloController.java rename to src/main/java/com/onekeycall/videotablet/controller/pub/HelloController.java index 1aae104..2e42539 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/HelloController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/pub/HelloController.java @@ -1,20 +1,18 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.pub; import com.onekeycall.videotablet.result.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController +@RequestMapping("/public") public class HelloController { //引入 redis @Autowired private StringRedisTemplate stringRedisTemplate; - @GetMapping("/public/hello") + @GetMapping("/hello") public Result getMethodName() { return Result.ok().message("Welcome to Yijiantong"); } @@ -24,7 +22,7 @@ public class HelloController { * * @return */ - @PostMapping("/public/set") + @PostMapping("/set") public Result setRedis(@RequestParam(value = "username") String username) { //存储 key-value 键值对: "username"-"jaychou" stringRedisTemplate.opsForValue().set("username", username); @@ -36,7 +34,7 @@ public class HelloController { * * @return */ - @GetMapping("/public/get") + @GetMapping("/get") public Result getRedis(@RequestParam(value = "username") String username) { //通过 key 值读取 value String result = stringRedisTemplate.opsForValue().get(username); diff --git a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java b/src/main/java/com/onekeycall/videotablet/controller/pub/LoginController.java similarity index 96% rename from src/main/java/com/onekeycall/videotablet/controller/LoginController.java rename to src/main/java/com/onekeycall/videotablet/controller/pub/LoginController.java index a18d4dd..608de5f 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/pub/LoginController.java @@ -1,10 +1,11 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.pub; import com.onekeycall.videotablet.dto.TokenPair; import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.utils.JwtUtil; +import com.onekeycall.videotablet.utils.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -77,6 +78,10 @@ public class LoginController { } String userId = user.getUserId(); + if (TextUtils.isEmpty(user.getPassword())) { + return Result.error().message("用户未设置密码,请使用验证码登录"); + } + // 1. 创建认证令牌 Authentication authenticationToken = new UsernamePasswordAuthenticationToken(userId, password); diff --git a/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java b/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java similarity index 99% rename from src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java rename to src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java index d942c71..1896c15 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/pub/ManageSnController.java @@ -1,4 +1,4 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.pub; import com.onekeycall.videotablet.config.FilePath; import com.onekeycall.videotablet.entity.DeviceInfo; diff --git a/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java b/src/main/java/com/onekeycall/videotablet/controller/sms/AliyunSmsController.java similarity index 99% rename from src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java rename to src/main/java/com/onekeycall/videotablet/controller/sms/AliyunSmsController.java index c1e7cb0..666345e 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sms/AliyunSmsController.java @@ -1,4 +1,4 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.sms; import com.aliyun.dysmsapi20170525.models.SendSmsResponse; import com.aliyun.tea.TeaException; diff --git a/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java b/src/main/java/com/onekeycall/videotablet/controller/sms/TencentSmsController.java similarity index 99% rename from src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java rename to src/main/java/com/onekeycall/videotablet/controller/sms/TencentSmsController.java index 5025165..769d81c 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sms/TencentSmsController.java @@ -1,4 +1,4 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.sms; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.sms.SendSms; diff --git a/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java b/src/main/java/com/onekeycall/videotablet/controller/sn/BindSnController.java similarity index 92% rename from src/main/java/com/onekeycall/videotablet/controller/BindSnController.java rename to src/main/java/com/onekeycall/videotablet/controller/sn/BindSnController.java index 17a9eaa..7c53007 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sn/BindSnController.java @@ -1,4 +1,4 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.sn; import com.google.gson.JsonObject; import com.onekeycall.videotablet.entity.DeviceInfo; @@ -160,22 +160,22 @@ public class BindSnController { // TODO: 2025/8/22 Device_Token在docker无法被接收到,使用Device-Token代替 @GetMapping("/get_bind_statu") public Result getBindStatus( - @RequestHeader("Device-Token") String deviceToken, @RequestHeader("Device-ID") String deviceId, - @RequestHeader("Device-Sig") String deviceSig, +// @RequestHeader("Device-Token") String deviceToken, @RequestHeader("Device-ID") String deviceId, +// @RequestHeader("Device-Sig") String deviceSig, @RequestParam(value = "sn") String sn) { - if (!jwtUtil.validateDeviceToken(deviceToken, deviceId, sn)) { - return Result.error().message("Invalid token"); - } +// if (!jwtUtil.validateDeviceToken(deviceToken, deviceId, sn)) { +// return Result.error().message("Invalid token"); +// } DeviceInfo deviceInfo = deviceSnService.findBySn(sn); if (deviceInfo == null) { return Result.notFound().message("sn not found"); } - if (!deviceInfo.getBindSig().equals(deviceSig)) { - return Result.error().message("device sig not match"); - } +// if (!deviceInfo.getBindSig().equals(deviceSig)) { +// return Result.error().message("device sig not match"); +// } if (TextUtils.isEmpty(deviceInfo.getBindPhone())) { return Result.error().message("sn not bind"); diff --git a/src/main/java/com/onekeycall/videotablet/controller/DeviceApkInfoController.java b/src/main/java/com/onekeycall/videotablet/controller/sn/DeviceApkInfoController.java similarity index 83% rename from src/main/java/com/onekeycall/videotablet/controller/DeviceApkInfoController.java rename to src/main/java/com/onekeycall/videotablet/controller/sn/DeviceApkInfoController.java index 25ec76f..80740a4 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/DeviceApkInfoController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sn/DeviceApkInfoController.java @@ -1,12 +1,8 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.sn; -import com.aliyun.core.annotation.Body; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.onekeycall.videotablet.bean.ApkUploadRequest; import com.onekeycall.videotablet.entity.ApkInfo; -import com.onekeycall.videotablet.entity.DeviceApkInfo; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.DeviceApkInfoService; import org.slf4j.Logger; diff --git a/src/main/java/com/onekeycall/videotablet/controller/DevicesController.java b/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java similarity index 76% rename from src/main/java/com/onekeycall/videotablet/controller/DevicesController.java rename to src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java index 5db5b3e..7993fd7 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/DevicesController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/sn/DevicesController.java @@ -1,4 +1,4 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.sn; import com.onekeycall.videotablet.entity.Contact; import com.onekeycall.videotablet.entity.DeviceInfo; @@ -14,9 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Date; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/sn") @@ -30,41 +28,7 @@ public class DevicesController { @Autowired private ContactService contactService; - @GetMapping("/get_sn_list") - public Result register( - @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, - @RequestParam(value = "user_id") String userId, @RequestParam(value = "sn", required = false) String sn) { - // 1. 校验 Authorization 头 - if (!authHeader.startsWith("Bearer ")) { - return Result.error().message("Invalid Authorization header"); - } - String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 - // 2. 校验 Token - if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { - return Result.error().message("Invalid token"); - } - - if (TextUtils.isEmpty(sn)) { - List deviceInfos = deviceSnService.findByUserId(userId); - if (deviceInfos == null || deviceInfos.isEmpty()) { - return Result.notFound().message("sn not found"); - } else { - return Result.ok().data("deviceInfos", deviceInfos); - } - } else { - DeviceInfo deviceInfo = deviceSnService.findBySn(sn); - if (deviceInfo == null) { - return Result.notFound().message("sn not found"); - } - - if (!deviceInfo.getUserId().equals(userId)) { - return Result.error().message("sn not belong to user"); - } - - return Result.ok().data("deviceInfo", deviceInfo); - } - } @PostMapping("/update_location") public Result updateLocation( diff --git a/src/main/java/com/onekeycall/videotablet/controller/UserController.java b/src/main/java/com/onekeycall/videotablet/controller/user/UserController.java similarity index 78% rename from src/main/java/com/onekeycall/videotablet/controller/UserController.java rename to src/main/java/com/onekeycall/videotablet/controller/user/UserController.java index a6ed15a..3e01841 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/UserController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/user/UserController.java @@ -1,6 +1,6 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.user; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import com.onekeycall.videotablet.controller.pub.LoginController; import com.onekeycall.videotablet.dto.TokenPair; import com.onekeycall.videotablet.entity.DeviceApkInfo; import com.onekeycall.videotablet.entity.DeviceInfo; @@ -21,6 +21,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.web.bind.annotation.*; import java.util.HashMap; +import java.util.List; import java.util.Map; @RestController @@ -105,6 +106,42 @@ public class UserController { return Result.ok().data("user_info", userInfo); } + @GetMapping("/get_sn_list") + public Result register( + @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, + @RequestParam(value = "user_id") String userId, @RequestParam(value = "sn", required = false) String sn) { + // 1. 校验 Authorization 头 + if (!authHeader.startsWith("Bearer ")) { + return Result.error().message("Invalid Authorization header"); + } + String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 + + // 2. 校验 Token + if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { + return Result.error().message("Invalid token"); + } + + if (TextUtils.isEmpty(sn)) { + List deviceInfos = deviceSnService.findByUserId(userId); + if (deviceInfos == null || deviceInfos.isEmpty()) { + return Result.notFound().message("sn not found"); + } else { + return Result.ok().data("deviceInfos", deviceInfos); + } + } else { + DeviceInfo deviceInfo = deviceSnService.findBySn(sn); + if (deviceInfo == null) { + return Result.notFound().message("sn not found"); + } + + if (!deviceInfo.getUserId().equals(userId)) { + return Result.error().message("sn not belong to user"); + } + + return Result.ok().data("deviceInfo", deviceInfo); + } + } + @GetMapping("/get_sn_location") public Result getSnLocation( @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, diff --git a/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java b/src/main/java/com/onekeycall/videotablet/controller/user/UserPasswordController.java similarity index 98% rename from src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java rename to src/main/java/com/onekeycall/videotablet/controller/user/UserPasswordController.java index 66e3e20..dc4131f 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/user/UserPasswordController.java @@ -1,11 +1,10 @@ -package com.onekeycall.videotablet.controller; +package com.onekeycall.videotablet.controller.user; import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.TextUtils; -import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; diff --git a/src/main/java/com/onekeycall/videotablet/entity/User.java b/src/main/java/com/onekeycall/videotablet/entity/User.java index 0d274bc..d7345b6 100644 --- a/src/main/java/com/onekeycall/videotablet/entity/User.java +++ b/src/main/java/com/onekeycall/videotablet/entity/User.java @@ -121,11 +121,11 @@ public class User implements UserDetails { } public String getUsername() { - return nickname; + return userId; } public void setUsername(String username) { - this.nickname = username; + this.userId = username; } public String getPassword() { diff --git a/src/main/java/com/onekeycall/videotablet/filter/JwtAuthenticationFilter.java b/src/main/java/com/onekeycall/videotablet/filter/JwtAuthenticationFilter.java index f970155..a278094 100644 --- a/src/main/java/com/onekeycall/videotablet/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/onekeycall/videotablet/filter/JwtAuthenticationFilter.java @@ -1,9 +1,22 @@ package com.onekeycall.videotablet.filter; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.onekeycall.videotablet.entity.DeviceInfo; +import com.onekeycall.videotablet.gson.GsonUtils; +import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.service.DeviceSnService; import com.onekeycall.videotablet.utils.JwtUtil; +import com.onekeycall.videotablet.utils.TextUtils; +import io.jsonwebtoken.lang.Collections; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -11,60 +24,176 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; import java.io.IOException; +import java.nio.charset.StandardCharsets; @Component +@RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private final UserDetailsService userDetailsService; + private final JwtUtil jwtUtil; - public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) { - this.userDetailsService = userDetailsService; - this.jwtUtil = jwtUtil; - } + private final DeviceSnService deviceSnService; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // 从请求头中获取Token - String authorizationHeader = request.getHeader("Authorization"); + // 使用ContentCachingRequestWrapper包装请求以支持多次读取 + ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); - String username = null; - String jwt = null; + String uripath = wrappedRequest.getRequestURI(); + // 新增请求路径日志 + logger.debug("Processing request: " + uripath); + + if (uripath.startsWith("/user")) { + // 从请求头中获取Token + String authorizationHeader = wrappedRequest.getHeader("Authorization"); + // 增强header检查日志 + if (authorizationHeader == null) { + logger.debug("Missing Authorization header for: " + wrappedRequest.getRequestURI()); + setResponse(response, Result.unAuthorized().message("Missing Authorization header")); + return; + } else { + logger.debug("Found Authorization header"); + } + + String username = null; + String jwt = null; + + // 检查Authorization头是否存在且以Bearer开头 + if (authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + try { + username = jwtUtil.getUsernameFromToken(jwt); + logger.debug("Extracted username: " + username); + } catch (Exception e) { + logger.error("Token解析失败 | Token: " + jwt, e); + setResponse(response, Result.unAuthorized().message("Invalid credentials")); + return; // 重要!验证失败时终止过滤器链 + } + } + + // 如果获取到用户名且当前上下文没有认证信息 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + // 新增权限检查日志 + logger.debug("Loaded user authorities: " + userDetails.getAuthorities()); + + // 验证Token + if (jwtUtil.validateToken(jwt, userDetails)) { + // 创建认证令牌 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + usernamePasswordAuthenticationToken + .setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + + // 将认证信息存入上下文 + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + logger.debug("Successfully authenticated user: " + username); + } else { + logger.warn("Token验证失败 | User: " + username); + setResponse(response, Result.unAuthorized().message("Token validation failed")); + return; // 重要!验证失败时终止过滤器链 + } + } + } else if (uripath.startsWith("/sn")) { + // 设备绑定相关鉴权 + String deviceToken = wrappedRequest.getHeader("Device-Token"); + String deviceId = wrappedRequest.getHeader("Device-Id"); + String deviceSig = wrappedRequest.getHeader("Device-Sig"); + String sn = null; + + // 1. 尝试从请求参数获取sn + sn = wrappedRequest.getParameter("sn"); + logger.debug("sn from parameter: {}", sn); + + // 2. 如果参数中没有sn且是POST请求,尝试从JSON请求体获取 + if (sn == null && "POST".equalsIgnoreCase(wrappedRequest.getMethod())) { + String contentType = wrappedRequest.getContentType(); + if (contentType != null && (contentType.contains("application/json") || contentType.contains("application/*+json"))) { + try { + String body = new String(wrappedRequest.getContentAsByteArray(), StandardCharsets.UTF_8); + if (!TextUtils.isEmpty(body)) { + JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject(); + if (jsonObject.has("sn")) { + sn = jsonObject.get("sn").getAsString(); + logger.debug("sn from request body: {}", sn); + } + } + } catch (Exception e) { + logger.error("Failed to parse sn from request body", e); + // 解析失败不中断,继续后续检查 + } + } + } + + if (deviceToken == null || deviceId == null || deviceSig == null || sn == null) { + logger.warn("Missing device headers | DeviceToken: {} | DeviceID: {} | DeviceSig: {} | SN: {}", deviceToken, deviceId, deviceSig, sn); + setResponse(response, Result.unAuthorized().message("缺少设备认证信息")); + return; + } + + DeviceInfo deviceInfo = deviceSnService.findBySn(sn); + if (deviceInfo == null) { + logger.warn("SN not found | SN: {}", sn); + setResponse(response, Result.notFound().message("sn not found")); + return; + } + + if (!deviceInfo.getBindSig().equals(deviceSig)) { + setResponse(response, Result.error().message("device sig not match")); + return; + } + + if (TextUtils.isEmpty(deviceInfo.getBindPhone())) { + setResponse(response, Result.error().message("sn not bind")); + return; + } - // 检查Authorization头是否存在且以Bearer开头 - if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { - jwt = authorizationHeader.substring(7); try { - username = jwtUtil.getUsernameFromToken(jwt); + // 调用SN验证服务(假设有SNService) + if (!jwtUtil.validateDeviceToken(deviceToken, deviceId, sn)) { + logger.warn("SN验证失败 | DeviceID: {} | SN: {}", deviceId, sn); + setResponse(response, Result.unAuthorized().message("设备验证失败")); + return; + } + logger.debug("SN验证成功 | DeviceID: {} | SN: {}", deviceId, sn); + + // 3. 设置认证信息到SecurityContext,解决403问题 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + sn, null, Collections.emptyList()); // 可根据需要添加角色权限 + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + SecurityContextHolder.getContext().setAuthentication(authToken); + logger.debug("Set authentication for SN: {}", sn); } catch (Exception e) { - logger.error("无法获取用户信息或Token无效", e); + logger.error("SN验证异常 | DeviceID: {} | SN: {}", deviceId, sn, e); + setResponse(response, Result.error().message("设备验证服务异常")); + return; } - } - // 如果获取到用户名且当前上下文没有认证信息 - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); - - // 验证Token - if (jwtUtil.validateToken(jwt, userDetails)) { - // 创建认证令牌 - UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - - usernamePasswordAuthenticationToken - .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - // 将认证信息存入上下文 - SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); - } } // 继续过滤器链 - filterChain.doFilter(request, response); + filterChain.doFilter(wrappedRequest, response); + } + + private void setResponse(HttpServletResponse response, Result result) throws IOException { + SecurityContextHolder.clearContext(); + + response.setStatus(HttpServletResponse.SC_OK); // 设置为200状态码 + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write(GsonUtils.toJSONString(result)); + response.getWriter().flush(); + response.getWriter().close(); } } diff --git a/src/main/java/com/onekeycall/videotablet/gson/GsonUtils.java b/src/main/java/com/onekeycall/videotablet/gson/GsonUtils.java new file mode 100644 index 0000000..fddfef1 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/gson/GsonUtils.java @@ -0,0 +1,153 @@ +package com.onekeycall.videotablet.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import com.onekeycall.videotablet.filter.JwtAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +public class GsonUtils { + //https://blog.csdn.net/zte1055889498/article/details/122400299 + static Logger logger = LoggerFactory.getLogger(GsonUtils.class); + + public static JsonObject getJsonObject(String jsonString) { + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + return jsonObject; + } + + private static final Gson gson; + + static { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapterFactory(new NullStringToEmptyAdapterFactory()); + builder.registerTypeAdapter(Integer.class, new IntegerDefault0Adapter()); + builder.registerTypeAdapter(int.class, new IntegerDefault0Adapter()); + builder.disableHtmlEscaping(); + builder.enableComplexMapKeySerialization(); + // builder.excludeFieldsWithoutExposeAnnotation(); + builder.setDateFormat("yyyy-MM-dd HH:mm:ss"); + gson = builder.create(); + } + + public static Type makeJavaType(Type rawType, Type... typeArguments) { + return TypeToken.getParameterized(rawType, typeArguments).getType(); + } + + public static String toString(Object value) { + if (Objects.isNull(value)) { + return null; + } + if (value instanceof String) { + return (String) value; + } + return toJSONString(value); + } + + public static String toJSONString(Object value) { + return gson.toJson(value); + } + + public static String toPrettyString(Object value) { + return gson.newBuilder().setPrettyPrinting().create().toJson(value); + } + + public static JsonElement fromJavaObject(Object value) { + JsonElement result = null; + if (Objects.nonNull(value) && (value instanceof String)) { + result = parseObject((String) value); + } else { + result = gson.toJsonTree(value); + } + return result; + } + + public static JsonElement parseObject(String content) { + return JsonParser.parseString(content); + } + + public static JsonElement getJsonElement(JsonObject node, String name) { + return node.get(name); + } + + public static JsonElement getJsonElement(JsonArray node, int index) { + return node.get(index); + } + + public static T toJavaObject(JsonElement node, Class clazz) { + return gson.fromJson(node, clazz); + } + + public static T toJavaObject(JsonElement node, Type type) { + return gson.fromJson(node, type); + } + + public static T toJavaObject(JsonElement node, TypeToken typeToken) { + return toJavaObject(node, typeToken.getType()); + } + + public static List toJavaList(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static T toJavaObject(String content, Class clazz) { + JsonObject jsonObject = getJsonObject(content); + String jsonString = jsonObject.toString(); + try { + return gson.fromJson(jsonString, clazz); + } catch (Exception e) { + logger.error("GsonUtils", "toJavaObject: " + e.getMessage()); + } + return null; + } + + public static T toJavaObject(String content, Type type) { + return gson.fromJson(content, type); + } + + public static T toJavaObject(String content, TypeToken typeToken) { + return toJavaObject(content, typeToken.getType()); + } + + public static List toJavaList(String content, Class clazz) { + return toJavaObject(content, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(String content, Class clazz) { + return toJavaObject(content, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } +} diff --git a/src/main/java/com/onekeycall/videotablet/gson/IntegerDefault0Adapter.java b/src/main/java/com/onekeycall/videotablet/gson/IntegerDefault0Adapter.java new file mode 100644 index 0000000..804d591 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/gson/IntegerDefault0Adapter.java @@ -0,0 +1,35 @@ +package com.onekeycall.videotablet.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; + +import java.lang.reflect.Type; + +public class IntegerDefault0Adapter implements JsonSerializer, JsonDeserializer { + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + try { + if (json.getAsString().equals("")) { + return 0; + } + } catch (Exception ignore) { + } + try { + return json.getAsInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src); + } +} \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/gson/NullStringToEmptyAdapterFactory.java b/src/main/java/com/onekeycall/videotablet/gson/NullStringToEmptyAdapterFactory.java new file mode 100644 index 0000000..b6d35d8 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/gson/NullStringToEmptyAdapterFactory.java @@ -0,0 +1,45 @@ +package com.onekeycall.videotablet.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class NullStringToEmptyAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + + Class rawType = (Class) type.getRawType(); + if (rawType != String.class) { + return null; + } + return (TypeAdapter) new StringAdapter(); + } + + public static class StringAdapter extends TypeAdapter { + @Override + public String read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return ""; + } + return reader.nextString(); + } + + @Override + public void write(JsonWriter writer, String value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + writer.value(value); + } + } + +} + diff --git a/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java b/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java index d99e40a..923c362 100644 --- a/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java +++ b/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java @@ -18,6 +18,8 @@ public class CustomWebSocketHandler extends TextWebSocketHandler { // 存储活跃会话(线程安全) private final Map sessions = new ConcurrentHashMap<>(); + + @Override public void afterConnectionEstablished(WebSocketSession session) { String sessionId = session.getId(); diff --git a/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java b/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java index 4a32759..f19cb6a 100644 --- a/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java +++ b/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java @@ -22,7 +22,7 @@ public class AuthHandshakeInterceptor implements HandshakeInterceptor { String deviceId = request.getHeaders().getFirst("Device-ID"); String userId = request.getHeaders().getFirst("user_id"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { + if (authHeader == null || !authHeader.startsWith("Bearer ")||userId==null) { return false; } String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 diff --git a/src/main/java/com/onekeycall/videotablet/service/UserService.java b/src/main/java/com/onekeycall/videotablet/service/UserService.java index 1c48ba0..657e015 100644 --- a/src/main/java/com/onekeycall/videotablet/service/UserService.java +++ b/src/main/java/com/onekeycall/videotablet/service/UserService.java @@ -5,6 +5,7 @@ import com.onekeycall.videotablet.repository.UserRepository; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.utils.AESUtil; import com.onekeycall.videotablet.utils.SecureIdGenerator; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.userdetails.UserDetails; @@ -17,18 +18,19 @@ import java.util.Date; import java.util.Map; @Service +@RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final RedisTemplate redisTemplate; - @Autowired - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, RedisTemplate redisTemplate) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.redisTemplate = redisTemplate; - } +// @Autowired +// public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, RedisTemplate redisTemplate) { +// this.userRepository = userRepository; +// this.passwordEncoder = passwordEncoder; +// this.redisTemplate = redisTemplate; +// } public User getUserByPhone(String phone) { return userRepository.findUserByPhone(phone).orElse(null); diff --git a/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java b/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java index d351b74..2a438bd 100644 --- a/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java +++ b/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java @@ -1,6 +1,5 @@ package com.onekeycall.videotablet.utils; -import com.onekeycall.videotablet.controller.LoginController; import com.onekeycall.videotablet.dto.TokenPair; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; @@ -207,7 +206,7 @@ public class JwtUtil { // 从Token中获取所有声明 private Claims getAllClaimsFromToken(String token) { return Jwts.parser() - .setSigningKey(secret) + .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) .build() .parseClaimsJws(token) .getBody(); @@ -239,14 +238,14 @@ public class JwtUtil { // 验证Token public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); - return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + boolean isExpired = isTokenExpired(token); + boolean result = username.equals(userDetails.getUsername()); + return result && !isExpired; } - @Value("${jwt.tablet.secret}") private String TABLET_SECRET; - /** * 生成设备签名(首次绑定) * diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 94a8f7a..81956e5 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -56,5 +56,8 @@ logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger - %msg%n logging.logback.rollingpolicy.max-file-size=10MB logging.logback.rollingpolicy.max-history=30 +logging.level.com.onekeycall.videotablet.filter=DEBUG +logging.level.org.springframework.security=DEBUG + mybatis.type-aliases-package=com.onekeycall.videotablet.entity mybatis.mapperLocations=classpath:mapper/*.xml \ No newline at end of file