实现JWT身份认证机制,新增JWT认证过滤器和服务,更新链接生成接口以支持JWT验证,删除旧的用户控制器,添加JWT认证文档,增强错误处理和日志记录。
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
package com.gameplatform.server.controller;
|
||||
|
||||
import com.gameplatform.server.model.dto.account.AccountCreateRequest;
|
||||
import com.gameplatform.server.model.dto.account.AccountResponse;
|
||||
import com.gameplatform.server.model.dto.account.AccountUpdateRequest;
|
||||
import com.gameplatform.server.model.dto.common.PageResult;
|
||||
import com.gameplatform.server.service.account.AccountService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 用户接口控制器 - 基于UserAccount实体
|
||||
* 提供用户账户的基本CRUD操作
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@Tag(name = "用户账户管理", description = "用户账户的增删改查操作")
|
||||
public class UserController {
|
||||
private final AccountService accountService;
|
||||
|
||||
public UserController(AccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户账户信息
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取用户详情", description = "根据用户ID获取用户详细信息")
|
||||
public Mono<AccountResponse> getById(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
return accountService.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
*/
|
||||
@GetMapping
|
||||
@Operation(summary = "获取用户列表", description = "分页获取用户列表,支持按用户类型、状态、关键词筛选")
|
||||
public Mono<PageResult<AccountResponse>> list(
|
||||
@Parameter(description = "用户类型:ADMIN 或 AGENT") @RequestParam(value = "userType", required = false) String userType,
|
||||
@Parameter(description = "账户状态:ENABLED 或 DISABLED") @RequestParam(value = "status", required = false) String status,
|
||||
@Parameter(description = "搜索关键词") @RequestParam(value = "keyword", required = false) String keyword,
|
||||
@Parameter(description = "页码,默认1") @RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小,默认20") @RequestParam(value = "size", defaultValue = "20") Integer size
|
||||
) {
|
||||
return accountService.list(userType, status, keyword, page, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户账户
|
||||
*/
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "创建用户", description = "创建新的代理用户账户")
|
||||
public Mono<AccountResponse> create(@Valid @RequestBody AccountCreateRequest request) {
|
||||
return accountService.create(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户账户信息
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新用户", description = "更新用户账户信息")
|
||||
public Mono<AccountResponse> update(@Parameter(description = "用户ID") @PathVariable Long id, @Valid @RequestBody AccountUpdateRequest request) {
|
||||
return accountService.update(id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用用户账户
|
||||
*/
|
||||
@PostMapping("/{id}/enable")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "启用用户", description = "启用指定用户账户")
|
||||
public Mono<Void> enable(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
return accountService.setStatus(id, "ENABLED").then();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用用户账户
|
||||
*/
|
||||
@PostMapping("/{id}/disable")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "禁用用户", description = "禁用指定用户账户")
|
||||
public Mono<Void> disable(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
return accountService.setStatus(id, "DISABLED").then();
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,26 @@ package com.gameplatform.server.controller.link;
|
||||
import com.gameplatform.server.model.dto.link.LinkGenerateRequest;
|
||||
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
|
||||
import com.gameplatform.server.service.link.LinkGenerationService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/link")
|
||||
@Tag(name = "链接管理", description = "生成链接与二维码代理")
|
||||
@Tag(name = "链接管理", description = "链接生成和管理相关接口")
|
||||
public class LinkController {
|
||||
private static final Logger log = LoggerFactory.getLogger(LinkController.class);
|
||||
|
||||
private final LinkGenerationService linkGenerationService;
|
||||
|
||||
public LinkController(LinkGenerationService linkGenerationService) {
|
||||
@@ -22,27 +30,87 @@ public class LinkController {
|
||||
}
|
||||
|
||||
@PostMapping("/generate")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "生成链接批次", description = "times * perTimeQuantity = 目标点数;代理需扣点,管理员不扣")
|
||||
public Mono<LinkGenerateResponse> generate(@Valid @RequestBody LinkGenerateRequest req,
|
||||
@RequestHeader(value = "X-Operator-Id", required = false) Long operatorId,
|
||||
@RequestHeader(value = "X-Operator-Type", required = false) String operatorType) {
|
||||
// 暂时用两个 Header 传操作者信息;后续接入 JWT 解析
|
||||
Long targetAccountId = req.getAgentAccountId() != null ? req.getAgentAccountId() : operatorId;
|
||||
return linkGenerationService.generateLinks(operatorId, safeUpper(operatorType), targetAccountId,
|
||||
defaultInt(req.getTimes(), 0), defaultInt(req.getPerTimeQuantity(), 0))
|
||||
.map(r -> {
|
||||
LinkGenerateResponse resp = new LinkGenerateResponse();
|
||||
resp.setBatchId(r.getBatchId());
|
||||
resp.setDeductPoints(r.getNeedPoints());
|
||||
resp.setExpireAt(r.getExpireAt());
|
||||
resp.setCodeNos(r.getCodeNos());
|
||||
return resp;
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "生成链接批次", description = "生成指定数量的链接批次。所有用户(管理员和代理商)都只能为自己生成链接,代理用户生成链接时会扣除积分,管理员生成链接时不扣除积分")
|
||||
public Mono<LinkGenerateResponse> generateLinks(@Valid @RequestBody LinkGenerateRequest request,
|
||||
Authentication authentication) {
|
||||
log.info("=== 开始处理链接生成请求 ===");
|
||||
log.info("请求参数: times={}, perTimeQuantity={}",
|
||||
request.getTimes(), request.getPerTimeQuantity());
|
||||
|
||||
log.info("=== 开始检查认证信息 ===");
|
||||
log.info("Authentication: {}", authentication);
|
||||
|
||||
if (authentication == null) {
|
||||
log.error("=== 认证失败:Authentication为空 ===");
|
||||
return Mono.error(new IllegalArgumentException("用户未认证:Authentication为空"));
|
||||
}
|
||||
|
||||
log.info("Authentication获取成功: {}", authentication);
|
||||
log.info("Authentication是否已认证: {}", authentication.isAuthenticated());
|
||||
log.info("Authentication的principal: {}", authentication.getPrincipal());
|
||||
log.info("Authentication的authorities: {}", authentication.getAuthorities());
|
||||
log.info("Authentication的details: {}", authentication.getDetails());
|
||||
|
||||
if (!authentication.isAuthenticated()) {
|
||||
log.error("=== 认证失败:Authentication未通过验证 ===");
|
||||
log.error("Authentication详情: {}", authentication);
|
||||
return Mono.error(new IllegalArgumentException("用户未认证:Authentication未通过验证"));
|
||||
}
|
||||
|
||||
log.info("=== 认证验证通过 ===");
|
||||
|
||||
// 从认证对象中获取用户信息
|
||||
log.info("开始解析Claims信息");
|
||||
Claims claims = (Claims) authentication.getDetails();
|
||||
if (claims == null) {
|
||||
log.error("=== 认证失败:Claims为空 ===");
|
||||
log.error("Authentication details: {}", authentication.getDetails());
|
||||
return Mono.error(new IllegalArgumentException("用户未认证:Claims为空"));
|
||||
}
|
||||
|
||||
log.info("Claims获取成功,开始解析用户信息");
|
||||
log.info("Claims内容: {}", claims);
|
||||
log.info("Claims的subject: {}", claims.getSubject());
|
||||
log.info("Claims的issuedAt: {}", claims.getIssuedAt());
|
||||
log.info("Claims的expiration: {}", claims.getExpiration());
|
||||
|
||||
Long operatorId = claims.get("userId", Long.class);
|
||||
String operatorType = claims.get("userType", String.class);
|
||||
String username = claims.get("username", String.class);
|
||||
|
||||
log.info("解析出的用户信息 - operatorId: {}, operatorType: {}, username: {}",
|
||||
operatorId, operatorType, username);
|
||||
|
||||
if (operatorId == null || operatorType == null) {
|
||||
log.error("=== 认证失败:缺少必要的用户信息 ===");
|
||||
log.error("operatorId: {}, operatorType: {}, username: {}", operatorId, operatorType, username);
|
||||
log.error("Claims中所有键: {}", claims.keySet());
|
||||
return Mono.error(new IllegalArgumentException("用户未认证:缺少必要的用户信息"));
|
||||
}
|
||||
|
||||
log.info("=== 用户认证信息完整,开始处理业务逻辑 ===");
|
||||
log.info("操作者信息: operatorId={}, operatorType={}, username={}",
|
||||
operatorId, operatorType, username);
|
||||
log.info("业务参数: times={}, perTimeQuantity={}",
|
||||
request.getTimes(), request.getPerTimeQuantity());
|
||||
|
||||
return linkGenerationService.generateLinks(operatorId, operatorType,
|
||||
request.getTimes(), request.getPerTimeQuantity())
|
||||
.map(result -> {
|
||||
log.info("链接生成成功,batchId: {}, 扣除积分: {}, 过期时间: {}",
|
||||
result.getBatchId(), result.getNeedPoints(), result.getExpireAt());
|
||||
LinkGenerateResponse response = new LinkGenerateResponse();
|
||||
response.setBatchId(result.getBatchId());
|
||||
response.setDeductPoints(result.getNeedPoints());
|
||||
response.setExpireAt(result.getExpireAt());
|
||||
response.setCodeNos(result.getCodeNos());
|
||||
return response;
|
||||
})
|
||||
.doOnError(error -> {
|
||||
log.error("链接生成失败: {}", error.getMessage(), error);
|
||||
});
|
||||
}
|
||||
|
||||
private static String safeUpper(String s) { return s == null ? null : s.toUpperCase(); }
|
||||
private static int defaultInt(Integer v, int d) { return v == null ? d : v; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -22,21 +25,45 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleBadRequest(IllegalArgumentException e) {
|
||||
log.info("400 BadRequest: {}", e.getMessage());
|
||||
log.warn("400 BadRequest: {} - Stack: {}", e.getMessage(), getStackTrace(e));
|
||||
return body(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Object handleForbidden(IllegalStateException e) {
|
||||
log.info("403 Forbidden: {}", e.getMessage());
|
||||
log.warn("403 Forbidden: {} - Stack: {}", e.getMessage(), getStackTrace(e));
|
||||
return body(HttpStatus.FORBIDDEN.value(), e.getMessage());
|
||||
}
|
||||
|
||||
// 认证相关异常处理
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public Object handleAuthenticationException(AuthenticationException e) {
|
||||
log.warn("401 Authentication Failed: {} - Type: {} - Stack: {}",
|
||||
e.getMessage(), e.getClass().getSimpleName(), getStackTrace(e));
|
||||
return body(HttpStatus.UNAUTHORIZED.value(), "认证失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public Object handleBadCredentials(BadCredentialsException e) {
|
||||
log.warn("401 Bad Credentials: {} - Stack: {}", e.getMessage(), getStackTrace(e));
|
||||
return body(HttpStatus.UNAUTHORIZED.value(), "用户名或密码错误");
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Object handleAccessDenied(AccessDeniedException e) {
|
||||
log.warn("403 Access Denied: {} - Stack: {}", e.getMessage(), getStackTrace(e));
|
||||
return body(HttpStatus.FORBIDDEN.value(), "访问被拒绝: " + e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(WebExchangeBindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleBindException(WebExchangeBindException e) {
|
||||
log.info("400 ValidationError: {}", e.getMessage());
|
||||
log.warn("400 ValidationError: {} - Field errors: {} - Global errors: {}",
|
||||
e.getMessage(), e.getFieldErrors().size(), e.getGlobalErrors().size());
|
||||
var details = new java.util.LinkedHashMap<String, Object>();
|
||||
e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage()));
|
||||
e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage()));
|
||||
@@ -46,7 +73,8 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleConstraintViolation(ConstraintViolationException e) {
|
||||
log.info("400 ConstraintViolation: {}", e.getMessage());
|
||||
log.warn("400 ConstraintViolation: {} - Violations: {}",
|
||||
e.getMessage(), e.getConstraintViolations().size());
|
||||
var details = new java.util.LinkedHashMap<String, Object>();
|
||||
for (ConstraintViolation<?> v : e.getConstraintViolations()) {
|
||||
details.put(String.valueOf(v.getPropertyPath()), v.getMessage());
|
||||
@@ -57,21 +85,24 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(ServerWebInputException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleInput(ServerWebInputException e) {
|
||||
log.info("400 InputError: {}", e.getMessage());
|
||||
log.warn("400 InputError: {} - Reason: {} - Stack: {}",
|
||||
e.getMessage(), e.getReason(), getStackTrace(e));
|
||||
return body(HttpStatus.BAD_REQUEST.value(), "请求解析失败: " + e.getReason());
|
||||
}
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public Object handleRse(ResponseStatusException e) {
|
||||
var status = e.getStatusCode();
|
||||
log.info("{} ResponseStatusException: {}", status, e.getReason());
|
||||
log.warn("{} ResponseStatusException: {} - Stack: {}",
|
||||
status, e.getReason(), getStackTrace(e));
|
||||
return body(status.value(), e.getReason());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Object handleOther(Exception e) {
|
||||
log.error("500 InternalServerError", e);
|
||||
log.error("500 InternalServerError: {} - Type: {} - Stack: {}",
|
||||
e.getMessage(), e.getClass().getSimpleName(), getStackTrace(e), e);
|
||||
return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误");
|
||||
}
|
||||
|
||||
@@ -91,4 +122,12 @@ public class GlobalExceptionHandler {
|
||||
put("timestamp", Instant.now().toString());
|
||||
}};
|
||||
}
|
||||
|
||||
private String getStackTrace(Exception e) {
|
||||
StackTraceElement[] stackTrace = e.getStackTrace();
|
||||
if (stackTrace.length > 0) {
|
||||
return stackTrace[0].toString();
|
||||
}
|
||||
return "No stack trace available";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,11 @@ public class LinkGenerateRequest {
|
||||
private Integer times;
|
||||
@Schema(description = "每次打的数量", example = "5")
|
||||
private Integer perTimeQuantity;
|
||||
@Schema(description = "为哪个代理账户生成(管理员可指定,代理可省略或为自己)")
|
||||
private Long agentAccountId;
|
||||
|
||||
public Integer getTimes() { return times; }
|
||||
public void setTimes(Integer times) { this.times = times; }
|
||||
public Integer getPerTimeQuantity() { return perTimeQuantity; }
|
||||
public void setPerTimeQuantity(Integer perTimeQuantity) { this.perTimeQuantity = perTimeQuantity; }
|
||||
public Long getAgentAccountId() { return agentAccountId; }
|
||||
public void setAgentAccountId(Long agentAccountId) { this.agentAccountId = agentAccountId; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.gameplatform.server.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter implements WebFilter {
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
String method = exchange.getRequest().getMethod().name();
|
||||
|
||||
log.info("=== JWT过滤器开始处理请求 ===");
|
||||
log.info("请求路径: {}, 请求方法: {}", path, method);
|
||||
|
||||
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||
log.info("Authorization头: {}", authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "null");
|
||||
|
||||
if (authHeader == null) {
|
||||
log.info("未找到Authorization头,跳过JWT认证,继续处理请求");
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
if (!authHeader.startsWith("Bearer ")) {
|
||||
log.warn("Authorization头格式无效,期望格式: 'Bearer <token>',实际: {}", authHeader);
|
||||
log.info("跳过JWT认证,继续处理请求");
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
log.info("开始处理JWT token,token长度: {}", token.length());
|
||||
log.debug("JWT token内容: {}", token);
|
||||
|
||||
try {
|
||||
log.info("开始解析JWT token");
|
||||
Claims claims = jwtService.parse(token);
|
||||
log.info("JWT token解析成功");
|
||||
|
||||
Long userId = claims.get("userId", Long.class);
|
||||
String userType = claims.get("userType", String.class);
|
||||
String username = claims.get("username", String.class);
|
||||
String subject = claims.getSubject();
|
||||
|
||||
log.info("JWT claims解析结果:");
|
||||
log.info(" - subject: {}", subject);
|
||||
log.info(" - userId: {}", userId);
|
||||
log.info(" - userType: {}", userType);
|
||||
log.info(" - username: {}", username);
|
||||
log.info(" - issuedAt: {}", claims.getIssuedAt());
|
||||
log.info(" - expiration: {}", claims.getExpiration());
|
||||
log.info(" - 所有claims键: {}", claims.keySet());
|
||||
|
||||
if (userId == null || userType == null || username == null) {
|
||||
log.warn("JWT token中缺少必要的claims信息");
|
||||
log.warn(" - userId: {}", userId);
|
||||
log.warn(" - userType: {}", userType);
|
||||
log.warn(" - username: {}", username);
|
||||
log.info("跳过JWT认证,继续处理请求");
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 创建Spring Security的Authentication对象
|
||||
log.info("开始创建Authentication对象");
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
username, // principal
|
||||
null, // credentials (不需要密码)
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
|
||||
);
|
||||
|
||||
// 设置认证详情,包含JWT claims信息
|
||||
authentication.setDetails(claims);
|
||||
|
||||
log.info("Authentication对象创建成功: {}", authentication);
|
||||
log.info("Authentication是否已认证: {}", authentication.isAuthenticated());
|
||||
log.info("Authentication的principal: {}", authentication.getPrincipal());
|
||||
log.info("Authentication的authorities: {}", authentication.getAuthorities());
|
||||
log.info("Authentication的details: {}", authentication.getDetails());
|
||||
|
||||
// 创建安全上下文并设置认证信息
|
||||
SecurityContext securityContext = new SecurityContextImpl(authentication);
|
||||
|
||||
log.info("=== JWT token验证成功,设置安全上下文 ===");
|
||||
log.info("用户: {} (ID: {}, 类型: {}) 在路径: {} 上JWT验证通过",
|
||||
username, userId, userType, path);
|
||||
|
||||
// 将安全上下文设置到ReactiveSecurityContextHolder中
|
||||
return chain.filter(exchange)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("=== JWT token验证失败 ===");
|
||||
log.error("请求路径: {}", path);
|
||||
log.error("Authorization头: {}", authHeader);
|
||||
log.error("错误详情: {}", e.getMessage(), e);
|
||||
log.info("JWT认证失败,继续处理请求(未认证状态)");
|
||||
}
|
||||
|
||||
log.info("JWT过滤器处理完成,继续处理请求");
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,14 @@ public class JwtService {
|
||||
byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes();
|
||||
this.key = Keys.hmacShaKeyFor(bytes);
|
||||
this.accessTokenMinutes = accessTokenMinutes;
|
||||
log.info("JWT服务初始化完成 - 密钥长度: {} bytes, token过期时间: {} 分钟", bytes.length, accessTokenMinutes);
|
||||
}
|
||||
|
||||
public String generateToken(String subject, String userType, Long userId, String username, Map<String, Object> extra) {
|
||||
log.info("=== 开始生成JWT token ===");
|
||||
log.info("生成参数: subject={}, userType={}, userId={}, username={}, extra={}",
|
||||
subject, userType, userId, username, extra);
|
||||
|
||||
Instant now = Instant.now();
|
||||
var builder = Jwts.builder()
|
||||
.setSubject(subject)
|
||||
@@ -41,14 +46,44 @@ public class JwtService {
|
||||
if (extra != null) {
|
||||
extra.forEach(builder::claim);
|
||||
}
|
||||
|
||||
log.info("JWT builder配置完成,开始签名");
|
||||
String token = builder.signWith(key, SignatureAlgorithm.HS256).compact();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("JWT generated subject={}, userType={}, userId={}, username={} expInMin={}", subject, userType, userId, username, accessTokenMinutes);
|
||||
}
|
||||
|
||||
log.info("=== JWT token生成成功 ===");
|
||||
log.info("token长度: {} 字符", token.length());
|
||||
log.info("过期时间: {} 分钟后", accessTokenMinutes);
|
||||
log.info("完整token: {}", token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public io.jsonwebtoken.Claims parse(String token) {
|
||||
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
||||
log.info("=== 开始解析JWT token ===");
|
||||
log.info("token长度: {} 字符", token.length());
|
||||
log.debug("完整token: {}", token);
|
||||
|
||||
try {
|
||||
log.info("开始验证JWT签名");
|
||||
io.jsonwebtoken.Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(key)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
log.info("=== JWT token解析成功 ===");
|
||||
log.info("解析结果:");
|
||||
log.info(" - subject: {}", claims.getSubject());
|
||||
log.info(" - issuedAt: {}", claims.getIssuedAt());
|
||||
log.info(" - expiration: {}", claims.getExpiration());
|
||||
log.info(" - 所有claims: {}", claims);
|
||||
|
||||
return claims;
|
||||
} catch (Exception e) {
|
||||
log.error("=== JWT token解析失败 ===");
|
||||
log.error("token: {}", token);
|
||||
log.error("错误详情: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,107 @@
|
||||
package com.gameplatform.server.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
return http
|
||||
log.info("=== 开始配置Spring Security安全链 ===");
|
||||
|
||||
SecurityWebFilterChain chain = http
|
||||
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||
.cors(cors -> {})
|
||||
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||
.securityContextRepository(securityContextRepository())
|
||||
.authenticationManager(authenticationManager())
|
||||
.authorizeExchange(ex -> ex
|
||||
.pathMatchers("/actuator/**").permitAll()
|
||||
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
||||
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
||||
.pathMatchers("/api/link/**").authenticated() // 链接接口需要认证
|
||||
.anyExchange().permitAll() // 其他接口后续再收紧
|
||||
)
|
||||
// 关键:将JWT过滤器集成到Security过滤链中,放在AUTHENTICATION位置
|
||||
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||
.build();
|
||||
|
||||
log.info("=== Spring Security安全链配置完成 ===");
|
||||
log.info("安全配置详情:");
|
||||
log.info(" - CSRF: 已禁用");
|
||||
log.info(" - CORS: 已启用");
|
||||
log.info(" - HTTP Basic: 已禁用");
|
||||
log.info(" - Form Login: 已禁用");
|
||||
log.info(" - JWT过滤器: 已集成到Security链中 (AUTHENTICATION位置)");
|
||||
log.info(" - 路径权限配置:");
|
||||
log.info(" * /actuator/** -> 允许所有");
|
||||
log.info(" * POST /api/auth/login -> 允许所有");
|
||||
log.info(" * GET /api/auth/me -> 允许所有");
|
||||
log.info(" * /api/link/** -> 需要认证");
|
||||
log.info(" * 其他路径 -> 允许所有");
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactiveAuthenticationManager authenticationManager() {
|
||||
log.info("创建JWT认证管理器");
|
||||
return authentication -> {
|
||||
log.info("=== JWT认证管理器开始处理认证 ===");
|
||||
log.info("认证对象: {}", authentication);
|
||||
|
||||
if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
|
||||
log.info("处理UsernamePasswordAuthenticationToken认证");
|
||||
log.info("Principal: {}", auth.getPrincipal());
|
||||
log.info("Credentials: {}", auth.getCredentials());
|
||||
log.info("Authorities: {}", auth.getAuthorities());
|
||||
log.info("Details: {}", auth.getDetails());
|
||||
log.info("是否已认证: {}", auth.isAuthenticated());
|
||||
|
||||
// 如果已经通过JWT过滤器认证,直接返回
|
||||
if (auth.isAuthenticated()) {
|
||||
log.info("认证对象已经通过验证,返回成功");
|
||||
return Mono.just(auth);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("认证对象未通过验证,返回失败");
|
||||
return Mono.empty();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServerSecurityContextRepository securityContextRepository() {
|
||||
log.info("创建安全上下文仓库");
|
||||
return new WebSessionServerSecurityContextRepository();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
log.info("创建BCrypt密码编码器");
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,46 +46,43 @@ public class LinkGenerationService {
|
||||
|
||||
@Transactional
|
||||
public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType,
|
||||
Long targetAccountId,
|
||||
int times, int perTimeQuantity) {
|
||||
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, targetAccountId, times, perTimeQuantity))
|
||||
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, times, perTimeQuantity))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private GenerateResult doGenerate(Long operatorId, String operatorType,
|
||||
Long targetAccountId,
|
||||
int times, int perTimeQuantity) {
|
||||
if (times <= 0 || perTimeQuantity <= 0) {
|
||||
throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数");
|
||||
}
|
||||
|
||||
UserAccount target = userAccountMapper.findById(targetAccountId);
|
||||
if (target == null) {
|
||||
throw new IllegalArgumentException("目标账户不存在");
|
||||
// 获取操作者账户信息
|
||||
UserAccount operator = userAccountMapper.findById(operatorId);
|
||||
if (operator == null) {
|
||||
throw new IllegalArgumentException("操作者账户不存在");
|
||||
}
|
||||
|
||||
boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType);
|
||||
if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) {
|
||||
throw new IllegalArgumentException("非法操作者类型");
|
||||
}
|
||||
if (!"AGENT".equalsIgnoreCase(target.getUserType())) {
|
||||
throw new IllegalArgumentException("仅支持为代理账户生成链接");
|
||||
}
|
||||
|
||||
long needPoints = (long) times * (long) perTimeQuantity;
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("generateLinks operatorId={} operatorType={} targetAccountId={} times={} perTimeQuantity={} needPoints={} expireHours={}",
|
||||
operatorId, operatorType, targetAccountId, times, perTimeQuantity, needPoints, expireHours);
|
||||
log.debug("generateLinks operatorId={} operatorType={} times={} perTimeQuantity={} needPoints={} expireHours={}",
|
||||
operatorId, operatorType, times, perTimeQuantity, needPoints, expireHours);
|
||||
}
|
||||
if (!isAdminOperator) {
|
||||
// 代理商自操作,需扣点判断
|
||||
long balance = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
|
||||
long balance = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
|
||||
if (balance < needPoints) {
|
||||
throw new IllegalStateException("点数不足");
|
||||
}
|
||||
}
|
||||
|
||||
LinkBatch batch = new LinkBatch();
|
||||
batch.setAgentId(target.getId());
|
||||
batch.setAgentId(operator.getId());
|
||||
batch.setQuantity(times);
|
||||
batch.setTimes(times);
|
||||
batch.setBatchSize(perTimeQuantity);
|
||||
@@ -98,7 +95,7 @@ public class LinkGenerationService {
|
||||
for (int i = 0; i < times; i++) {
|
||||
LinkTask t = new LinkTask();
|
||||
t.setBatchId(batch.getId());
|
||||
t.setAgentId(target.getId());
|
||||
t.setAgentId(operator.getId());
|
||||
t.setCodeNo(generateCodeNo());
|
||||
t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
|
||||
t.setExpireAt(expireAt);
|
||||
@@ -109,11 +106,11 @@ public class LinkGenerationService {
|
||||
|
||||
if (!isAdminOperator) {
|
||||
// 扣点流水 + 账户余额
|
||||
long before = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
|
||||
long before = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
|
||||
long after = before - needPoints;
|
||||
|
||||
AgentPointsTx tx = new AgentPointsTx();
|
||||
tx.setAccountId(target.getId());
|
||||
tx.setAccountId(operator.getId());
|
||||
tx.setType("DEDUCT");
|
||||
tx.setBeforePoints(before);
|
||||
tx.setDeltaPoints(-needPoints);
|
||||
@@ -124,7 +121,7 @@ public class LinkGenerationService {
|
||||
agentPointsTxMapper.insert(tx);
|
||||
|
||||
UserAccount patch = new UserAccount();
|
||||
patch.setId(target.getId());
|
||||
patch.setId(operator.getId());
|
||||
patch.setPointsBalance(after);
|
||||
userAccountMapper.update(patch);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user