增加构建通知
This commit is contained in:
parent
1838a26494
commit
5b8e12b82f
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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字符串
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = "获取当前用户信息")
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user