增加构建通知

This commit is contained in:
dengqichen 2025-11-20 17:16:48 +08:00
parent 1838a26494
commit 5b8e12b82f
9 changed files with 326 additions and 14 deletions

View File

@ -131,6 +131,13 @@
<scope>provided</scope>
</dependency>
<!-- User Agent Parser -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>7.26.0</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -15,6 +15,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@ -34,23 +35,23 @@ public class ServerApiController
private IServerService serverService;
@Override
public Response<ServerDTO> create(ServerDTO dto) {
public Response<ServerDTO> create(@Validated @RequestBody ServerDTO dto) {
return super.create(dto);
}
@Override
public Response<ServerDTO> update(Long aLong, ServerDTO dto) {
return super.update(aLong, dto);
public Response<ServerDTO> update(@PathVariable Long id, @Validated @RequestBody ServerDTO dto) {
return super.update(id, dto);
}
@Override
public Response<Void> delete(Long aLong) {
return super.delete(aLong);
public Response<Void> delete(@PathVariable Long id) {
return super.delete(id);
}
@Override
public Response<ServerDTO> findById(Long aLong) {
return super.findById(aLong);
public Response<ServerDTO> findById(Long id) {
return super.findById(id);
}
@Override

View File

@ -2,10 +2,12 @@ package com.qqchen.deploy.backend.framework.security.util;
import com.qqchen.deploy.backend.framework.security.CustomUserDetails;
import com.qqchen.deploy.backend.framework.utils.RedisUtil;
import com.qqchen.deploy.backend.framework.utils.UserAgentUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
@ -32,6 +34,9 @@ public class JwtTokenUtil {
@Resource
private RedisUtil redisUtil;
@Resource
private UserAgentUtil userAgentUtil;
// Redis Key前缀
private static final String TOKEN_PREFIX = "auth:token:";
@ -74,7 +79,7 @@ public class JwtTokenUtil {
}
/**
* 生成Token并存储到Redis
* 生成Token集成Redis存储
*
* @param userDetails 用户信息
* @return Token字符串
@ -91,6 +96,26 @@ public class JwtTokenUtil {
return token;
}
/**
* 生成Token集成Redis存储包含请求信息
*
* @param userDetails 用户信息
* @param request HTTP请求
* @return Token字符串
*/
public String generateToken(UserDetails userDetails, HttpServletRequest request) {
Map<String, Object> claims = new HashMap<>();
String token = doGenerateToken(claims, userDetails.getUsername());
// 存储到Redis如果userDetails是CustomUserDetails可以获取userId
if (userDetails instanceof CustomUserDetails) {
Long userId = ((CustomUserDetails) userDetails).getUserId();
storeToken(userId, token, request);
}
return token;
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
@ -159,6 +184,32 @@ public class JwtTokenUtil {
log.debug("Token已存储到Redis: userId={}, ttl={}秒", userId, expiration);
}
/**
* 存储Token到Redis包含登录时间和请求信息
*/
private void storeToken(Long userId, String token, HttpServletRequest request) {
String key = TOKEN_PREFIX + userId;
// 获取IP地址
String ipAddress = userAgentUtil.getRealIpAddress(request);
// 解析User-Agent
String userAgentString = request.getHeader("User-Agent");
UserAgentUtil.UserAgentInfo userAgentInfo = userAgentUtil.parseUserAgent(userAgentString);
// 存储Token + 登录时间 + 请求信息
Map<String, Object> tokenInfo = new HashMap<>();
tokenInfo.put("token", token);
tokenInfo.put("loginTime", LocalDateTime.now().toString());
tokenInfo.put("ipAddress", ipAddress);
tokenInfo.put("browser", userAgentInfo.getBrowser());
tokenInfo.put("os", userAgentInfo.getOs());
redisUtil.set(key, tokenInfo, expiration);
log.debug("Token已存储到Redis: userId={}, ip={}, browser={}, os={}, ttl={}秒",
userId, ipAddress, userAgentInfo.getBrowser(), userAgentInfo.getOs(), expiration);
}
/**
* 从Redis获取Token字符串
*/

View File

@ -0,0 +1,244 @@
package com.qqchen.deploy.backend.framework.utils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import nl.basjes.parse.useragent.UserAgent;
import nl.basjes.parse.useragent.UserAgentAnalyzer;
import org.springframework.stereotype.Component;
/**
* User-Agent解析工具类
*
* @author qqchen
* @date 2025-11-20
*/
@Slf4j
@Component
public class UserAgentUtil {
private static final UserAgentAnalyzer USER_AGENT_ANALYZER = UserAgentAnalyzer
.newBuilder()
.hideMatcherLoadStats()
.withCache(10000)
.build();
/**
* 解析User-Agent信息
*
* @param userAgentString User-Agent字符串
* @return 解析结果
*/
public UserAgentInfo parseUserAgent(String userAgentString) {
if (userAgentString == null || userAgentString.trim().isEmpty()) {
return UserAgentInfo.unknown();
}
try {
UserAgent userAgent = USER_AGENT_ANALYZER.parse(userAgentString);
return UserAgentInfo.builder()
.browser(getBrowserName(userAgent))
.browserVersion(userAgent.getValue(UserAgent.AGENT_VERSION))
.os(getOperatingSystem(userAgent))
.osVersion(userAgent.getValue(UserAgent.OPERATING_SYSTEM_VERSION))
.deviceType(userAgent.getValue(UserAgent.DEVICE_CLASS))
.build();
} catch (Exception e) {
log.warn("解析User-Agent失败: {}", userAgentString, e);
return UserAgentInfo.unknown();
}
}
/**
* 从HttpServletRequest获取真实IP地址
*
* @param request HTTP请求
* @return IP地址
*/
public String getRealIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
// 多级代理的情况第一个IP为客户端真实IP
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index).trim();
} else {
return ip.trim();
}
}
ip = request.getHeader("X-Real-IP");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
ip = request.getHeader("Proxy-Client-IP");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
ip = request.getHeader("WL-Proxy-Client-IP");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
ip = request.getHeader("HTTP_CLIENT_IP");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 如果都没有返回request.getRemoteAddr()
return request.getRemoteAddr();
}
/**
* 获取浏览器名称
*/
private String getBrowserName(UserAgent userAgent) {
String agentName = userAgent.getValue(UserAgent.AGENT_NAME);
String agentVersion = userAgent.getValue(UserAgent.AGENT_VERSION);
if ("Unknown".equals(agentName)) {
return "Unknown";
}
// 返回浏览器名称 + 主版本号
if (agentVersion != null && !agentVersion.isEmpty() && !"Unknown".equals(agentVersion)) {
String majorVersion = agentVersion.split("\\.")[0];
return agentName + " " + majorVersion;
}
return agentName;
}
/**
* 获取操作系统名称
*/
private String getOperatingSystem(UserAgent userAgent) {
String osName = userAgent.getValue(UserAgent.OPERATING_SYSTEM_NAME);
String osVersion = userAgent.getValue(UserAgent.OPERATING_SYSTEM_VERSION);
if ("Unknown".equals(osName)) {
return "Unknown";
}
// Windows NT 版本号转换为友好名称
if ("Windows NT".equals(osName) && osVersion != null && !osVersion.isEmpty()) {
return convertWindowsNTVersion(osVersion);
}
// 返回操作系统名称 + 版本
if (osVersion != null && !osVersion.isEmpty() && !"Unknown".equals(osVersion)) {
return osName + " " + osVersion;
}
return osName;
}
/**
* 将Windows NT版本号转换为用户友好的显示名称
*/
private String convertWindowsNTVersion(String ntVersion) {
switch (ntVersion) {
case "10.0":
return "Windows 10/11"; // Windows 10 11 都是 NT 10.0
case "6.3":
return "Windows 8.1";
case "6.2":
return "Windows 8";
case "6.1":
return "Windows 7";
case "6.0":
return "Windows Vista";
case "5.2":
return "Windows Server 2003";
case "5.1":
return "Windows XP";
case "5.0":
return "Windows 2000";
default:
return "Windows NT " + ntVersion; // 未知版本保持原样
}
}
/**
* User-Agent解析结果
*/
public static class UserAgentInfo {
private String browser;
private String browserVersion;
private String os;
private String osVersion;
private String deviceType;
public static UserAgentInfo unknown() {
return UserAgentInfo.builder()
.browser("Unknown")
.browserVersion("Unknown")
.os("Unknown")
.osVersion("Unknown")
.deviceType("Unknown")
.build();
}
public static UserAgentInfoBuilder builder() {
return new UserAgentInfoBuilder();
}
// Getters
public String getBrowser() { return browser; }
public String getBrowserVersion() { return browserVersion; }
public String getOs() { return os; }
public String getOsVersion() { return osVersion; }
public String getDeviceType() { return deviceType; }
// Builder
public static class UserAgentInfoBuilder {
private String browser;
private String browserVersion;
private String os;
private String osVersion;
private String deviceType;
public UserAgentInfoBuilder browser(String browser) {
this.browser = browser;
return this;
}
public UserAgentInfoBuilder browserVersion(String browserVersion) {
this.browserVersion = browserVersion;
return this;
}
public UserAgentInfoBuilder os(String os) {
this.os = os;
return this;
}
public UserAgentInfoBuilder osVersion(String osVersion) {
this.osVersion = osVersion;
return this;
}
public UserAgentInfoBuilder deviceType(String deviceType) {
this.deviceType = deviceType;
return this;
}
public UserAgentInfo build() {
UserAgentInfo info = new UserAgentInfo();
info.browser = this.browser;
info.browserVersion = this.browserVersion;
info.os = this.os;
info.osVersion = this.osVersion;
info.deviceType = this.deviceType;
return info;
}
}
}
}

View File

@ -12,6 +12,7 @@ import com.qqchen.deploy.backend.system.model.response.LoginResponse;
import com.qqchen.deploy.backend.system.service.IUserService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
@ -71,8 +72,8 @@ public class UserApiController extends BaseController<User, UserDTO, Long, UserQ
@Operation(summary = "用户登录")
@PostMapping("/login")
public Response<LoginResponse> login(@Validated @RequestBody LoginRequest request) {
return Response.success(userService.login(request));
public Response<LoginResponse> login(@Validated @RequestBody LoginRequest request, HttpServletRequest httpRequest) {
return Response.success(userService.login(request, httpRequest));
}
@Operation(summary = "获取当前用户信息")

View File

@ -9,12 +9,13 @@ import com.qqchen.deploy.backend.system.model.response.LoginResponse;
import com.qqchen.deploy.backend.system.entity.User;
import com.qqchen.deploy.backend.system.model.request.UserRequest;
import com.qqchen.deploy.backend.system.model.RoleDTO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public interface IUserService extends IBaseService<User, UserDTO, UserQuery, Long> {
LoginResponse login(LoginRequest request);
LoginResponse login(LoginRequest request, HttpServletRequest httpRequest);
UserDTO register(UserRequest request);
boolean checkUsernameExists(String username);
boolean checkEmailExists(String email);

View File

@ -96,6 +96,9 @@ public class OnlineUserServiceImpl implements IOnlineUserService {
if (tokenInfo == null) continue;
String loginTimeStr = (String) tokenInfo.get("loginTime");
String ipAddress = (String) tokenInfo.get("ipAddress");
String browser = (String) tokenInfo.get("browser");
String os = (String) tokenInfo.get("os");
// 查询用户详细信息
User user = userRepository.findById(userId).orElse(null);
@ -115,6 +118,9 @@ public class OnlineUserServiceImpl implements IOnlineUserService {
.loginTime(loginTime)
.lastActiveTime(loginTime) // 由于没有心跳最后活跃时间=登录时间
.onlineDuration(onlineDuration)
.ipAddress(ipAddress)
.browser(browser)
.os(os)
.build();
onlineUsers.add(vo);

View File

@ -25,6 +25,7 @@ import com.qqchen.deploy.backend.system.repository.IRoleRepository;
import com.qqchen.deploy.backend.system.entity.Department;
import com.qqchen.deploy.backend.system.entity.Role;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Hibernate;
import org.springframework.security.authentication.AuthenticationManager;
@ -118,12 +119,12 @@ public class UserServiceImpl extends BaseServiceImpl<User, UserDTO, UserQuery, L
@Override
@Transactional(readOnly = true)
@Audited(action = "USER_LOGIN", detail = "登录")
public LoginResponse login(LoginRequest request) {
public LoginResponse login(LoginRequest request, HttpServletRequest httpRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
String token = jwtTokenUtil.generateToken(userDetails, httpRequest);
User user = userRepository.findByUsernameAndDeletedFalse(userDetails.getUsername())
.orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND));

View File

@ -104,7 +104,7 @@ VALUES
-- 权限管理(隐藏菜单)
(6, '权限管理', '/system/permissions', 'System/Permission/List', 'SafetyOutlined', 'system:permission', 2, 1, 50, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 在线用户管理
(7, '在线用户', '/system/online', 'System/Online/List', 'UsersRound', 'system:online:view', 2, 1, 60, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
(7, '在线用户', '/system/online', 'System/Online/List', 'UserSwitchOutlined', 'system:online:view', 2, 1, 60, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
-- ==================== 初始化角色数据 ====================
DELETE FROM sys_role WHERE id < 100;