refactor(security): 重构安全配置并优化测试环境

- 移除旧的测试套件和UAT测试文件
- 更新密码编码器配置使用BCrypt strength=12
- 添加用户角色关联表和相关服务
- 优化前端日期显示格式
- 清理无用资源和配置文件
- 增强测试数据管理和清理功能
This commit is contained in:
张翔
2026-03-27 13:00:22 +08:00
parent ce30893a96
commit af44c23f21
294 changed files with 16057 additions and 22601 deletions
@@ -0,0 +1,35 @@
package cn.novalon.manage.sys.config;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 密码编码器配置
*
* @author 张翔
* @date 2026-03-26
*/
@Configuration
public class PasswordEncoderConfig {
private static final Logger logger = LoggerFactory.getLogger(PasswordEncoderConfig.class);
@Bean
@Primary
public PasswordEncoder passwordEncoder() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
logger.info("创建主密码编码器: BCryptPasswordEncoder(strength=12), 类型: {}", encoder.getClass().getName());
return encoder;
}
@PostConstruct
public void init() {
logger.info("PasswordEncoderConfig 已加载");
}
}
@@ -28,6 +28,9 @@ public class SysUser extends BaseDomain {
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "头像", example = "https://example.com/avatar.jpg")
private String avatar;
@Schema(description = "角色ID", example = "1")
private Long roleId;
@@ -74,6 +77,14 @@ public class SysUser extends BaseDomain {
this.phone = phone;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Long getRoleId() {
return roleId;
}
@@ -0,0 +1,63 @@
package cn.novalon.manage.sys.core.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "用户角色关联实体")
public class UserRole {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "角色ID")
private Long roleId;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "创建人")
private String createdBy;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
}
@@ -0,0 +1,28 @@
package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.sys.core.domain.UserRole;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IUserRoleRepository {
Mono<UserRole> save(UserRole userRole);
Mono<Void> deleteById(Long id);
Mono<Void> deleteByUserId(Long userId);
Mono<Void> deleteByRoleId(Long roleId);
Flux<UserRole> findByUserId(Long userId);
Flux<UserRole> findByRoleId(Long roleId);
Mono<Long> countByUserId(Long userId);
Mono<Long> countByRoleId(Long roleId);
Flux<UserRole> findAll();
Mono<UserRole> findById(Long id);
}
@@ -54,4 +54,10 @@ public interface ISysUserService {
Mono<SysUser> changePassword(Long userId, String oldPassword, String newPassword);
Mono<Void> updateRoleIdToNullByRoleId(Long roleId);
Mono<Void> assignRolesToUser(Long userId, java.util.List<Long> roleIds);
Flux<cn.novalon.manage.sys.core.domain.SysRole> getUserRoles(Long userId);
Flux<Long> getUserRoleIds(Long userId);
}
@@ -2,12 +2,20 @@ package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysRole;
import cn.novalon.manage.sys.core.domain.UserRole;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
import cn.novalon.manage.sys.core.repository.IUserRoleRepository;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.command.CreateUserCommand;
import cn.novalon.manage.sys.core.command.UpdateUserCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@@ -29,14 +37,26 @@ import java.util.List;
@Service
public class SysUserService implements ISysUserService {
private static final Logger logger = LoggerFactory.getLogger(SysUserService.class);
private final ISysUserRepository userRepository;
private final ISysRoleRepository roleRepository;
private final IUserRoleRepository userRoleRepository;
private final PasswordEncoder passwordEncoder;
public SysUserService(ISysUserRepository userRepository, PasswordEncoder passwordEncoder) {
public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository;
this.passwordEncoder = passwordEncoder;
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
}
private static final BCryptPasswordEncoder directEncoder = new BCryptPasswordEncoder(12);
@Override
public Mono<SysUser> findById(Long id) {
return userRepository.findById(id);
@@ -73,7 +93,17 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<SysUser> createUser(SysUser user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}",
user.getUsername(),
user.getPassword() != null ? user.getPassword().substring(0, 7) : "null");
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")
&& !user.getPassword().startsWith("$2b$")) {
logger.info("密码不以$2a$或$2b$开头,重新编码");
user.setPassword(directEncoder.encode(user.getPassword()));
logger.info("重新编码后的密码前缀: {}", user.getPassword().substring(0, 7));
} else {
logger.info("密码已编码,跳过重新编码");
}
user.setCreatedAt(LocalDateTime.now());
if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED);
@@ -85,7 +115,7 @@ public class SysUserService implements ISysUserService {
public Mono<SysUser> createUser(CreateUserCommand command) {
SysUser user = new SysUser();
user.setUsername(command.username().getValue());
user.setPassword(passwordEncoder.encode(command.password().getValue()));
user.setPassword(directEncoder.encode(command.password().getValue()));
user.setEmail(command.email().getValue());
user.setNickname(command.nickname());
user.setPhone(command.phone());
@@ -196,4 +226,34 @@ public class SysUserService implements ISysUserService {
public Mono<Void> restoreUsers(List<Long> ids) {
return userRepository.restoreByIds(ids);
}
@Override
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
if (roleIds == null || roleIds.isEmpty()) {
return userRoleRepository.deleteByUserId(userId);
}
return userRoleRepository.deleteByUserId(userId)
.thenMany(Flux.fromIterable(roleIds))
.flatMap(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole);
})
.then();
}
@Override
public Flux<SysRole> getUserRoles(Long userId) {
return userRoleRepository.findByUserId(userId)
.flatMap(userRole -> roleRepository.findById(userRole.getRoleId()));
}
@Override
public Flux<Long> getUserRoleIds(Long userId) {
return userRoleRepository.findByUserId(userId)
.map(UserRole::getRoleId);
}
}
@@ -6,15 +6,20 @@ import cn.novalon.manage.sys.dto.response.AuthResponse;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysLoginLog;
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import cn.novalon.manage.sys.util.IpLocationParser;
import cn.novalon.manage.common.util.StatusConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
@@ -43,19 +48,40 @@ public class SysAuthHandler {
private static final Logger logger = LoggerFactory.getLogger(SysAuthHandler.class);
private final ISysUserService userService;
private final ISysUserRepository userRepository;
@SuppressWarnings("unused")
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final ISysLoginLogService loginLogService;
private final UserAgentParser userAgentParser;
private final IpLocationParser ipLocationParser;
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder,
// 使用多个编码器来支持不同的 BCrypt 版本和 strength
private static final BCryptPasswordEncoder directEncoder10 = new BCryptPasswordEncoder(10);
private static final BCryptPasswordEncoder directEncoder12 = new BCryptPasswordEncoder(12);
public SysAuthHandler(ISysUserService userService, ISysUserRepository userRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider, ISysLoginLogService loginLogService,
UserAgentParser userAgentParser) {
UserAgentParser userAgentParser, IpLocationParser ipLocationParser) {
this.userService = userService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.loginLogService = loginLogService;
this.userAgentParser = userAgentParser;
this.ipLocationParser = ipLocationParser;
logger.info("SysAuthHandler使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
// 测试编码器
String testPassword = "test123";
String testHash10 = directEncoder10.encode(testPassword);
String testHash12 = directEncoder12.encode(testPassword);
logger.info("DirectEncoder10测试: 密码={}, 哈希={}, 前缀={}",
testPassword, testHash10.substring(0, 10), testHash10.substring(0, 7));
logger.info("DirectEncoder12测试: 密码={}, 哈希={}, 前缀={}",
testPassword, testHash12.substring(0, 10), testHash12.substring(0, 7));
}
@Operation(summary = "用户登录", description = "使用用户名和密码登录系统")
@@ -73,34 +99,64 @@ public class SysAuthHandler {
String userAgent = request.headers().firstHeader("User-Agent");
return userService.findByUsername(loginRequest.getUsername())
.flatMap(user -> {
if (!passwordEncoder.matches(loginRequest.getPassword(),
// 尝试使用不同的编码器验证密码
boolean passwordMatches = false;
// 首先尝试使用 strength=12 的编码器
if (directEncoder12.matches(loginRequest.getPassword(),
user.getPassword())) {
passwordMatches = true;
logger.info("密码验证成功: 使用strength=12编码器");
}
// 如果失败,尝试使用 strength=10 的编码器
if (!passwordMatches && directEncoder10.matches(
loginRequest.getPassword(),
user.getPassword())) {
passwordMatches = true;
logger.info("密码验证成功: 使用strength=10编码器");
}
if (!passwordMatches) {
logger.warn("用户登录失败: username={}, reason=密码错误",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "密码错误", userAgent);
recordLoginLog(loginRequest.getUsername(),
clientIp, "1", "密码错误",
userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
if (user.getStatus() != 1) {
logger.warn("用户登录失败: username={}, reason=用户已禁用",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户已禁用", userAgent);
recordLoginLog(loginRequest.getUsername(),
clientIp, "1", "用户已禁用",
userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
String token = jwtTokenProvider.generateToken(
user.getUsername(), user.getId());
logger.info("用户登录成功: username={}, userId={}",
user.getUsername(), user.getId());
recordLoginLog(loginRequest.getUsername(), clientIp, "0", "登录成功", userAgent);
AuthResponse response = new AuthResponse(token,
user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
return userService.getUserRoles(user.getId())
.map(role -> role.getRoleKey())
.collectList()
.flatMap(roleKeys -> {
String token = jwtTokenProvider.generateToken(
user.getUsername(), user.getId(), roleKeys);
logger.info("用户登录成功: username={}, userId={}, roles={}",
user.getUsername(), user.getId(), roleKeys);
recordLoginLog(loginRequest.getUsername(), clientIp,
"0", "登录成功", userAgent);
AuthResponse response = new AuthResponse(token,
user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
});
})
.switchIfEmpty(Mono.defer(() -> {
logger.warn("用户登录失败: username={}, reason=用户不存在",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户不存在", userAgent);
recordLoginLog(loginRequest.getUsername(), clientIp,
"1", "用户不存在", userAgent);
return Mono.error(new RuntimeException("用户名或密码错误"));
}));
})
@@ -140,17 +196,19 @@ public class SysAuthHandler {
SysLoginLog loginLog = new SysLoginLog();
loginLog.setUsername(username);
loginLog.setIp(ip);
loginLog.setLocation(ipLocationParser.parseLocation(ip));
loginLog.setStatus(status);
loginLog.setMessage(message);
loginLog.setLoginTime(LocalDateTime.now());
if (userAgent != null && !userAgent.isEmpty()) {
loginLog.setBrowser(userAgentParser.parseBrowser(userAgent));
loginLog.setOs(userAgentParser.parseOS(userAgent));
}
loginLogService.save(loginLog)
.doOnSuccess(saved -> logger.debug("登录日志记录成功: username={}, status={}", username, status))
.doOnSuccess(saved -> logger.debug("登录日志记录成功: username={}, status={}", username,
status))
.doOnError(error -> logger.error("登录日志记录失败: {}", error.getMessage()))
.subscribe();
} catch (Exception e) {
@@ -186,8 +244,16 @@ public class SysAuthHandler {
registerRequest.getUsername(), registerRequest.getEmail());
SysUser user = new SysUser();
user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
String encodedPassword = directEncoder12.encode(registerRequest.getPassword());
logger.info("密码编码结果: {} (前缀: {})",
encodedPassword.substring(0, 10),
encodedPassword.substring(0, 7));
user.setPassword(encodedPassword);
user.setEmail(registerRequest.getEmail());
user.setCreatedAt(LocalDateTime.now());
if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED);
}
return userService.findByUsername(registerRequest.getUsername())
.flatMap(existing -> {
logger.warn("用户注册失败: username={}, reason=用户名已存在",
@@ -195,11 +261,13 @@ public class SysAuthHandler {
return Mono.<ServerResponse>error(
new RuntimeException("用户名已存在"));
})
.switchIfEmpty(userService.createUser(user)
.switchIfEmpty(userRepository.save(user)
.flatMap(u -> {
logger.info("用户注册成功: username={}, userId={}",
logger.info("用户注册成功: username={}, userId={}, password={}",
u.getUsername(),
u.getId());
u.getId(),
u.getPassword().substring(
0, 10));
return ServerResponse
.status(HttpStatus.CREATED)
.bodyValue(u);
@@ -16,7 +16,10 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map;
/**
* 用户处理器
@@ -74,7 +77,25 @@ public class SysUserHandler {
public Mono<ServerResponse> getUserById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.findById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.flatMap(user -> {
return userService.getUserRoleIds(id)
.collectList()
.map(roleIds -> {
Map<String, Object> userWithRoles = new HashMap<>();
userWithRoles.put("id", user.getId());
userWithRoles.put("username", user.getUsername());
userWithRoles.put("nickname", user.getNickname());
userWithRoles.put("email", user.getEmail());
userWithRoles.put("phone", user.getPhone());
userWithRoles.put("avatar", user.getAvatar());
userWithRoles.put("status", user.getStatus());
userWithRoles.put("roles", roleIds);
userWithRoles.put("createdAt", user.getCreatedAt());
userWithRoles.put("updatedAt", user.getUpdatedAt());
return userWithRoles;
});
})
.flatMap(userWithRoles -> ServerResponse.ok().bodyValue(userWithRoles))
.switchIfEmpty(ServerResponse.notFound().build());
}
@@ -127,8 +148,16 @@ public class SysUserHandler {
@Operation(summary = "删除用户", description = "物理删除用户")
public Mono<ServerResponse> deleteUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.deleteUser(id)
.then(ServerResponse.noContent().build());
return userService.findById(id)
.flatMap(user -> userService.deleteUser(id)
.then(ServerResponse.noContent().build()))
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.onErrorResume(RuntimeException.class, ex -> {
if (ex.getMessage().contains("not found")) {
return ServerResponse.notFound().build();
}
return Mono.error(ex);
});
}
@Operation(summary = "修改密码", description = "修改用户密码")
@@ -142,8 +171,16 @@ public class SysUserHandler {
@Operation(summary = "逻辑删除用户", description = "逻辑删除单个用户")
public Mono<ServerResponse> logicalDeleteUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.logicalDeleteUser(id)
.then(ServerResponse.noContent().build());
return userService.findById(id)
.flatMap(user -> userService.logicalDeleteUser(id)
.then(ServerResponse.noContent().build()))
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.onErrorResume(RuntimeException.class, ex -> {
if (ex.getMessage().contains("not found")) {
return ServerResponse.notFound().build();
}
return Mono.error(ex);
});
}
@Operation(summary = "批量逻辑删除用户", description = "批量逻辑删除多个用户")
@@ -158,7 +195,13 @@ public class SysUserHandler {
public Mono<ServerResponse> restoreUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.restoreUser(id)
.then(ServerResponse.noContent().build());
.then(ServerResponse.noContent().build())
.onErrorResume(RuntimeException.class, ex -> {
if (ex.getMessage().contains("not found")) {
return ServerResponse.notFound().build();
}
return Mono.error(ex);
});
}
@Operation(summary = "批量恢复用户", description = "批量恢复被逻辑删除的用户")
@@ -182,4 +225,20 @@ public class SysUserHandler {
return userService.existsByEmail(email)
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
@Operation(summary = "为用户分配角色", description = "为指定用户分配角色列表")
public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(new org.springframework.core.ParameterizedTypeReference<List<Long>>() {
})
.flatMap(roleIds -> userService.assignRolesToUser(id, roleIds))
.then(ServerResponse.ok().build());
}
@Operation(summary = "获取用户的角色", description = "根据用户ID获取该用户拥有的所有角色")
public Mono<ServerResponse> getUserRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return ServerResponse.ok()
.body(userService.getUserRoles(id), cn.novalon.manage.sys.core.domain.SysRole.class);
}
}
@@ -13,6 +13,9 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 操作日志过滤器
*
@@ -29,8 +32,11 @@ public class OperationLogFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(OperationLogFilter.class);
private final IOperationLogService logService;
private final ObjectMapper objectMapper;
public OperationLogFilter(IOperationLogService logService, ObjectMapper objectMapper) {
this.logService = logService;
this.objectMapper = objectMapper;
}
@Override
@@ -91,8 +97,14 @@ public class OperationLogFilter implements WebFilter {
log.setResult("Success");
}
String queryParams = exchange.getRequest().getQueryParams().toSingleValueMap().toString();
log.setParams(queryParams);
Map<String, String> queryParams = new LinkedHashMap<>(exchange.getRequest().getQueryParams().toSingleValueMap());
String formattedParams;
try {
formattedParams = objectMapper.writeValueAsString(queryParams);
} catch (Exception e) {
formattedParams = queryParams.toString();
}
log.setParams(formattedParams);
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志记录成功: {}", log.getOperation()))
@@ -11,7 +11,8 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT认证过滤器
@@ -33,14 +34,23 @@ public class JwtAuthenticationFilter implements WebFilter {
String token = extractToken(exchange.getRequest());
if (token != null && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getUserIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
Long userId = jwtTokenProvider.getUserIdFromToken(token);
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
if (authorities.isEmpty()) {
authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
authorities
);
return chain.filter(exchange)
@@ -45,6 +45,21 @@ public class JwtTokenProvider {
.compact();
}
public String generateToken(String username, Long userId, java.util.List<String> roles) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("roles", roles);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
.signWith(getSigningKey())
.compact();
}
public Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
@@ -61,6 +76,15 @@ public class JwtTokenProvider {
return getClaimsFromToken(token).get("userId", Long.class);
}
@SuppressWarnings("unchecked")
public java.util.List<String> getRolesFromToken(String token) {
Object roles = getClaimsFromToken(token).get("roles");
if (roles instanceof java.util.List) {
return (java.util.List<String>) roles;
}
return java.util.Collections.emptyList();
}
public boolean validateToken(String token) {
try {
getClaimsFromToken(token);
@@ -0,0 +1,72 @@
package cn.novalon.manage.sys.util;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* IP地址解析工具类
*
* 用于解析IP地址,获取地理位置信息
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class IpLocationParser {
private static final Map<String, String> IP_LOCATION_CACHE = new HashMap<>();
static {
IP_LOCATION_CACHE.put("127.0.0.1", "本地");
IP_LOCATION_CACHE.put("0:0:0:0:0:0:0:1", "本地");
IP_LOCATION_CACHE.put("localhost", "本地");
}
public String parseLocation(String ip) {
if (ip == null || ip.isEmpty()) {
return "未知位置";
}
if (IP_LOCATION_CACHE.containsKey(ip)) {
return IP_LOCATION_CACHE.get(ip);
}
if (isInternalIp(ip)) {
return "内网";
}
return "未知位置";
}
private boolean isInternalIp(String ip) {
if (ip == null || ip.isEmpty()) {
return false;
}
String[] parts = ip.split("\\.");
if (parts.length != 4) {
return false;
}
try {
int first = Integer.parseInt(parts[0]);
int second = Integer.parseInt(parts[1]);
if (first == 10) {
return true;
}
if (first == 172 && second >= 16 && second <= 31) {
return true;
}
if (first == 192 && second == 168) {
return true;
}
} catch (NumberFormatException e) {
return false;
}
return false;
}
}
@@ -17,12 +17,7 @@ import java.util.regex.Pattern;
public class UserAgentParser {
private static final Pattern BROWSER_PATTERN = Pattern.compile(
"(Chrome|Firefox|Safari|Edge|MSIE|Trident|Opera)[/\\s]([\\d.]+)"
);
private static final Pattern OS_PATTERN = Pattern.compile(
"(Windows NT|Mac OS X|Linux|Android|iPhone|iPad|iPod)[\\s/_-]?([\\d._]+)?"
);
"(Chrome|Firefox|Safari|Edge|MSIE|Trident|Opera)[/\\s]([\\d.]+)");
/**
* 解析User-Agent字符串,返回浏览器信息
@@ -55,7 +50,7 @@ public class UserAgentParser {
}
String ua = userAgent;
if (ua.contains("Windows NT 10.0")) {
return "Windows 10";
} else if (ua.contains("Windows NT 6.3")) {
@@ -1,95 +0,0 @@
-- 系统菜单初始化数据
-- @author 张翔
-- @date 2026-03-24
-- 清空现有菜单数据
DELETE FROM sys_menu WHERE id > 0;
-- 一级菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, 0, '系统管理', 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, 0, '审计日志', 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, 0, '系统监控', 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, 1, '用户管理', 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, 1, '角色管理', 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, 1, '菜单管理', 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, 1, '部门管理', 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, 1, '字典管理', 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, 1, '参数管理', 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, 1, '通知公告', 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, 1, '文件管理', 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, 11, '用户查询', 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, 11, '用户新增', 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, 11, '用户修改', 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, 11, '用户删除', 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, 11, '用户导出', 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, 11, '用户导入', 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, 11, '重置密码', 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, 12, '角色查询', 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, 12, '角色新增', 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, 12, '角色修改', 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, 12, '角色删除', 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, 12, '角色导出', 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, 13, '菜单查询', 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, 13, '菜单新增', 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, 13, '菜单修改', 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, 13, '菜单删除', 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, 2, '操作日志', 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, 2, '登录日志', 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, 2, '异常日志', 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, 21, '操作查询', 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, 21, '操作删除', 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, 21, '操作导出', 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, 22, '登录查询', 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, 22, '登录删除', 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, 22, '登录导出', 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, 23, '异常查询', 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, 23, '异常删除', 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, 23, '异常导出', 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, 3, '在线用户', 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, 3, '定时任务', 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, 3, '数据监控', 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, 3, '服务监控', 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, 3, '缓存监控', 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, 31, '在线查询', 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, 31, '在线强退', 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, 32, '任务查询', 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, 32, '任务新增', 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, 32, '任务修改', 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, 32, '任务删除', 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, 32, '任务执行', 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
COMMIT;
@@ -36,6 +36,12 @@ class SysUserServiceTest {
@Mock
private ISysUserRepository userRepository;
@Mock
private cn.novalon.manage.sys.core.repository.ISysRoleRepository roleRepository;
@Mock
private cn.novalon.manage.sys.core.repository.IUserRoleRepository userRoleRepository;
@Mock
private PasswordEncoder passwordEncoder;
@@ -45,7 +51,7 @@ class SysUserServiceTest {
@BeforeEach
void setUp() {
userService = new SysUserService(userRepository, passwordEncoder);
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
testUser = new SysUser();
testUser.setId(1L);
@@ -7,6 +7,7 @@ import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import cn.novalon.manage.sys.util.IpLocationParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -30,6 +31,9 @@ class SysAuthHandlerTest {
@Mock
private ISysUserService userService;
@Mock
private cn.novalon.manage.sys.core.repository.ISysUserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@@ -42,13 +46,16 @@ class SysAuthHandlerTest {
@Mock
private UserAgentParser userAgentParser;
@Mock
private IpLocationParser ipLocationParser;
private SysAuthHandler authHandler;
private SysUser testUser;
@BeforeEach
void setUp() {
authHandler = new SysAuthHandler(userService, passwordEncoder, jwtTokenProvider, loginLogService,
userAgentParser);
authHandler = new SysAuthHandler(userService, userRepository, passwordEncoder, jwtTokenProvider, loginLogService,
userAgentParser, ipLocationParser);
testUser = new SysUser();
testUser.setId(1L);
@@ -16,8 +16,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@@ -1,6 +1,5 @@
package cn.novalon.manage.sys.util;
import cn.novalon.manage.sys.util.UserAgentParser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;